mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Move theme from ConfigNotifier to ThemeNotifier
Summary: This reverts the behavior of onOptions, which had unintentionally changed recently and no longer matched the API documentation. Test Plan: Existing tests. Reviewers: jarek Reviewed By: jarek Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4064
This commit is contained in:
parent
a101337213
commit
1be1e5f647
@ -11,6 +11,7 @@ import {
|
|||||||
MinimumLevel,
|
MinimumLevel,
|
||||||
RecordNotifier,
|
RecordNotifier,
|
||||||
TableNotifier,
|
TableNotifier,
|
||||||
|
ThemeNotifier,
|
||||||
WidgetAPIImpl,
|
WidgetAPIImpl,
|
||||||
WidgetFrame
|
WidgetFrame
|
||||||
} from 'app/client/components/WidgetFrame';
|
} from 'app/client/components/WidgetFrame';
|
||||||
@ -215,6 +216,7 @@ export class CustomView extends Disposable {
|
|||||||
}) {
|
}) {
|
||||||
const {baseUrl, access, showAfterReady} = options;
|
const {baseUrl, access, showAfterReady} = options;
|
||||||
const documentSettings = this.gristDoc.docData.docSettings();
|
const documentSettings = this.gristDoc.docData.docSettings();
|
||||||
|
const readonly = this.gristDoc.isReadonly.get();
|
||||||
return grains.create(WidgetFrame, {
|
return grains.create(WidgetFrame, {
|
||||||
url: baseUrl || this.getEmptyWidgetPage(),
|
url: baseUrl || this.getEmptyWidgetPage(),
|
||||||
access,
|
access,
|
||||||
@ -225,13 +227,8 @@ export class CustomView extends Disposable {
|
|||||||
timeZone: this.gristDoc.docInfo.timezone() ?? "UTC",
|
timeZone: this.gristDoc.docInfo.timezone() ?? "UTC",
|
||||||
currency: documentSettings.currency?? "USD",
|
currency: documentSettings.currency?? "USD",
|
||||||
},
|
},
|
||||||
readonly: this.gristDoc.isReadonly.get(),
|
readonly,
|
||||||
showAfterReady,
|
showAfterReady,
|
||||||
onSettingsInitialized: async () => {
|
|
||||||
if (!this.customDef.renderAfterReady.peek()) {
|
|
||||||
await this.customDef.renderAfterReady.setAndSave(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
configure: (frame) => {
|
configure: (frame) => {
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
// Need to cast myself to a BaseView
|
// Need to cast myself to a BaseView
|
||||||
@ -266,6 +263,9 @@ export class CustomView extends Disposable {
|
|||||||
theme: this.gristDoc.currentTheme,
|
theme: this.gristDoc.currentTheme,
|
||||||
}),
|
}),
|
||||||
new MinimumLevel(AccessLevel.none)); // none access is enough
|
new MinimumLevel(AccessLevel.none)); // none access is enough
|
||||||
|
frame.useEvents(
|
||||||
|
ThemeNotifier.create(frame, this.gristDoc.currentTheme),
|
||||||
|
new MinimumLevel(AccessLevel.none));
|
||||||
},
|
},
|
||||||
onElem: (iframe) => onFrameFocus(iframe, () => {
|
onElem: (iframe) => onFrameFocus(iframe, () => {
|
||||||
if (this.isDisposed()) { return; }
|
if (this.isDisposed()) { return; }
|
||||||
|
@ -54,17 +54,10 @@ export interface WidgetFrameOptions {
|
|||||||
/**
|
/**
|
||||||
* If set, show the iframe after `grist.ready()`.
|
* If set, show the iframe after `grist.ready()`.
|
||||||
*
|
*
|
||||||
* Currently, this is only used to defer showing a widget until it has had
|
* This is used to defer showing a widget on initial load until it has finished
|
||||||
* a chance to apply the Grist theme.
|
* applying the Grist theme.
|
||||||
*/
|
*/
|
||||||
showAfterReady?: boolean;
|
showAfterReady?: boolean;
|
||||||
/**
|
|
||||||
* Handler for the settings initialized message.
|
|
||||||
*
|
|
||||||
* Currently, this is only used to defer showing a widget until it has had
|
|
||||||
* a chance to apply the Grist theme.
|
|
||||||
*/
|
|
||||||
onSettingsInitialized: () => void;
|
|
||||||
/**
|
/**
|
||||||
* Optional callback to configure exposed API.
|
* Optional callback to configure exposed API.
|
||||||
*/
|
*/
|
||||||
@ -216,9 +209,12 @@ export class WidgetFrame extends DisposableWithEvents {
|
|||||||
this.trigger('ready', this);
|
this.trigger('ready', this);
|
||||||
this._readyCalled.set(true);
|
this._readyCalled.set(true);
|
||||||
}
|
}
|
||||||
if (event.data.data?.settings?.status === 'initialized') {
|
if (
|
||||||
|
event.data.data?.message === 'themeInitialized' ||
|
||||||
|
// DEPRECATED: remove once the plugin API starts sending the message above.
|
||||||
|
event.data.data?.settings?.status === 'initialized'
|
||||||
|
) {
|
||||||
this._visible.set(true);
|
this._visible.set(true);
|
||||||
this._options.onSettingsInitialized();
|
|
||||||
}
|
}
|
||||||
this._rpc.receiveMessage(event.data);
|
this._rpc.receiveMessage(event.data);
|
||||||
}
|
}
|
||||||
@ -580,56 +576,79 @@ export interface ConfigNotifierOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies about options change. Exposed in the API as a onOptions handler.
|
* Notifies about options changes. Exposed in the API as `onOptions`.
|
||||||
*/
|
*/
|
||||||
export class ConfigNotifier extends BaseEventSource {
|
export class ConfigNotifier extends BaseEventSource {
|
||||||
private _accessLevel = this._options.access;
|
private _accessLevel = this._options.access;
|
||||||
private _theme = this._options.theme;
|
private _theme = this._options.theme;
|
||||||
private _currentConfig: Computed<any | null>;
|
private _currentConfig = Computed.create(this, use => {
|
||||||
// Debounced call to let the view know linked cursor changed.
|
|
||||||
private _debounced: (fromReady?: boolean) => void;
|
|
||||||
constructor(private _section: ViewSectionRec, private _options: ConfigNotifierOptions) {
|
|
||||||
super();
|
|
||||||
this._currentConfig = Computed.create(this, use => {
|
|
||||||
const options = use(this._section.activeCustomOptions);
|
const options = use(this._section.activeCustomOptions);
|
||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
this._debounced = debounce((fromReady?: boolean) => this._update(fromReady), 0);
|
|
||||||
const subscribe = (...observables: Observable<any>[]) => {
|
// Debounced call to let the view know linked cursor changed.
|
||||||
for (const obs of observables) {
|
private _debounced = debounce((options?: {fromReady?: boolean}) => this._update(options), 0);
|
||||||
|
|
||||||
|
constructor(private _section: ViewSectionRec, private _options: ConfigNotifierOptions) {
|
||||||
|
super();
|
||||||
this.autoDispose(
|
this.autoDispose(
|
||||||
obs.addListener((cur, prev) => {
|
this._currentConfig.addListener((newConfig, oldConfig) => {
|
||||||
if (isEqual(prev, cur)) {
|
if (isEqual(newConfig, oldConfig)) { return; }
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._debounced();
|
this._debounced();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
subscribe(this._currentConfig, this._theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _ready() {
|
protected _ready() {
|
||||||
// On ready, send initial configuration.
|
// On ready, send initial configuration.
|
||||||
this._debounced(true);
|
this._debounced({fromReady: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _update(fromReady = false) {
|
private _update({fromReady}: {fromReady?: boolean} = {}) {
|
||||||
if (this.isDisposed()) {
|
if (this.isDisposed()) { return; }
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._notify({
|
this._notify({
|
||||||
options: this._currentConfig.get(),
|
options: this._currentConfig.get(),
|
||||||
settings: {
|
settings: {
|
||||||
accessLevel: this._accessLevel,
|
accessLevel: this._accessLevel,
|
||||||
theme: this._theme.get(),
|
// DEPRECATED: remove once the plugin API includes the `onThemeChange` handler.
|
||||||
|
...(fromReady ? {theme: this._theme.get()} : undefined),
|
||||||
},
|
},
|
||||||
fromReady,
|
fromReady,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies about theme changes. Exposed in the API as `onThemeChange`.
|
||||||
|
*/
|
||||||
|
export class ThemeNotifier extends BaseEventSource {
|
||||||
|
constructor(private _theme: Computed<Theme>) {
|
||||||
|
super();
|
||||||
|
this.autoDispose(
|
||||||
|
this._theme.addListener((newTheme, oldTheme) => {
|
||||||
|
if (isEqual(newTheme, oldTheme)) { return; }
|
||||||
|
|
||||||
|
this._update();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _ready() {
|
||||||
|
this._update({fromReady: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _update({fromReady}: {fromReady?: boolean} = {}) {
|
||||||
|
if (this.isDisposed()) { return; }
|
||||||
|
|
||||||
|
this._notify({
|
||||||
|
theme: this._theme.get(),
|
||||||
|
fromReady,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies about cursor table data or structure change.
|
* Notifies about cursor table data or structure change.
|
||||||
* Exposed in the API as a onRecords handler.
|
* Exposed in the API as a onRecords handler.
|
||||||
|
@ -2,6 +2,8 @@ import {ClientScope} from 'app/client/components/ClientScope';
|
|||||||
import {SafeBrowser} from 'app/client/lib/SafeBrowser';
|
import {SafeBrowser} from 'app/client/lib/SafeBrowser';
|
||||||
import {LocalPlugin} from 'app/common/plugin';
|
import {LocalPlugin} from 'app/common/plugin';
|
||||||
import {createRpcLogger, PluginInstance} from 'app/common/PluginInstance';
|
import {createRpcLogger, PluginInstance} from 'app/common/PluginInstance';
|
||||||
|
import {Theme} from 'app/common/ThemePrefs';
|
||||||
|
import {Computed} from 'grainjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Home plugins are all plugins that contributes to a general Grist management tasks.
|
* Home plugins are all plugins that contributes to a general Grist management tasks.
|
||||||
@ -13,9 +15,13 @@ export class HomePluginManager {
|
|||||||
|
|
||||||
public pluginsList: PluginInstance[];
|
public pluginsList: PluginInstance[];
|
||||||
|
|
||||||
constructor(localPlugins: LocalPlugin[],
|
constructor(options: {
|
||||||
|
localPlugins: LocalPlugin[],
|
||||||
untrustedContentOrigin: string,
|
untrustedContentOrigin: string,
|
||||||
clientScope: ClientScope) {
|
clientScope: ClientScope,
|
||||||
|
theme: Computed<Theme>,
|
||||||
|
}) {
|
||||||
|
const {localPlugins, untrustedContentOrigin, clientScope, theme} = options;
|
||||||
this.pluginsList = [];
|
this.pluginsList = [];
|
||||||
for (const plugin of localPlugins) {
|
for (const plugin of localPlugins) {
|
||||||
try {
|
try {
|
||||||
@ -35,6 +41,7 @@ export class HomePluginManager {
|
|||||||
clientScope,
|
clientScope,
|
||||||
untrustedContentOrigin,
|
untrustedContentOrigin,
|
||||||
mainPath: components.safeBrowser,
|
mainPath: components.safeBrowser,
|
||||||
|
theme,
|
||||||
});
|
});
|
||||||
if (components.safeBrowser) {
|
if (components.safeBrowser) {
|
||||||
pluginInstance.rpc.registerForwarder(components.safeBrowser, safeBrowser);
|
pluginInstance.rpc.registerForwarder(components.safeBrowser, safeBrowser);
|
||||||
|
@ -73,7 +73,7 @@ export class SafeBrowser extends BaseComponent {
|
|||||||
new IframeProcess(safeBrowser, rpc, src);
|
new IframeProcess(safeBrowser, rpc, src);
|
||||||
}
|
}
|
||||||
|
|
||||||
public theme? = this._options.theme;
|
public theme = this._options.theme;
|
||||||
|
|
||||||
// All view processes. This is not used anymore to dispose all processes on deactivation (this is
|
// All view processes. This is not used anymore to dispose all processes on deactivation (this is
|
||||||
// now achieved using `this._mainProcess.autoDispose(...)`) but rather to be able to dispatch
|
// now achieved using `this._mainProcess.autoDispose(...)`) but rather to be able to dispatch
|
||||||
@ -94,10 +94,10 @@ export class SafeBrowser extends BaseComponent {
|
|||||||
pluginInstance: PluginInstance,
|
pluginInstance: PluginInstance,
|
||||||
clientScope: ClientScope,
|
clientScope: ClientScope,
|
||||||
untrustedContentOrigin: string,
|
untrustedContentOrigin: string,
|
||||||
|
theme: Computed<Theme>,
|
||||||
mainPath?: string,
|
mainPath?: string,
|
||||||
baseLogger?: BaseLogger,
|
baseLogger?: BaseLogger,
|
||||||
rpcLogger?: IRpcLogger,
|
rpcLogger?: IRpcLogger,
|
||||||
theme?: Computed<Theme>,
|
|
||||||
}) {
|
}) {
|
||||||
super(
|
super(
|
||||||
_options.pluginInstance.definition.manifest,
|
_options.pluginInstance.definition.manifest,
|
||||||
@ -292,8 +292,8 @@ class WorkerProcess extends ClientProcess {
|
|||||||
export class ViewProcess extends ClientProcess {
|
export class ViewProcess extends ClientProcess {
|
||||||
public element: HTMLElement;
|
public element: HTMLElement;
|
||||||
|
|
||||||
// Set once all of the plugin's onOptions handlers have been called.
|
// Set once all of the plugin's onThemeChange handlers have been called.
|
||||||
protected _optionsInitialized: Observable<boolean>;
|
protected _themeInitialized: Observable<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -302,21 +302,21 @@ export class ViewProcess extends ClientProcess {
|
|||||||
class IframeProcess extends ViewProcess {
|
class IframeProcess extends ViewProcess {
|
||||||
public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {
|
public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {
|
||||||
super.create(safeBrowser, rpc, src);
|
super.create(safeBrowser, rpc, src);
|
||||||
this._optionsInitialized = Observable.create(this, false);
|
this._themeInitialized = Observable.create(this, false);
|
||||||
const iframe = this.element = this.autoDispose(
|
const iframe = this.element = this.autoDispose(
|
||||||
grainjsDom(`iframe.safe_browser_process.clipboard_focus`,
|
grainjsDom(`iframe.safe_browser_process.clipboard_focus`,
|
||||||
{src},
|
{src},
|
||||||
grainjsDom.style('visibility', use => use(this._optionsInitialized) ? 'visible' : 'hidden'),
|
grainjsDom.style('visibility', use => use(this._themeInitialized) ? 'visible' : 'hidden'),
|
||||||
) as HTMLIFrameElement
|
) as HTMLIFrameElement
|
||||||
);
|
);
|
||||||
const listener = async (event: MessageEvent) => {
|
const listener = async (event: MessageEvent) => {
|
||||||
if (event.source === iframe.contentWindow) {
|
if (event.source === iframe.contentWindow) {
|
||||||
if (event.data.mtype === MsgType.Ready) {
|
if (event.data.mtype === MsgType.Ready) {
|
||||||
await this._sendSettings({theme: safeBrowser.theme?.get()}, true);
|
await this._sendTheme({theme: safeBrowser.theme.get(), fromReady: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.data.data?.settings?.status === 'initialized') {
|
if (event.data.data?.message === 'themeInitialized') {
|
||||||
this._optionsInitialized.set(true);
|
this._themeInitialized.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rpc.receiveMessage(event.data);
|
this.rpc.receiveMessage(event.data);
|
||||||
@ -333,14 +333,14 @@ class IframeProcess extends ViewProcess {
|
|||||||
safeBrowser.theme.addListener(async (newTheme, oldTheme) => {
|
safeBrowser.theme.addListener(async (newTheme, oldTheme) => {
|
||||||
if (isEqual(newTheme, oldTheme)) { return; }
|
if (isEqual(newTheme, oldTheme)) { return; }
|
||||||
|
|
||||||
await this._sendSettings({theme: safeBrowser.theme?.get()});
|
await this._sendTheme({theme: newTheme});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _sendSettings(settings: {theme?: Theme}, fromReady = false) {
|
private async _sendTheme({theme, fromReady = false}: {theme: Theme, fromReady?: boolean}) {
|
||||||
await this.rpc.postMessage({settings, fromReady});
|
await this.rpc.postMessage({theme, fromReady});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,10 +186,12 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
|||||||
this._updateWorkspaces().catch(reportError)));
|
this._updateWorkspaces().catch(reportError)));
|
||||||
|
|
||||||
// Defer home plugin initialization
|
// Defer home plugin initialization
|
||||||
const pluginManager = new HomePluginManager(
|
const pluginManager = new HomePluginManager({
|
||||||
_app.topAppModel.plugins,
|
localPlugins: _app.topAppModel.plugins,
|
||||||
_app.topAppModel.getUntrustedContentOrigin()!,
|
untrustedContentOrigin: _app.topAppModel.getUntrustedContentOrigin()!,
|
||||||
clientScope);
|
clientScope,
|
||||||
|
theme: _app.currentTheme,
|
||||||
|
});
|
||||||
const importSources = ImportSourceElement.fromArray(pluginManager.pluginsList);
|
const importSources = ImportSourceElement.fromArray(pluginManager.pluginsList);
|
||||||
this.importSources.set(importSources);
|
this.importSources.set(importSources);
|
||||||
|
|
||||||
|
@ -272,8 +272,8 @@ export interface CustomViewSectionDef {
|
|||||||
/**
|
/**
|
||||||
* If set, render the widget after `grist.ready()`.
|
* If set, render the widget after `grist.ready()`.
|
||||||
*
|
*
|
||||||
* Currently, this is only used to defer rendering a widget until it has had
|
* This is used to defer showing a widget on initial load until it has finished
|
||||||
* a chance to apply the Grist theme.
|
* applying the Grist theme.
|
||||||
*/
|
*/
|
||||||
renderAfterReady: modelUtil.KoSaveableObservable<boolean>;
|
renderAfterReady: modelUtil.KoSaveableObservable<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -459,7 +459,7 @@ export class CustomSectionConfig extends Disposable {
|
|||||||
}
|
}
|
||||||
bundleChanges(() => {
|
bundleChanges(() => {
|
||||||
// Reset whether widget should render after `grist.ready()`.
|
// Reset whether widget should render after `grist.ready()`.
|
||||||
_section.customDef.renderAfterReady(false);
|
_section.customDef.renderAfterReady(selectedWidget.renderAfterReady ?? false);
|
||||||
// Clear access level
|
// Clear access level
|
||||||
_section.customDef.access(AccessLevel.none);
|
_section.customDef.access(AccessLevel.none);
|
||||||
// When widget wants some access, set desired access level.
|
// When widget wants some access, set desired access level.
|
||||||
@ -627,16 +627,16 @@ export class CustomSectionConfig extends Disposable {
|
|||||||
|
|
||||||
protected async _getWidgets() {
|
protected async _getWidgets() {
|
||||||
const api = this._gristDoc.app.topAppModel.api;
|
const api = this._gristDoc.app.topAppModel.api;
|
||||||
const wigets = await api.getWidgets();
|
const widgets = await api.getWidgets();
|
||||||
// Request for rest of the widgets.
|
// Request for rest of the widgets.
|
||||||
if (this._canSelect) {
|
if (this._canSelect) {
|
||||||
// From the start we will provide single widget definition
|
// From the start we will provide single widget definition
|
||||||
// that was chosen previously.
|
// that was chosen previously.
|
||||||
if (this._section.customDef.widgetDef.peek()) {
|
if (this._section.customDef.widgetDef.peek()) {
|
||||||
wigets.push(this._section.customDef.widgetDef.peek()!);
|
widgets.push(this._section.customDef.widgetDef.peek()!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._widgets.set(wigets);
|
this._widgets.set(widgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _accept() {
|
private _accept() {
|
||||||
|
@ -21,8 +21,8 @@ export interface ICustomWidget {
|
|||||||
/**
|
/**
|
||||||
* If set, Grist will render the widget after `grist.ready()`.
|
* If set, Grist will render the widget after `grist.ready()`.
|
||||||
*
|
*
|
||||||
* Currently, this is only used to defer rendering a widget until it has had
|
* This is used to defer showing a widget on initial load until it has finished
|
||||||
* a chance to apply the Grist theme.
|
* applying the Grist theme.
|
||||||
*/
|
*/
|
||||||
renderAfterReady?: boolean;
|
renderAfterReady?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ export const InteractionOptionsRequest = t.iface([], {
|
|||||||
|
|
||||||
export const InteractionOptions = t.iface([], {
|
export const InteractionOptions = t.iface([], {
|
||||||
"accessLevel": "string",
|
"accessLevel": "string",
|
||||||
"theme": "any",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WidgetColumnMap = t.iface([], {
|
export const WidgetColumnMap = t.iface([], {
|
||||||
|
@ -69,16 +69,6 @@ export interface InteractionOptions{
|
|||||||
* Granted access level.
|
* Granted access level.
|
||||||
*/
|
*/
|
||||||
accessLevel: string,
|
accessLevel: string,
|
||||||
/**
|
|
||||||
* Information about the current Grist theme.
|
|
||||||
*
|
|
||||||
* Includes the theme appearance ("light" or "dark"), and a mapping of UI elements to
|
|
||||||
* CSS color values. The CSS values are also accessible within a widget via CSS variables
|
|
||||||
* prefixed with "--grist-theme-" (e.g. `var(--grist-theme-text)`).
|
|
||||||
*
|
|
||||||
* NOTE: the variables aren't yet finalized and may change in the future.
|
|
||||||
*/
|
|
||||||
theme: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -380,14 +380,6 @@ export function onRecords(callback: (data: RowRecord[], mappings: WidgetColumnMa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep track of the completion status of all initial onOptions calls made prior to sending
|
|
||||||
* the ready message. The `ready` function will wait for this list of calls to settle after
|
|
||||||
* it receives the initial options message from Grist.
|
|
||||||
*
|
|
||||||
* Note that this includes all internal calls to `onOptions`, such as the one made by
|
|
||||||
* `syncThemeWithGrist`. */
|
|
||||||
const _pendingInitializeOptionsCalls: Promise<unknown>[] = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For custom widgets, add a handler that will be called whenever the
|
* For custom widgets, add a handler that will be called whenever the
|
||||||
* widget options change (and on initial ready message). Handler will be
|
* widget options change (and on initial ready message). Handler will be
|
||||||
@ -396,16 +388,26 @@ const _pendingInitializeOptionsCalls: Promise<unknown>[] = [];
|
|||||||
* the document that contains it.
|
* the document that contains it.
|
||||||
*/
|
*/
|
||||||
export function onOptions(callback: (options: any, settings: InteractionOptions) => unknown) {
|
export function onOptions(callback: (options: any, settings: InteractionOptions) => unknown) {
|
||||||
let finishInitialization: () => void;
|
on('message', function(msg) {
|
||||||
if (!_readyCalled) {
|
|
||||||
const promise = new Promise<void>(resolve => { finishInitialization = resolve; });
|
|
||||||
_pendingInitializeOptionsCalls.push(promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
on('message', async function(msg) {
|
|
||||||
if (msg.settings) {
|
if (msg.settings) {
|
||||||
await callback(msg.options || null, msg.settings);
|
callback(msg.options || null, msg.settings);
|
||||||
finishInitialization?.();
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called whenever the Grist theme changes (and on initial ready message).
|
||||||
|
*/
|
||||||
|
function onThemeChange(callback: (theme: any) => unknown) {
|
||||||
|
on('message', function(msg) {
|
||||||
|
if (msg.theme) {
|
||||||
|
callback(msg.theme);
|
||||||
|
|
||||||
|
if (msg.fromReady) {
|
||||||
|
void (async function() {
|
||||||
|
await rpc.postMessage({message: 'themeInitialized'});
|
||||||
|
})();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -468,24 +470,6 @@ export function ready(settings?: ReadyPayload): void {
|
|||||||
rpc.registerFunc('editOptions', settings.onEditOptions);
|
rpc.registerFunc('editOptions', settings.onEditOptions);
|
||||||
}
|
}
|
||||||
on('message', async function(msg) {
|
on('message', async function(msg) {
|
||||||
if (msg.settings && msg.fromReady) {
|
|
||||||
/* Grist sends an options message immediately after receiving a ready message, containing
|
|
||||||
* some initial settings (such as the Grist theme). Grist may decide to wait until all
|
|
||||||
* initial onOptions calls have completed before making the custom widget's iframe visible
|
|
||||||
* to the client. This is the case when a widget's manifest explicitly declares a widget
|
|
||||||
* must be rendered after the ready, or in subsequent renders of a custom widget that
|
|
||||||
* previously sent Grist a ready message. The reason for this behavior is to make the
|
|
||||||
* experience of embedding iframes within a Grist document feel more seamless and polished.
|
|
||||||
*
|
|
||||||
* Here, we check for that initial options message, waiting for all onOptions calls to
|
|
||||||
* settle before notifying Grist that all settings have been initialized. */
|
|
||||||
await Promise.all(_pendingInitializeOptionsCalls);
|
|
||||||
|
|
||||||
void (async function() {
|
|
||||||
await rpc.postMessage({settings: {status: 'initialized'}});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.tableId && msg.tableId !== _tableId) {
|
if (msg.tableId && msg.tableId !== _tableId) {
|
||||||
if (!_tableId) { _setInitialized(); }
|
if (!_tableId) { _setInitialized(); }
|
||||||
_tableId = msg.tableId;
|
_tableId = msg.tableId;
|
||||||
@ -566,14 +550,12 @@ function createRpcLogger(): IRpcLogger {
|
|||||||
|
|
||||||
let _theme: any;
|
let _theme: any;
|
||||||
|
|
||||||
function syncThemeWithGrist() {
|
onThemeChange((newTheme) => {
|
||||||
onOptions((_options, settings) => {
|
if (isEqual(newTheme, _theme)) { return; }
|
||||||
if (settings.theme && !isEqual(settings.theme, _theme)) {
|
|
||||||
_theme = settings.theme;
|
_theme = newTheme;
|
||||||
attachCssThemeVars(_theme);
|
attachCssThemeVars(_theme);
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachCssThemeVars({appearance, colors}: any) {
|
function attachCssThemeVars({appearance, colors}: any) {
|
||||||
// Prepare the custom properties needed for applying the theme.
|
// Prepare the custom properties needed for applying the theme.
|
||||||
@ -613,5 +595,3 @@ function getOrCreateStyleElement(id: string) {
|
|||||||
document.head.append(style);
|
document.head.append(style);
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
syncThemeWithGrist();
|
|
||||||
|
@ -3,11 +3,13 @@ import { Disposable } from 'app/client/lib/dispose';
|
|||||||
import { ClientProcess, SafeBrowser } from 'app/client/lib/SafeBrowser';
|
import { ClientProcess, SafeBrowser } from 'app/client/lib/SafeBrowser';
|
||||||
import { LocalPlugin } from 'app/common/plugin';
|
import { LocalPlugin } from 'app/common/plugin';
|
||||||
import { PluginInstance } from 'app/common/PluginInstance';
|
import { PluginInstance } from 'app/common/PluginInstance';
|
||||||
|
import { GristLight } from 'app/common/themes/GristLight';
|
||||||
import { GristAPI, RPC_GRISTAPI_INTERFACE } from 'app/plugin/GristAPI';
|
import { GristAPI, RPC_GRISTAPI_INTERFACE } from 'app/plugin/GristAPI';
|
||||||
import { Storage } from 'app/plugin/StorageAPI';
|
import { Storage } from 'app/plugin/StorageAPI';
|
||||||
import { checkers } from 'app/plugin/TypeCheckers';
|
import { checkers } from 'app/plugin/TypeCheckers';
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { Rpc } from 'grain-rpc';
|
import { Rpc } from 'grain-rpc';
|
||||||
|
import { Computed } from 'grainjs';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import { basename } from 'path';
|
import { basename } from 'path';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
@ -186,6 +188,7 @@ describe('SafeBrowser', function() {
|
|||||||
untrustedContentOrigin: '',
|
untrustedContentOrigin: '',
|
||||||
mainPath,
|
mainPath,
|
||||||
baseLogger: {},
|
baseLogger: {},
|
||||||
|
theme: Computed.create(null, () => ({appearance: 'light', colors: GristLight})),
|
||||||
});
|
});
|
||||||
cleanup.push(() => safeBrowser.deactivate());
|
cleanup.push(() => safeBrowser.deactivate());
|
||||||
pluginInstance.rpc.registerForwarder(mainPath, safeBrowser);
|
pluginInstance.rpc.registerForwarder(mainPath, safeBrowser);
|
||||||
|
Loading…
Reference in New Issue
Block a user