aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2024-02-20 10:12:27 -0500
committerGitHub <noreply@github.com>2024-02-20 15:12:27 +0000
commit0e4ae922451af967c78616057ed26b85ba5d4b5c (patch)
tree8f1aed3d93e9c6e99e6c55e846b29d8dc154e113
parent088c6c17ac7b6076604fd3bc40287d4afda0d940 (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.js15
-rw-r--r--ext/js/pages/settings/popup-preview-controller.js17
-rw-r--r--ext/js/pages/settings/popup-preview-frame.js62
-rw-r--r--types/ext/core.d.ts3
-rw-r--r--types/ext/popup-preview-frame.d.ts75
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];