diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2024-02-20 10:12:27 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-02-20 15:12:27 +0000 | 
| commit | 0e4ae922451af967c78616057ed26b85ba5d4b5c (patch) | |
| tree | 8f1aed3d93e9c6e99e6c55e846b29d8dc154e113 | |
| parent | 088c6c17ac7b6076604fd3bc40287d4afda0d940 (diff) | |
Popup preview frame API map (#712)
* Add API map type safety
* Add API map types
* Simplify names
* Remove unused type
| -rw-r--r-- | ext/js/app/popup.js | 15 | ||||
| -rw-r--r-- | ext/js/pages/settings/popup-preview-controller.js | 17 | ||||
| -rw-r--r-- | ext/js/pages/settings/popup-preview-frame.js | 62 | ||||
| -rw-r--r-- | types/ext/core.d.ts | 3 | ||||
| -rw-r--r-- | types/ext/popup-preview-frame.d.ts | 75 | 
5 files changed, 115 insertions, 57 deletions
| diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index 7a8b3f8c..4caf8241 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -722,22 +722,25 @@ export class Popup extends EventDispatcher {      }      /** -     * @param {string} action -     * @param {import('core').SerializableObject} params +     * @template {import('display').WindowApiNames} TName +     * @param {TName} action +     * @param {import('display').WindowApiParams<TName>} params       */ -    _invokeWindow(action, params = {}) { +    _invokeWindow(action, params) {          const contentWindow = this._frame.contentWindow;          if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; } -        const message = this._frameClient.createMessage({action, params}); -        contentWindow.postMessage(message, this._targetOrigin); +        /** @type {import('display').WindowApiMessage<TName>} */ +        const message = {action, params}; +        const messageWrapper = this._frameClient.createMessage(message); +        contentWindow.postMessage(messageWrapper, this._targetOrigin);      }      /**       * @returns {void}       */      _onExtensionUnloaded() { -        this._invokeWindow('displayExtensionUnloaded'); +        this._invokeWindow('displayExtensionUnloaded', void 0);      }      /** diff --git a/ext/js/pages/settings/popup-preview-controller.js b/ext/js/pages/settings/popup-preview-controller.js index 8361809d..e661b738 100644 --- a/ext/js/pages/settings/popup-preview-controller.js +++ b/ext/js/pages/settings/popup-preview-controller.js @@ -71,38 +71,41 @@ export class PopupPreviewController {      /** */      _onCustomCssChange() {          const css = /** @type {HTMLTextAreaElement} */ (this._customCss).value; -        this._invoke('PopupPreviewFrame.setCustomCss', {css}); +        this._invoke('setCustomCss', {css});      }      /** */      _onCustomOuterCssChange() {          const css = /** @type {HTMLTextAreaElement} */ (this._customOuterCss).value; -        this._invoke('PopupPreviewFrame.setCustomOuterCss', {css}); +        this._invoke('setCustomOuterCss', {css});      }      /** */      _onOptionsContextChange() {          const optionsContext = this._settingsController.getOptionsContext(); -        this._invoke('PopupPreviewFrame.updateOptionsContext', {optionsContext}); +        this._invoke('updateOptionsContext', {optionsContext});      }      /**       * @param {import('settings-controller').EventArgument<'optionsChanged'>} details       */      _onOptionsChanged({options}) { -        this._invoke('PopupPreviewFrame.optionsChanged', {options}); +        this._invoke('setLanguageExampleText', {language: options.general.language});      }      /**       * @param {import('dom-data-binder').SettingChangedEvent} settingChangedEvent       */      _onLanguageSelectChanged(settingChangedEvent) { -        this._invoke('PopupPreviewFrame.setLanguageExampleText', {language: settingChangedEvent.detail.value}); +        const {value} = settingChangedEvent.detail; +        if (typeof value !== 'string') { return; } +        this._invoke('setLanguageExampleText', {language: value});      }      /** -     * @param {string} action -     * @param {import('core').SerializableObject} params +     * @template {import('popup-preview-frame').ApiNames} TName +     * @param {TName} action +     * @param {import('popup-preview-frame').ApiParams<TName>} params       */      _invoke(action, params) {          if (this._frame === null || this._frame.contentWindow === null) { return; } diff --git a/ext/js/pages/settings/popup-preview-frame.js b/ext/js/pages/settings/popup-preview-frame.js index 8d881cc6..ad6e420f 100644 --- a/ext/js/pages/settings/popup-preview-frame.js +++ b/ext/js/pages/settings/popup-preview-frame.js @@ -18,6 +18,7 @@  import * as wanakana from '../../../lib/wanakana.js';  import {Frontend} from '../../app/frontend.js'; +import {createApiMap, invokeApiMapHandler} from '../../core/api-map.js';  import {querySelectorNotNull} from '../../dom/query-selector.js';  import {TextSourceRange} from '../../dom/text-source-range.js'; @@ -58,15 +59,14 @@ export class PopupPreviewFrame {          this._wanakanaBound = false;          /* eslint-disable @stylistic/no-multi-spaces */ -        /** @type {Map<string, (params: import('core').SerializableObjectAny) => void>} */ -        this._windowMessageHandlers = new Map(/** @type {[key: string, handler: (params: import('core').SerializableObjectAny) => void][]} */ ([ -            ['PopupPreviewFrame.setText',                this._onSetText.bind(this)], -            ['PopupPreviewFrame.setCustomCss',           this._setCustomCss.bind(this)], -            ['PopupPreviewFrame.setCustomOuterCss',      this._setCustomOuterCss.bind(this)], -            ['PopupPreviewFrame.updateOptionsContext',   this._updateOptionsContext.bind(this)], -            ['PopupPreviewFrame.optionsChanged',         this._onOptionsChanged.bind(this)], -            ['PopupPreviewFrame.setLanguageExampleText', this._setLanguageExampleText.bind(this)] -        ])); +        /** @type {import('popup-preview-frame').ApiMap} */ +        this._windowMessageHandlers = createApiMap([ +            ['setText',                this._onSetText.bind(this)], +            ['setCustomCss',           this._setCustomCss.bind(this)], +            ['setCustomOuterCss',      this._setCustomOuterCss.bind(this)], +            ['updateOptionsContext',   this._updateOptionsContext.bind(this)], +            ['setLanguageExampleText', this._setLanguageExampleText.bind(this)] +        ]);          /* eslint-enable @stylistic/no-multi-spaces */      } @@ -89,7 +89,7 @@ export class PopupPreviewFrame {          this._languageSummaries = await this._application.api.getLanguageSummaries();          const options = await this._application.api.optionsGet({current: true}); -        void this._onOptionsChanged({options, optionsContext: {current: true}}); +        void this._setLanguageExampleText({language: options.general.language});          // Overwrite frontend          this._frontend = new Frontend({ @@ -156,16 +156,13 @@ export class PopupPreviewFrame {      }      /** -     * @param {MessageEvent<{action: string, params: import('core').SerializableObject}>} e +     * @param {MessageEvent<import('popup-preview-frame.js').ApiMessageAny>} event       */ -    _onMessage(e) { -        if (e.origin !== this._targetOrigin) { return; } - -        const {action, params} = e.data; -        const handler = this._windowMessageHandlers.get(action); -        if (typeof handler !== 'function') { return; } - -        handler(params); +    _onMessage(event) { +        if (event.origin !== this._targetOrigin) { return; } +        const {action, params} = event.data; +        const callback = () => {}; // NOP +        invokeApiMapHandler(this._windowMessageHandlers, action, params, [], callback);      }      /** @@ -209,9 +206,7 @@ export class PopupPreviewFrame {          this._setText(element.value, false);      } -    /** -     * @param {{text: string}} details -     */ +    /** @type {import('popup-preview-frame').ApiHandler<'setText'>} */      _onSetText({text}) {          this._setText(text, true);      } @@ -242,9 +237,7 @@ export class PopupPreviewFrame {          node.classList.toggle('placeholder-info-visible', visible);      } -    /** -     * @param {{css: string}} details -     */ +    /** @type {import('popup-preview-frame').ApiHandler<'setCustomCss'>} */      _setCustomCss({css}) {          if (this._frontend === null) { return; }          const popup = this._frontend.popup; @@ -252,9 +245,7 @@ export class PopupPreviewFrame {          void popup.setCustomCss(css);      } -    /** -     * @param {{css: string}} details -     */ +    /** @type {import('popup-preview-frame').ApiHandler<'setCustomOuterCss'>} */      _setCustomOuterCss({css}) {          if (this._frontend === null) { return; }          const popup = this._frontend.popup; @@ -262,9 +253,7 @@ export class PopupPreviewFrame {          void popup.setCustomOuterCss(css, false);      } -    /** -     * @param {{optionsContext: import('settings').OptionsContext}} details -     */ +    /** @type {import('popup-preview-frame').ApiHandler<'updateOptionsContext'>} */      async _updateOptionsContext(details) {          const {optionsContext} = details;          this._optionsContext = optionsContext; @@ -274,16 +263,7 @@ export class PopupPreviewFrame {          await this._updateSearch();      } -    /** -     * @param {import('settings-controller').EventArgument<'optionsChanged'>} details -     */ -    async _onOptionsChanged({options: {general: {language}}}) { -        this._setLanguageExampleText({language}); -    } - -    /** -     * @param {{language: string}} details -     */ +    /** @type {import('popup-preview-frame').ApiHandler<'setLanguageExampleText'>} */      _setLanguageExampleText({language}) {          const activeLanguage = /** @type {import('language').LanguageSummary} */ (this._languageSummaries.find(({iso}) => iso === language)); diff --git a/types/ext/core.d.ts b/types/ext/core.d.ts index a18a7bf7..8e8184b3 100644 --- a/types/ext/core.d.ts +++ b/types/ext/core.d.ts @@ -32,9 +32,6 @@ export type RejectionReason = SafeAny;  export type SerializableObject = {[key: string]: unknown};  /** This type is used as an explicit way of permitting the `object` type. */ -export type SerializableObjectAny = {[key: string]: SafeAny}; - -/** This type is used as an explicit way of permitting the `object` type. */  export type UnknownObject = {[key: string | symbol]: unknown};  export type TokenString = string; diff --git a/types/ext/popup-preview-frame.d.ts b/types/ext/popup-preview-frame.d.ts new file mode 100644 index 00000000..4b4f3009 --- /dev/null +++ b/types/ext/popup-preview-frame.d.ts @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024  Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +import type { +    ApiMap as BaseApiMap, +    ApiHandler as BaseApiHandler, +    ApiParams as BaseApiParams, +    ApiNames as BaseApiNames, +    ApiReturn as BaseApiReturn, +} from './api-map'; +import type {OptionsContext} from './settings'; + +export type ApiSurface = { +    setText: { +        params: { +            text: string; +        }; +        return: void; +    }; +    setCustomCss: { +        params: { +            css: string; +        }; +        return: void; +    }; +    setCustomOuterCss: { +        params: { +            css: string; +        }; +        return: void; +    }; +    updateOptionsContext: { +        params: { +            optionsContext: OptionsContext; +        }; +        return: void; +    }; +    setLanguageExampleText: { +        params: { +            language: string; +        }; +        return: void; +    }; +}; + +export type ApiParams<TName extends ApiNames> = BaseApiParams<ApiSurface[TName]>; + +export type ApiNames = BaseApiNames<ApiSurface>; + +export type ApiMap = BaseApiMap<ApiSurface>; + +export type ApiHandler<TName extends ApiNames> = BaseApiHandler<ApiSurface[TName]>; + +export type ApiReturn<TName extends ApiNames> = BaseApiReturn<ApiSurface[TName]>; + +type ApiMessage<TName extends ApiNames> = { +    action: TName; +    params: ApiParams<TName>; +}; + +export type ApiMessageAny = {[name in ApiNames]: ApiMessage<name>}[ApiNames]; |