summaryrefslogtreecommitdiff
path: root/ext/js/app/popup-factory.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/app/popup-factory.js')
-rw-r--r--ext/js/app/popup-factory.js182
1 files changed, 139 insertions, 43 deletions
diff --git a/ext/js/app/popup-factory.js b/ext/js/app/popup-factory.js
index e871f7ec..6fa50796 100644
--- a/ext/js/app/popup-factory.js
+++ b/ext/js/app/popup-factory.js
@@ -32,9 +32,13 @@ export class PopupFactory {
* @param {number} frameId The frame ID of the host frame.
*/
constructor(frameId) {
+ /** @type {number} */
this._frameId = frameId;
+ /** @type {FrameOffsetForwarder} */
this._frameOffsetForwarder = new FrameOffsetForwarder(frameId);
+ /** @type {Map<string, import('popup').PopupAny>} */
this._popups = new Map();
+ /** @type {Map<string, {popup: import('popup').PopupAny, token: string}[]>} */
this._allPopupVisibilityTokenMap = new Map();
}
@@ -46,17 +50,17 @@ export class PopupFactory {
yomitan.crossFrame.registerHandlers([
['PopupFactory.getOrCreatePopup', {async: true, handler: this._onApiGetOrCreatePopup.bind(this)}],
['PopupFactory.setOptionsContext', {async: true, handler: this._onApiSetOptionsContext.bind(this)}],
- ['PopupFactory.hide', {async: false, handler: this._onApiHide.bind(this)}],
+ ['PopupFactory.hide', {async: true, handler: this._onApiHide.bind(this)}],
['PopupFactory.isVisible', {async: true, handler: this._onApiIsVisibleAsync.bind(this)}],
['PopupFactory.setVisibleOverride', {async: true, handler: this._onApiSetVisibleOverride.bind(this)}],
['PopupFactory.clearVisibleOverride', {async: true, handler: this._onApiClearVisibleOverride.bind(this)}],
['PopupFactory.containsPoint', {async: true, handler: this._onApiContainsPoint.bind(this)}],
['PopupFactory.showContent', {async: true, handler: this._onApiShowContent.bind(this)}],
- ['PopupFactory.setCustomCss', {async: false, handler: this._onApiSetCustomCss.bind(this)}],
- ['PopupFactory.clearAutoPlayTimer', {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}],
- ['PopupFactory.setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}],
- ['PopupFactory.updateTheme', {async: false, handler: this._onApiUpdateTheme.bind(this)}],
- ['PopupFactory.setCustomOuterCss', {async: false, handler: this._onApiSetCustomOuterCss.bind(this)}],
+ ['PopupFactory.setCustomCss', {async: true, handler: this._onApiSetCustomCss.bind(this)}],
+ ['PopupFactory.clearAutoPlayTimer', {async: true, handler: this._onApiClearAutoPlayTimer.bind(this)}],
+ ['PopupFactory.setContentScale', {async: true, handler: this._onApiSetContentScale.bind(this)}],
+ ['PopupFactory.updateTheme', {async: true, handler: this._onApiUpdateTheme.bind(this)}],
+ ['PopupFactory.setCustomOuterCss', {async: true, handler: this._onApiSetCustomOuterCss.bind(this)}],
['PopupFactory.getFrameSize', {async: true, handler: this._onApiGetFrameSize.bind(this)}],
['PopupFactory.setFrameSize', {async: true, handler: this._onApiSetFrameSize.bind(this)}]
]);
@@ -64,14 +68,8 @@ export class PopupFactory {
/**
* Gets or creates a popup based on a set of parameters
- * @param {object} details Details about how to acquire the popup.
- * @param {?number} [details.frameId] The ID of the frame that should host the popup.
- * @param {?string} [details.id] A specific ID used to find an existing popup, or to assign to the new popup.
- * @param {?string} [details.parentPopupId] The ID of the parent popup.
- * @param {?number} [details.depth] A specific depth value to assign to the popup.
- * @param {boolean} [details.popupWindow] Whether or not a separate popup window should be used, rather than an iframe.
- * @param {boolean} [details.childrenSupported] Whether or not the popup is able to show child popups.
- * @returns {Popup|PopupWindow|PopupProxy} The new or existing popup.
+ * @param {import('popup-factory').GetOrCreatePopupDetails} details Details about how to acquire the popup.
+ * @returns {Promise<import('popup').PopupAny>}
*/
async getOrCreatePopup({
frameId=null,
@@ -140,7 +138,7 @@ export class PopupFactory {
if (parent.child !== null) {
throw new Error('Parent popup already has a child');
}
- popup.parent = parent;
+ popup.parent = /** @type {Popup} */ (parent);
parent.child = popup;
}
this._popups.set(id, popup);
@@ -151,16 +149,18 @@ export class PopupFactory {
throw new Error('Invalid frameId');
}
const useFrameOffsetForwarder = (parentPopupId === null);
- ({id, depth, frameId} = await yomitan.crossFrame.invoke(frameId, 'PopupFactory.getOrCreatePopup', {
+ /** @type {{id: string, depth: number, frameId: number}} */
+ const info = await yomitan.crossFrame.invoke(frameId, 'PopupFactory.getOrCreatePopup', /** @type {import('popup-factory').GetOrCreatePopupDetails} */ ({
id,
parentPopupId,
frameId,
childrenSupported
}));
+ id = info.id;
const popup = new PopupProxy({
id,
- depth,
- frameId,
+ depth: info.depth,
+ frameId: info.frameId,
frameOffsetForwarder: useFrameOffsetForwarder ? this._frameOffsetForwarder : null
});
this._popups.set(id, popup);
@@ -172,24 +172,34 @@ export class PopupFactory {
* Force all popups to have a specific visibility value.
* @param {boolean} value Whether or not the popups should be visible.
* @param {number} priority The priority of the override.
- * @returns {string} A token which can be passed to clearAllVisibleOverride.
+ * @returns {Promise<import('core').TokenString>} A token which can be passed to clearAllVisibleOverride.
* @throws An exception is thrown if any popup fails to have its visibiltiy overridden.
*/
async setAllVisibleOverride(value, priority) {
const promises = [];
- const errors = [];
for (const popup of this._popups.values()) {
- const promise = popup.setVisibleOverride(value, priority)
- .then(
- (token) => ({popup, token}),
- (error) => { errors.push(error); return null; }
- );
+ const promise = this._setPopupVisibleOverrideReturnTuple(popup, value, priority);
promises.push(promise);
}
- const results = (await Promise.all(promises)).filter(({token}) => token !== null);
+ /** @type {undefined|unknown} */
+ let error = void 0;
+ /** @type {{popup: import('popup').PopupAny, token: string}[]} */
+ const results = [];
+ for (const promise of promises) {
+ try {
+ const {popup, token} = await promise;
+ if (token !== null) {
+ results.push({popup, token});
+ }
+ } catch (e) {
+ if (typeof error === 'undefined') {
+ error = new Error(`Failed to set popup visibility override: ${e}`);
+ }
+ }
+ }
- if (errors.length === 0) {
+ if (typeof error === 'undefined') {
const token = generateId(16);
this._allPopupVisibilityTokenMap.set(token, results);
return token;
@@ -197,13 +207,24 @@ export class PopupFactory {
// Revert on error
await this._revertPopupVisibilityOverrides(results);
- throw errors[0];
+ throw error;
+ }
+
+ /**
+ * @param {import('popup').PopupAny} popup
+ * @param {boolean} value
+ * @param {number} priority
+ * @returns {Promise<{popup: import('popup').PopupAny, token: ?string}>}
+ */
+ async _setPopupVisibleOverrideReturnTuple(popup, value, priority) {
+ const token = await popup.setVisibleOverride(value, priority);
+ return {popup, token};
}
/**
* Clears a visibility override that was generated by `setAllVisibleOverride`.
- * @param {string} token The token returned from `setAllVisibleOverride`.
- * @returns {boolean} `true` if the override existed and was removed, `false` otherwise.
+ * @param {import('core').TokenString} token The token returned from `setAllVisibleOverride`.
+ * @returns {Promise<boolean>} `true` if the override existed and was removed, `false` otherwise.
*/
async clearAllVisibleOverride(token) {
const results = this._allPopupVisibilityTokenMap.get(token);
@@ -216,6 +237,10 @@ export class PopupFactory {
// API message handlers
+ /**
+ * @param {import('popup-factory').GetOrCreatePopupDetails} details
+ * @returns {Promise<{id: string, depth: number, frameId: number}>}
+ */
async _onApiGetOrCreatePopup(details) {
const popup = await this.getOrCreatePopup(details);
return {
@@ -225,31 +250,53 @@ export class PopupFactory {
};
}
+ /**
+ * @param {{id: string, optionsContext: import('settings').OptionsContext}} params
+ */
async _onApiSetOptionsContext({id, optionsContext}) {
const popup = this._getPopup(id);
- return await popup.setOptionsContext(optionsContext);
+ await popup.setOptionsContext(optionsContext);
}
- _onApiHide({id, changeFocus}) {
+ /**
+ * @param {{id: string, changeFocus: boolean}} params
+ */
+ async _onApiHide({id, changeFocus}) {
const popup = this._getPopup(id);
- return popup.hide(changeFocus);
+ await popup.hide(changeFocus);
}
+ /**
+ * @param {{id: string}} params
+ * @returns {Promise<boolean>}
+ */
async _onApiIsVisibleAsync({id}) {
const popup = this._getPopup(id);
return await popup.isVisible();
}
+ /**
+ * @param {{id: string, value: boolean, priority: number}} params
+ * @returns {Promise<?import('core').TokenString>}
+ */
async _onApiSetVisibleOverride({id, value, priority}) {
const popup = this._getPopup(id);
return await popup.setVisibleOverride(value, priority);
}
+ /**
+ * @param {{id: string, token: import('core').TokenString}} params
+ * @returns {Promise<boolean>}
+ */
async _onApiClearVisibleOverride({id, token}) {
const popup = this._getPopup(id);
return await popup.clearVisibleOverride(token);
}
+ /**
+ * @param {{id: string, x: number, y: number}} params
+ * @returns {Promise<boolean>}
+ */
async _onApiContainsPoint({id, x, y}) {
const popup = this._getPopup(id);
const offset = this._getPopupOffset(popup);
@@ -258,6 +305,10 @@ export class PopupFactory {
return await popup.containsPoint(x, y);
}
+ /**
+ * @param {{id: string, details: import('popup').ContentDetails, displayDetails: ?import('display').ContentDetails}} params
+ * @returns {Promise<void>}
+ */
async _onApiShowContent({id, details, displayDetails}) {
const popup = this._getPopup(id);
if (!this._popupCanShow(popup)) { return; }
@@ -274,36 +325,64 @@ export class PopupFactory {
return await popup.showContent(details, displayDetails);
}
- _onApiSetCustomCss({id, css}) {
+ /**
+ * @param {{id: string, css: string}} params
+ * @returns {Promise<void>}
+ */
+ async _onApiSetCustomCss({id, css}) {
const popup = this._getPopup(id);
- return popup.setCustomCss(css);
+ await popup.setCustomCss(css);
}
- _onApiClearAutoPlayTimer({id}) {
+ /**
+ * @param {{id: string}} params
+ * @returns {Promise<void>}
+ */
+ async _onApiClearAutoPlayTimer({id}) {
const popup = this._getPopup(id);
- return popup.clearAutoPlayTimer();
+ await popup.clearAutoPlayTimer();
}
- _onApiSetContentScale({id, scale}) {
+ /**
+ * @param {{id: string, scale: number}} params
+ * @returns {Promise<void>}
+ */
+ async _onApiSetContentScale({id, scale}) {
const popup = this._getPopup(id);
- return popup.setContentScale(scale);
+ await popup.setContentScale(scale);
}
- _onApiUpdateTheme({id}) {
+ /**
+ * @param {{id: string}} params
+ * @returns {Promise<void>}
+ */
+ async _onApiUpdateTheme({id}) {
const popup = this._getPopup(id);
- return popup.updateTheme();
+ await popup.updateTheme();
}
- _onApiSetCustomOuterCss({id, css, useWebExtensionApi}) {
+ /**
+ * @param {{id: string, css: string, useWebExtensionApi: boolean}} params
+ * @returns {Promise<void>}
+ */
+ async _onApiSetCustomOuterCss({id, css, useWebExtensionApi}) {
const popup = this._getPopup(id);
- return popup.setCustomOuterCss(css, useWebExtensionApi);
+ await popup.setCustomOuterCss(css, useWebExtensionApi);
}
+ /**
+ * @param {{id: string}} params
+ * @returns {Promise<import('popup').ValidSize>}
+ */
async _onApiGetFrameSize({id}) {
const popup = this._getPopup(id);
return await popup.getFrameSize();
}
+ /**
+ * @param {{id: string, width: number, height: number}} params
+ * @returns {Promise<boolean>}
+ */
async _onApiSetFrameSize({id, width, height}) {
const popup = this._getPopup(id);
return await popup.setFrameSize(width, height);
@@ -311,6 +390,11 @@ export class PopupFactory {
// Private functions
+ /**
+ * @param {string} id
+ * @returns {import('popup').PopupAny}
+ * @throws {Error}
+ */
_getPopup(id) {
const popup = this._popups.get(id);
if (typeof popup === 'undefined') {
@@ -319,6 +403,10 @@ export class PopupFactory {
return popup;
}
+ /**
+ * @param {import('popup').PopupAny} popup
+ * @returns {{x: number, y: number}}
+ */
_getPopupOffset(popup) {
const {parent} = popup;
if (parent !== null) {
@@ -330,11 +418,19 @@ export class PopupFactory {
return {x: 0, y: 0};
}
+ /**
+ * @param {import('popup').PopupAny} popup
+ * @returns {boolean}
+ */
_popupCanShow(popup) {
const parent = popup.parent;
return parent === null || parent.isVisibleSync();
}
+ /**
+ * @param {{popup: import('popup').PopupAny, token: string}[]} overrides
+ * @returns {Promise<boolean[]>}
+ */
async _revertPopupVisibilityOverrides(overrides) {
const promises = [];
for (const value of overrides) {