diff options
| author | Darius Jahandarie <djahandarie@gmail.com> | 2023-12-06 03:53:16 +0000 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-06 03:53:16 +0000 | 
| commit | bd5bc1a5db29903bc098995cd9262c4576bf76af (patch) | |
| tree | c9214189e0214480fcf6539ad1c6327aef6cbd1c /ext/js/app/popup.js | |
| parent | fd6bba8a2a869eaf2b2c1fa49001f933fce3c618 (diff) | |
| parent | 23e6fb76319c9ed7c9bcdc3efba39bc5dd38f288 (diff) | |
Merge pull request #339 from toasted-nutbread/type-annotations
Type annotations
Diffstat (limited to 'ext/js/app/popup.js')
| -rw-r--r-- | ext/js/app/popup.js | 334 | 
1 files changed, 235 insertions, 99 deletions
| diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index 0e2e2493..7419785b 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -18,6 +18,7 @@  import {FrameClient} from '../comm/frame-client.js';  import {DynamicProperty, EventDispatcher, EventListenerCollection, deepEqual} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js';  import {DocumentUtil} from '../dom/document-util.js';  import {dynamicLoader} from '../script/dynamic-loader.js';  import {yomitan} from '../yomitan.js'; @@ -25,53 +26,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 +40,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 +130,7 @@ export class Popup extends EventDispatcher {      /**       * The parent of the popup. -     * @type {Popup} +     * @type {?Popup}       */      get parent() {          return this._parent; @@ -151,7 +146,7 @@ export class Popup extends EventDispatcher {      /**       * The child of the popup. -     * @type {Popup} +     * @type {?Popup}       */      get child() {          return this._child; @@ -167,7 +162,7 @@ export class Popup extends EventDispatcher {      /**       * The depth of the popup. -     * @type {numer} +     * @type {number}       */      get depth() {          return this._depth; @@ -215,11 +210,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 +249,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 +258,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 +272,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 +284,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 +307,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 +360,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 +377,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 +397,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 +430,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 +456,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 +465,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 +479,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 +514,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 +544,9 @@ export class Popup extends EventDispatcher {          await this._injectStyles();      } +    /** +     * @returns {Promise<void>} +     */      async _injectStyles() {          try {              await this._injectPopupOuterStylesheet(); @@ -528,7 +561,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 +577,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 +594,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 +604,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 +635,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 +662,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 +681,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 +726,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 +745,8 @@ export class Popup extends EventDispatcher {          if (              fullscreenElement === null ||              fullscreenElement.shadowRoot || -            fullscreenElement.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions +            // @ts-expect-error - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions +            fullscreenElement.openOrClosedShadowRoot          ) {              return defaultParent;          } @@ -675,10 +762,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 +792,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 +808,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 +845,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 +874,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 +893,10 @@ export class Popup extends EventDispatcher {          }      } +    /** +     * @param {import('document-util').NormalizedWritingMode} writingMode +     * @returns {boolean} +     */      _isWritingModeLeftToRight(writingMode) {          switch (writingMode) {              case 'vertical-lr': @@ -809,6 +907,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 +931,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 +963,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 +998,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 +1021,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 +1034,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 +1055,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 +1089,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 +1105,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 +1118,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; } +} |