aboutsummaryrefslogtreecommitdiff
path: root/ext/js/app/popup.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/app/popup.js')
-rw-r--r--ext/js/app/popup.js333
1 files changed, 234 insertions, 99 deletions
diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js
index 0e2e2493..31b18f01 100644
--- a/ext/js/app/popup.js
+++ b/ext/js/app/popup.js
@@ -25,53 +25,12 @@ import {ThemeController} from './theme-controller.js';
/**
* This class is the container which hosts the display of search results.
+ * @augments EventDispatcher<import('popup').PopupAnyEventType>
*/
export class Popup extends EventDispatcher {
/**
- * Information about how popup content should be shown, specifically related to the outer popup frame.
- * @typedef {object} ContentDetails
- * @property {?object} optionsContext The options context for the content to show.
- * @property {Rect[]} sourceRects The rectangles of the source content.
- * @property {'horizontal-tb' | 'vertical-rl' | 'vertical-lr' | 'sideways-rl' | 'sideways-lr'} writingMode The normalized CSS writing-mode value of the source content.
- */
-
- /**
- * A rectangle representing a DOM region, similar to DOMRect.
- * @typedef {object} Rect
- * @property {number} left The left position of the rectangle.
- * @property {number} top The top position of the rectangle.
- * @property {number} right The right position of the rectangle.
- * @property {number} bottom The bottom position of the rectangle.
- */
-
- /**
- * A rectangle representing a DOM region, similar to DOMRect but with a `valid` property.
- * @typedef {object} ValidRect
- * @property {number} left The left position of the rectangle.
- * @property {number} top The top position of the rectangle.
- * @property {number} right The right position of the rectangle.
- * @property {number} bottom The bottom position of the rectangle.
- * @property {boolean} valid Whether or not the rectangle is valid.
- */
-
- /**
- * A rectangle representing a DOM region for placing the popup frame.
- * @typedef {object} SizeRect
- * @property {number} left The left position of the rectangle.
- * @property {number} top The top position of the rectangle.
- * @property {number} width The width of the rectangle.
- * @property {number} height The height of the rectangle.
- * @property {boolean} after Whether or not the rectangle is positioned to the right of the source rectangle.
- * @property {boolean} below Whether or not the rectangle is positioned below the source rectangle.
- */
-
- /**
* Creates a new instance.
- * @param {object} details The details used to construct the new instance.
- * @param {string} details.id The ID of the popup.
- * @param {number} details.depth The depth of the popup.
- * @param {number} details.frameId The ID of the host frame.
- * @param {boolean} details.childrenSupported Whether or not the popup is able to show child popups.
+ * @param {import('popup').PopupConstructorDetails} details The details used to construct the new instance.
*/
constructor({
id,
@@ -80,48 +39,83 @@ export class Popup extends EventDispatcher {
childrenSupported
}) {
super();
+ /** @type {string} */
this._id = id;
+ /** @type {number} */
this._depth = depth;
+ /** @type {number} */
this._frameId = frameId;
+ /** @type {boolean} */
this._childrenSupported = childrenSupported;
+ /** @type {?Popup} */
this._parent = null;
+ /** @type {?Popup} */
this._child = null;
+ /** @type {?Promise<boolean>} */
this._injectPromise = null;
+ /** @type {boolean} */
this._injectPromiseComplete = false;
+ /** @type {DynamicProperty<boolean>} */
this._visible = new DynamicProperty(false);
+ /** @type {boolean} */
this._visibleValue = false;
+ /** @type {?import('settings').OptionsContext} */
this._optionsContext = null;
+ /** @type {number} */
this._contentScale = 1.0;
+ /** @type {string} */
this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, '');
- this._optionsAssigned = false;
+ /** @type {number} */
this._initialWidth = 400;
+ /** @type {number} */
this._initialHeight = 250;
+ /** @type {number} */
this._horizontalOffset = 0;
+ /** @type {number} */
this._verticalOffset = 10;
+ /** @type {number} */
this._horizontalOffset2 = 10;
+ /** @type {number} */
this._verticalOffset2 = 0;
+ /** @type {import('settings').PopupVerticalTextPosition} */
this._verticalTextPosition = 'before';
+ /** @type {boolean} */
this._horizontalTextPositionBelow = true;
+ /** @type {import('settings').PopupDisplayMode} */
this._displayMode = 'default';
+ /** @type {boolean} */
this._displayModeIsFullWidth = false;
+ /** @type {boolean} */
this._scaleRelativeToVisualViewport = true;
+ /** @type {boolean} */
this._useSecureFrameUrl = true;
+ /** @type {boolean} */
this._useShadowDom = true;
+ /** @type {string} */
this._customOuterCss = '';
+ /** @type {?number} */
this._frameSizeContentScale = null;
+ /** @type {?FrameClient} */
this._frameClient = null;
+ /** @type {HTMLIFrameElement} */
this._frame = document.createElement('iframe');
this._frame.className = 'yomitan-popup';
this._frame.style.width = '0';
this._frame.style.height = '0';
+ /** @type {boolean} */
+ this._frameConnected = false;
+ /** @type {HTMLElement} */
this._container = this._frame;
+ /** @type {?ShadowRoot} */
this._shadow = null;
+ /** @type {ThemeController} */
this._themeController = new ThemeController(this._frame);
+ /** @type {EventListenerCollection} */
this._fullscreenEventListeners = new EventListenerCollection();
}
@@ -135,7 +129,7 @@ export class Popup extends EventDispatcher {
/**
* The parent of the popup.
- * @type {Popup}
+ * @type {?Popup}
*/
get parent() {
return this._parent;
@@ -151,7 +145,7 @@ export class Popup extends EventDispatcher {
/**
* The child of the popup.
- * @type {Popup}
+ * @type {?Popup}
*/
get child() {
return this._child;
@@ -167,7 +161,7 @@ export class Popup extends EventDispatcher {
/**
* The depth of the popup.
- * @type {numer}
+ * @type {number}
*/
get depth() {
return this._depth;
@@ -215,11 +209,13 @@ export class Popup extends EventDispatcher {
/**
* Sets the options context for the popup.
- * @param {object} optionsContext The options context object.
+ * @param {import('settings').OptionsContext} optionsContext The options context object.
*/
async setOptionsContext(optionsContext) {
await this._setOptionsContext(optionsContext);
- await this._invokeSafe('Display.setOptionsContext', {optionsContext});
+ if (this._frameConnected) {
+ await this._invokeSafe('Display.setOptionsContext', {optionsContext});
+ }
}
/**
@@ -252,7 +248,7 @@ export class Popup extends EventDispatcher {
* Force assigns the visibility of the popup.
* @param {boolean} value Whether or not the popup should be visible.
* @param {number} priority The priority of the override.
- * @returns {Promise<string?>} A token used which can be passed to `clearVisibleOverride`,
+ * @returns {Promise<?import('core').TokenString>} A token used which can be passed to `clearVisibleOverride`,
* or null if the override wasn't assigned.
*/
async setVisibleOverride(value, priority) {
@@ -261,7 +257,7 @@ export class Popup extends EventDispatcher {
/**
* Clears a visibility override that was generated by `setVisibleOverride`.
- * @param {string} token The token returned from `setVisibleOverride`.
+ * @param {import('core').TokenString} token The token returned from `setVisibleOverride`.
* @returns {Promise<boolean>} `true` if the override existed and was removed, `false` otherwise.
*/
async clearVisibleOverride(token) {
@@ -275,7 +271,8 @@ export class Popup extends EventDispatcher {
* @returns {Promise<boolean>} `true` if the point is contained within the popup's rect, `false` otherwise.
*/
async containsPoint(x, y) {
- for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup.child) {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ for (let popup = /** @type {?Popup} */ (this); popup !== null && popup.isVisibleSync(); popup = popup.child) {
const rect = popup.getFrameRect();
if (rect.valid && x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) {
return true;
@@ -286,12 +283,12 @@ export class Popup extends EventDispatcher {
/**
* Shows and updates the positioning and content of the popup.
- * @param {ContentDetails} details Settings for the outer popup.
- * @param {Display.ContentDetails} displayDetails The details parameter passed to `Display.setContent`.
+ * @param {import('popup').ContentDetails} details Settings for the outer popup.
+ * @param {?import('display').ContentDetails} displayDetails The details parameter passed to `Display.setContent`.
* @returns {Promise<void>}
*/
async showContent(details, displayDetails) {
- if (!this._optionsAssigned) { throw new Error('Options not assigned'); }
+ if (this._optionsContext === null) { throw new Error('Options not assigned'); }
const {optionsContext, sourceRects, writingMode} = details;
if (optionsContext !== null) {
@@ -309,25 +306,27 @@ export class Popup extends EventDispatcher {
* Sets the custom styles for the popup content.
* @param {string} css The CSS rules.
*/
- setCustomCss(css) {
- this._invokeSafe('Display.setCustomCss', {css});
+ async setCustomCss(css) {
+ await this._invokeSafe('Display.setCustomCss', {css});
}
/**
* Stops the audio auto-play timer, if one has started.
*/
- clearAutoPlayTimer() {
- this._invokeSafe('Display.clearAutoPlayTimer');
+ async clearAutoPlayTimer() {
+ if (this._frameConnected) {
+ await this._invokeSafe('Display.clearAutoPlayTimer', {});
+ }
}
/**
* Sets the scaling factor of the popup content.
* @param {number} scale The scaling factor.
*/
- setContentScale(scale) {
+ async setContentScale(scale) {
this._contentScale = scale;
this._frame.style.fontSize = `${scale}px`;
- this._invokeSafe('Display.setContentScale', {scale});
+ await this._invokeSafe('Display.setContentScale', {scale});
}
/**
@@ -360,12 +359,14 @@ export class Popup extends EventDispatcher {
parentNode = this._shadow;
}
const node = await dynamicLoader.loadStyle('yomitan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi, parentNode);
- this.trigger('customOuterCssChanged', {node, useWebExtensionApi, inShadow});
+ /** @type {import('popup').CustomOuterCssChangedEvent} */
+ const event = {node, useWebExtensionApi, inShadow};
+ this.trigger('customOuterCssChanged', event);
}
/**
* Gets the rectangle of the DOM frame, synchronously.
- * @returns {ValidRect} The rect.
+ * @returns {import('popup').ValidRect} The rect.
* `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame.
*/
getFrameRect() {
@@ -375,7 +376,7 @@ export class Popup extends EventDispatcher {
/**
* Gets the size of the DOM frame.
- * @returns {Promise<{width: number, height: number, valid: boolean}>} The size and whether or not it is valid.
+ * @returns {Promise<import('popup').ValidSize>} The size and whether or not it is valid.
*/
async getFrameSize() {
const {width, height} = this._getFrameBoundingClientRect();
@@ -395,14 +396,23 @@ export class Popup extends EventDispatcher {
// Private functions
+ /**
+ * @returns {void}
+ */
_onFrameMouseOver() {
this.trigger('framePointerOver', {});
}
+ /**
+ * @returns {void}
+ */
_onFrameMouseOut() {
this.trigger('framePointerOut', {});
}
+ /**
+ * @returns {Promise<boolean>}
+ */
_inject() {
let injectPromise = this._injectPromise;
if (injectPromise === null) {
@@ -419,19 +429,25 @@ export class Popup extends EventDispatcher {
return injectPromise;
}
+ /**
+ * @returns {Promise<boolean>}
+ */
async _injectInner1() {
try {
await this._injectInner2();
return true;
} catch (e) {
this._resetFrame();
- if (e.source === this) { return false; } // Passive error
+ if (e instanceof PopupError && e.source === this) { return false; } // Passive error
throw e;
}
}
+ /**
+ * @returns {Promise<void>}
+ */
async _injectInner2() {
- if (!this._optionsAssigned) {
+ if (this._optionsContext === null) {
throw new Error('Options not initialized');
}
@@ -439,6 +455,7 @@ export class Popup extends EventDispatcher {
await this._setUpContainer(this._useShadowDom);
+ /** @type {import('frame-client').SetupFrameFunction} */
const setupFrame = (frame) => {
frame.removeAttribute('src');
frame.removeAttribute('srcdoc');
@@ -447,9 +464,8 @@ export class Popup extends EventDispatcher {
const {contentDocument} = frame;
if (contentDocument === null) {
// This can occur when running inside a sandboxed frame without "allow-same-origin"
- const error = new Error('Popup not supoprted in this context');
- error.source = this; // Used to detect a passive error which should be ignored
- throw error;
+ // Custom error is used to detect a passive error which should be ignored
+ throw new PopupError('Popup not supported in this context', this);
}
const url = chrome.runtime.getURL('/popup.html');
if (useSecurePopupFrameUrl) {
@@ -462,23 +478,32 @@ export class Popup extends EventDispatcher {
const frameClient = new FrameClient();
this._frameClient = frameClient;
await frameClient.connect(this._frame, this._targetOrigin, this._frameId, setupFrame);
+ this._frameConnected = true;
// Configure
- await this._invokeSafe('Display.configure', {
+ /** @type {import('display').ConfigureMessageDetails} */
+ const configureParams = {
depth: this._depth,
parentPopupId: this._id,
parentFrameId: this._frameId,
childrenSupported: this._childrenSupported,
scale: this._contentScale,
optionsContext: this._optionsContext
- });
+ };
+ await this._invokeSafe('Display.configure', configureParams);
}
+ /**
+ * @returns {void}
+ */
_onFrameLoad() {
if (!this._injectPromiseComplete) { return; }
this._resetFrame();
}
+ /**
+ * @returns {void}
+ */
_resetFrame() {
const parent = this._container.parentNode;
if (parent !== null) {
@@ -488,10 +513,14 @@ export class Popup extends EventDispatcher {
this._frame.removeAttribute('srcdoc');
this._frameClient = null;
+ this._frameConnected = false;
this._injectPromise = null;
this._injectPromiseComplete = false;
}
+ /**
+ * @param {boolean} usePopupShadowDom
+ */
async _setUpContainer(usePopupShadowDom) {
if (usePopupShadowDom && typeof this._frame.attachShadow === 'function') {
const container = document.createElement('div');
@@ -514,6 +543,9 @@ export class Popup extends EventDispatcher {
await this._injectStyles();
}
+ /**
+ * @returns {Promise<void>}
+ */
async _injectStyles() {
try {
await this._injectPopupOuterStylesheet();
@@ -528,7 +560,11 @@ export class Popup extends EventDispatcher {
}
}
+ /**
+ * @returns {Promise<void>}
+ */
async _injectPopupOuterStylesheet() {
+ /** @type {'code'|'file'|'file-content'} */
let fileType = 'file';
let useWebExtensionApi = true;
let parentNode = null;
@@ -540,6 +576,9 @@ export class Popup extends EventDispatcher {
await dynamicLoader.loadStyle('yomitan-popup-outer-stylesheet', fileType, '/css/popup-outer.css', useWebExtensionApi, parentNode);
}
+ /**
+ * @param {boolean} observe
+ */
_observeFullscreen(observe) {
if (!observe) {
this._fullscreenEventListeners.removeAllEventListeners();
@@ -554,6 +593,9 @@ export class Popup extends EventDispatcher {
DocumentUtil.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners);
}
+ /**
+ * @returns {void}
+ */
_onFullscreenChanged() {
const parent = this._getFrameParentElement();
if (parent !== null && this._container.parentNode !== parent) {
@@ -561,6 +603,10 @@ export class Popup extends EventDispatcher {
}
}
+ /**
+ * @param {import('popup').Rect[]} sourceRects
+ * @param {import('document-util').NormalizedWritingMode} writingMode
+ */
async _show(sourceRects, writingMode) {
const injected = await this._inject();
if (!injected) { return; }
@@ -588,16 +634,26 @@ export class Popup extends EventDispatcher {
}
}
+ /**
+ * @param {number} width
+ * @param {number} height
+ */
_setFrameSize(width, height) {
const {style} = this._frame;
style.width = `${width}px`;
style.height = `${height}px`;
}
+ /**
+ * @param {boolean} visible
+ */
_setVisible(visible) {
this._visible.defaultValue = visible;
}
+ /**
+ * @param {import('dynamic-property').ChangeEventDetails<boolean>} event
+ */
_onVisibleChange({value}) {
if (this._visibleValue === value) { return; }
this._visibleValue = value;
@@ -605,6 +661,9 @@ export class Popup extends EventDispatcher {
this._invokeSafe('Display.visibilityChanged', {value});
}
+ /**
+ * @returns {void}
+ */
_focusParent() {
if (this._parent !== null) {
// Chrome doesn't like focusing iframe without contentWindow.
@@ -621,23 +680,43 @@ export class Popup extends EventDispatcher {
}
}
- async _invoke(action, params={}) {
+ /**
+ * @template {import('core').SerializableObject} TParams
+ * @template [TReturn=unknown]
+ * @param {string} action
+ * @param {TParams} params
+ * @returns {Promise<TReturn>}
+ */
+ async _invoke(action, params) {
const contentWindow = this._frame.contentWindow;
- if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; }
+ if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) {
+ throw new Error(`Failed to invoke action ${action}: frame state invalid`);
+ }
const message = this._frameClient.createMessage({action, params});
return await yomitan.crossFrame.invoke(this._frameClient.frameId, 'popupMessage', message);
}
- async _invokeSafe(action, params={}, defaultReturnValue) {
+ /**
+ * @template {import('core').SerializableObject} TParams
+ * @template [TReturn=unknown]
+ * @param {string} action
+ * @param {TParams} params
+ * @returns {Promise<TReturn|undefined>}
+ */
+ async _invokeSafe(action, params) {
try {
return await this._invoke(action, params);
} catch (e) {
if (!yomitan.isExtensionUnloaded) { throw e; }
- return defaultReturnValue;
+ return void 0;
}
}
+ /**
+ * @param {string} action
+ * @param {import('core').SerializableObject} params
+ */
_invokeWindow(action, params={}) {
const contentWindow = this._frame.contentWindow;
if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; }
@@ -646,10 +725,16 @@ export class Popup extends EventDispatcher {
contentWindow.postMessage(message, this._targetOrigin);
}
+ /**
+ * @returns {void}
+ */
_onExtensionUnloaded() {
this._invokeWindow('Display.extensionUnloaded');
}
+ /**
+ * @returns {Element}
+ */
_getFrameParentElement() {
let defaultParent = document.body;
if (defaultParent !== null && defaultParent.tagName.toLowerCase() === 'frameset') {
@@ -659,7 +744,8 @@ export class Popup extends EventDispatcher {
if (
fullscreenElement === null ||
fullscreenElement.shadowRoot ||
- fullscreenElement.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions
+ // @ts-ignore - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions
+ fullscreenElement.openOrClosedShadowRoot
) {
return defaultParent;
}
@@ -675,10 +761,10 @@ export class Popup extends EventDispatcher {
/**
* Computes the position where the popup should be placed relative to the source content.
- * @param {Rect[]} sourceRects The rectangles of the source content.
- * @param {string} writingMode The CSS writing mode of the source text.
- * @param {Rect} viewport The viewport that the popup can be placed within.
- * @returns {SizeRect} The calculated rectangle for where to position the popup.
+ * @param {import('popup').Rect[]} sourceRects The rectangles of the source content.
+ * @param {import('document-util').NormalizedWritingMode} writingMode The CSS writing mode of the source text.
+ * @param {import('popup').Rect} viewport The viewport that the popup can be placed within.
+ * @returns {import('popup').SizeRect} The calculated rectangle for where to position the popup.
*/
_getPosition(sourceRects, writingMode, viewport) {
sourceRects = this._convertSourceRectsCoordinateSpace(sourceRects);
@@ -705,6 +791,7 @@ export class Popup extends EventDispatcher {
horizontalOffset *= contentScale;
verticalOffset *= contentScale;
+ /** @type {?import('popup').SizeRect} */
let best = null;
const sourceRectsLength = sourceRects.length;
for (let i = 0, ii = (sourceRectsLength > 1 ? sourceRectsLength : 0); i <= ii; ++i) {
@@ -720,19 +807,20 @@ export class Popup extends EventDispatcher {
if (result.height >= frameHeight) { break; }
}
}
- return best;
+ // Given the loop conditions, this is guaranteed to be non-null
+ return /** @type {import('popup').SizeRect} */ (best);
}
/**
* Computes the position where the popup should be placed for horizontal text.
- * @param {Rect} sourceRect The rectangle of the source content.
+ * @param {import('popup').Rect} sourceRect The rectangle of the source content.
* @param {number} frameWidth The preferred width of the frame.
* @param {number} frameHeight The preferred height of the frame.
- * @param {Rect} viewport The viewport that the frame can be placed within.
+ * @param {import('popup').Rect} viewport The viewport that the frame can be placed within.
* @param {number} horizontalOffset The horizontal offset from the source rect that the popup will be placed.
* @param {number} verticalOffset The vertical offset from the source rect that the popup will be placed.
* @param {boolean} preferBelow Whether or not the popup is preferred to be placed below the source content.
- * @returns {SizeRect} The calculated rectangle for where to position the popup.
+ * @returns {import('popup').SizeRect} The calculated rectangle for where to position the popup.
*/
_getPositionForHorizontalText(sourceRect, frameWidth, frameHeight, viewport, horizontalOffset, verticalOffset, preferBelow) {
const [left, width, after] = this._getConstrainedPosition(
@@ -756,14 +844,14 @@ export class Popup extends EventDispatcher {
/**
* Computes the position where the popup should be placed for vertical text.
- * @param {Rect} sourceRect The rectangle of the source content.
+ * @param {import('popup').Rect} sourceRect The rectangle of the source content.
* @param {number} frameWidth The preferred width of the frame.
* @param {number} frameHeight The preferred height of the frame.
- * @param {Rect} viewport The viewport that the frame can be placed within.
+ * @param {import('popup').Rect} viewport The viewport that the frame can be placed within.
* @param {number} horizontalOffset The horizontal offset from the source rect that the popup will be placed.
* @param {number} verticalOffset The vertical offset from the source rect that the popup will be placed.
* @param {boolean} preferRight Whether or not the popup is preferred to be placed to the right of the source content.
- * @returns {SizeRect} The calculated rectangle for where to position the popup.
+ * @returns {import('popup').SizeRect} The calculated rectangle for where to position the popup.
*/
_getPositionForVerticalText(sourceRect, frameWidth, frameHeight, viewport, horizontalOffset, verticalOffset, preferRight) {
const [left, width, after] = this._getConstrainedPositionBinary(
@@ -785,6 +873,11 @@ export class Popup extends EventDispatcher {
return {left, top, width, height, after, below};
}
+ /**
+ * @param {import('settings').PopupVerticalTextPosition} positionPreference
+ * @param {import('document-util').NormalizedWritingMode} writingMode
+ * @returns {boolean}
+ */
_isVerticalTextPopupOnRight(positionPreference, writingMode) {
switch (positionPreference) {
case 'before':
@@ -799,6 +892,10 @@ export class Popup extends EventDispatcher {
}
}
+ /**
+ * @param {import('document-util').NormalizedWritingMode} writingMode
+ * @returns {boolean}
+ */
_isWritingModeLeftToRight(writingMode) {
switch (writingMode) {
case 'vertical-lr':
@@ -809,6 +906,15 @@ export class Popup extends EventDispatcher {
}
}
+ /**
+ * @param {number} positionBefore
+ * @param {number} positionAfter
+ * @param {number} size
+ * @param {number} minLimit
+ * @param {number} maxLimit
+ * @param {boolean} after
+ * @returns {[position: number, size: number, after: boolean]}
+ */
_getConstrainedPosition(positionBefore, positionAfter, size, minLimit, maxLimit, after) {
size = Math.min(size, maxLimit - minLimit);
@@ -824,6 +930,15 @@ export class Popup extends EventDispatcher {
return [position, size, after];
}
+ /**
+ * @param {number} positionBefore
+ * @param {number} positionAfter
+ * @param {number} size
+ * @param {number} minLimit
+ * @param {number} maxLimit
+ * @param {boolean} after
+ * @returns {[position: number, size: number, after: boolean]}
+ */
_getConstrainedPositionBinary(positionBefore, positionAfter, size, minLimit, maxLimit, after) {
const overflowBefore = minLimit - (positionBefore - size);
const overflowAfter = (positionAfter + size) - maxLimit;
@@ -847,11 +962,11 @@ export class Popup extends EventDispatcher {
/**
* Gets the visual viewport.
* @param {boolean} useVisualViewport Whether or not the `window.visualViewport` should be used.
- * @returns {Rect} The rectangle of the visual viewport.
+ * @returns {import('popup').Rect} The rectangle of the visual viewport.
*/
_getViewport(useVisualViewport) {
- const visualViewport = window.visualViewport;
- if (visualViewport !== null && typeof visualViewport === 'object') {
+ const {visualViewport} = window;
+ if (typeof visualViewport !== 'undefined' && visualViewport !== null) {
const left = visualViewport.offsetLeft;
const top = visualViewport.offsetTop;
const width = visualViewport.width;
@@ -882,6 +997,9 @@ export class Popup extends EventDispatcher {
};
}
+ /**
+ * @param {import('settings').OptionsContext} optionsContext
+ */
async _setOptionsContext(optionsContext) {
this._optionsContext = optionsContext;
const options = await yomitan.api.optionsGet(optionsContext);
@@ -902,10 +1020,12 @@ export class Popup extends EventDispatcher {
this._useSecureFrameUrl = general.useSecurePopupFrameUrl;
this._useShadowDom = general.usePopupShadowDom;
this._customOuterCss = general.customPopupOuterCss;
- this._optionsAssigned = true;
this.updateTheme();
}
+ /**
+ * @param {import('settings').OptionsContext} optionsContext
+ */
async _setOptionsContextIfDifferent(optionsContext) {
if (deepEqual(this._optionsContext, optionsContext)) { return; }
await this._setOptionsContext(optionsContext);
@@ -913,8 +1033,8 @@ export class Popup extends EventDispatcher {
/**
* Computes the bounding rectangle for a set of rectangles.
- * @param {Rect[]} sourceRects An array of rectangles.
- * @returns {Rect} The bounding rectangle for all of the source rectangles.
+ * @param {import('popup').Rect[]} sourceRects An array of rectangles.
+ * @returns {import('popup').Rect} The bounding rectangle for all of the source rectangles.
*/
_getBoundingSourceRect(sourceRects) {
switch (sourceRects.length) {
@@ -934,8 +1054,8 @@ export class Popup extends EventDispatcher {
/**
* Checks whether or not a rectangle is overlapping any other rectangles.
- * @param {SizeRect} sizeRect The rectangles to check for overlaps.
- * @param {Rect[]} sourceRects The list of rectangles to compare against.
+ * @param {import('popup').SizeRect} sizeRect The rectangles to check for overlaps.
+ * @param {import('popup').Rect[]} sourceRects The list of rectangles to compare against.
* @param {number} ignoreIndex The index of an item in `sourceRects` to ignore.
* @returns {boolean} `true` if `sizeRect` overlaps any one of `sourceRects`, excluding `sourceRects[ignoreIndex]`; `false` otherwise.
*/
@@ -968,8 +1088,8 @@ export class Popup extends EventDispatcher {
/**
* Converts the coordinate space of source rectangles.
- * @param {Rect[]} sourceRects The list of rectangles to convert.
- * @returns {Rect[]} Either an updated list of rectangles, or `sourceRects` if no change is required.
+ * @param {import('popup').Rect[]} sourceRects The list of rectangles to convert.
+ * @returns {import('popup').Rect[]} Either an updated list of rectangles, or `sourceRects` if no change is required.
*/
_convertSourceRectsCoordinateSpace(sourceRects) {
let scale = DocumentUtil.computeZoomScale(this._container);
@@ -984,9 +1104,9 @@ export class Popup extends EventDispatcher {
/**
* Creates a scaled rectangle.
- * @param {Rect} rect The rectangle to scale.
+ * @param {import('popup').Rect} rect The rectangle to scale.
* @param {number} scale The scale factor.
- * @returns {Rect} A new rectangle which has been scaled.
+ * @returns {import('popup').Rect} A new rectangle which has been scaled.
*/
_createScaledRect(rect, scale) {
return {
@@ -997,3 +1117,18 @@ export class Popup extends EventDispatcher {
};
}
}
+
+class PopupError extends ExtensionError {
+ /**
+ * @param {string} message
+ * @param {Popup} source
+ */
+ constructor(message, source) {
+ super(message);
+ /** @type {Popup} */
+ this._source = source;
+ }
+
+ /** @type {Popup} */
+ get source() { return this._source; }
+}