diff options
| -rw-r--r-- | ext/js/app/frontend.js | 53 | ||||
| -rw-r--r-- | ext/js/app/popup-factory.js | 32 | ||||
| -rw-r--r-- | ext/js/app/popup-proxy.js | 136 | ||||
| -rw-r--r-- | ext/js/app/popup-window.js | 125 | ||||
| -rw-r--r-- | ext/js/app/popup.js | 127 | ||||
| -rw-r--r-- | ext/js/core.js | 130 | ||||
| -rw-r--r-- | ext/js/yomichan.js | 55 | 
7 files changed, 593 insertions, 65 deletions
| diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index bbc0ff85..9fa6009b 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -21,7 +21,26 @@   * TextSourceRange   */ +/** + * This is the main class responsible for scanning and handling webpage content. + */  class Frontend { +    /** +     * Creates a new instance. +     * @param {object} details +     * @param {string} details.pageType The type of page, one of 'web', 'popup', or 'search'. +     * @param {PopupFactory} details.popupFactory A PopupFactory instance to use for generating popups. +     * @param {number} details.depth The nesting depth value of the popup. +     * @param {number} details.tabId The tab ID of the host tab. +     * @param {number} details.frameId The frame ID of the host frame. +     * @param {?string} details.parentPopupId The popup ID of the parent popup if one exists, otherwise null. +     * @param {?number} details.parentFrameId The frame ID of the parent popup if one exists, otherwise null. +     * @param {boolean} details.useProxyPopup Whether or not proxy popups should be used. +     * @param {boolean} details.canUseWindowPopup Whether or not window popups can be used. +     * @param {boolean} details.allowRootFramePopupProxy Whether or not popups can be hosted in the root frame. +     * @param {boolean} details.childrenSupported Whether popups can create child popups or not. +     * @param {HotkeyHandler} details.hotkeyHandler A HotkeyHandler instance. +     */      constructor({          pageType,          popupFactory, @@ -83,18 +102,33 @@ class Frontend {          ]);      } +    /** +     * Get whether or not the text selection can be cleared. +     * @type {boolean} +     */      get canClearSelection() {          return this._textScanner.canClearSelection;      } +    /** +     * Set whether or not the text selection can be cleared. +     * @param {boolean} value The new value to assign. +     */      set canClearSelection(value) {          this._textScanner.canClearSelection = value;      } +    /** +     * Gets the popup instance. +     * @type {Popup} +     */      get popup() {          return this._popup;      } +    /** +     * Prepares the instance for use. +     */      async prepare() {          await this.updateOptions();          try { @@ -135,20 +169,35 @@ class Frontend {          this._signalFrontendReady();      } +    /** +     * Set whether or not the instance is disabled. +     * @param {boolean} disabled Whether or not the instance is disabled. +     */      setDisabledOverride(disabled) {          this._disabledOverride = disabled;          this._updateTextScannerEnabled();      } +    /** +     * Set or clear an override options context object. +     * @param {?object} optionsContext An options context object to use as the override, or `null` to clear the override. +     */      setOptionsContextOverride(optionsContext) {          this._optionsContextOverride = optionsContext;      } +    /** +     * Performs a new search on a specific source. +     * @param {TextSourceRange|TextSourceElement} textSource The text source to search. +     */      async setTextSource(textSource) {          this._textScanner.setCurrentTextSource(null);          await this._textScanner.search(textSource);      } +    /** +     * Updates the internal options representation. +     */      async updateOptions() {          try {              await this._updateOptionsInternal(); @@ -159,6 +208,10 @@ class Frontend {          }      } +    /** +     * Waits for the previous `showContent` call to be completed. +     * @returns {Promise} A promise which is resolved when the previous `showContent` call has completed. +     */      showContentCompleted() {          return this._lastShowPromise;      } diff --git a/ext/js/app/popup-factory.js b/ext/js/app/popup-factory.js index 096d145e..05ab1942 100644 --- a/ext/js/app/popup-factory.js +++ b/ext/js/app/popup-factory.js @@ -22,7 +22,14 @@   * PopupWindow   */ +/** + * A class which is used to generate and manage popups. + */  class PopupFactory { +    /** +     * Creates a new instance. +     * @param {number} frameId The frame ID of the host frame. +     */      constructor(frameId) {          this._frameId = frameId;          this._frameOffsetForwarder = new FrameOffsetForwarder(frameId); @@ -32,6 +39,9 @@ class PopupFactory {      // Public functions +    /** +     * Prepares the instance for use. +     */      prepare() {          this._frameOffsetForwarder.prepare();          yomichan.crossFrame.registerHandlers([ @@ -53,6 +63,16 @@ class PopupFactory {          ]);      } +    /** +     * Gets or creates a popup based on a set of parameters +     * @param {object} details +     * @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. +     */      async getOrCreatePopup({          frameId=null,          id=null, @@ -148,6 +168,13 @@ 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. +     * @throws An exception is thrown if any popup fails to have its visibiltiy overridden. +     */      async setAllVisibleOverride(value, priority) {          const promises = [];          const errors = []; @@ -173,6 +200,11 @@ class PopupFactory {          throw errors[0];      } +    /** +     * 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. +     */      async clearAllVisibleOverride(token) {          const results = this._allPopupVisibilityTokenMap.get(token);          if (typeof results === 'undefined') { return false; } diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js index c46821d5..d9e1af8d 100644 --- a/ext/js/app/popup-proxy.js +++ b/ext/js/app/popup-proxy.js @@ -15,7 +15,19 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/** + * This class is a proxy for a Popup that is hosted in a different frame. + * It effectively forwards all API calls to the underlying Popup. + */  class PopupProxy extends EventDispatcher { +    /** +     * Creates a new instance. +     * @param {object} details +     * @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 {FrameOffsetForwarder} details.frameOffsetForwarder A `FrameOffsetForwarder` instance which is used to determine frame positioning. +     */      constructor({          id,          depth, @@ -36,64 +48,137 @@ class PopupProxy extends EventDispatcher {      // Public properties +    /** +     * The ID of the popup. +     * @type {string} +     */      get id() {          return this._id;      } +    /** +     * The parent of the popup, which is always `null` for `PopupProxy` instances, +     * since any potential parent popups are in a different frame. +     * @type {Popup} +     */      get parent() {          return null;      } +    /** +     * Attempts to set the parent popup. +     * @param {Popup} value +     * @throws Throws an error, since this class doesn't support a direct parent. +     */      set parent(value) {          throw new Error('Not supported on PopupProxy');      } +    /** +     * The popup child popup, which is always null for `PopupProxy` instances, +     * since any potential child popups are in a different frame. +     * @type {Popup} +     */      get child() {          return null;      } +    /** +     * Attempts to set the child popup. +     * @param {Popup} value +     * @throws Throws an error, since this class doesn't support children. +     */      set child(value) {          throw new Error('Not supported on PopupProxy');      } +    /** +     * The depth of the popup. +     * @type {numer} +     */      get depth() {          return this._depth;      } +    /** +     * Gets the content window of the frame. This value is null, +     * since the window is hosted in a different frame. +     * @type {Window} +     */      get frameContentWindow() {          return null;      } +    /** +     * Gets the DOM node that contains the frame. +     * @type {Element} +     */      get container() {          return null;      } +    /** +     * Gets the ID of the frame. +     * @type {number} +     */      get frameId() {          return this._frameId;      }      // Public functions +    /** +     * Sets the options context for the popup. +     * @param {object} optionsContext The options context object. +     * @returns {Promise<void>} +     */      setOptionsContext(optionsContext, source) {          return this._invokeSafe('setOptionsContext', {id: this._id, optionsContext, source});      } +    /** +     * Hides the popup. +     * @param {boolean} changeFocus Whether or not the parent popup or host frame should be focused. +     * @returns {Promise<void>} +     */      hide(changeFocus) {          return this._invokeSafe('hide', {id: this._id, changeFocus});      } +    /** +     * Returns whether or not the popup is currently visible. +     * @returns {Promise<boolean>} `true` if the popup is visible, `false` otherwise. +     */      isVisible() {          return this._invokeSafe('isVisible', {id: this._id}, false);      } +    /** +     * 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`, +     *   or null if the override wasn't assigned. +     */      setVisibleOverride(value, priority) {          return this._invokeSafe('setVisibleOverride', {id: this._id, value, priority}, null);      } +    /** +     * Clears a visibility override that was generated by `setVisibleOverride`. +     * @param {string} token The token returned from `setVisibleOverride`. +     * @returns {Promise<boolean>} `true` if the override existed and was removed, `false` otherwise. +     */      clearVisibleOverride(token) {          return this._invokeSafe('clearVisibleOverride', {id: this._id, token}, false);      } +    /** +     * Checks whether a point is contained within the popup's rect. +     * @param {number} x The x coordinate. +     * @param {number} y The y coordinate. +     * @returns {Promise<boolean>} `true` if the point is contained within the popup's rect, `false` otherwise. +     */      async containsPoint(x, y) {          if (this._frameOffsetForwarder !== null) {              await this._updateFrameOffset(); @@ -102,6 +187,12 @@ class PopupProxy extends EventDispatcher {          return await this._invokeSafe('containsPoint', {id: this._id, x, y}, false);      } +    /** +     * Shows and updates the positioning and content of the popup. +     * @param {{optionsContext: object, elementRect: {x: number, y: number, width: number, height: number}, writingMode: string}} details Settings for the outer popup. +     * @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. +     * @returns {Promise<void>} +     */      async showContent(details, displayDetails) {          const {elementRect} = details;          if (typeof elementRect !== 'undefined' && this._frameOffsetForwarder !== null) { @@ -111,38 +202,83 @@ class PopupProxy extends EventDispatcher {          return await this._invokeSafe('showContent', {id: this._id, details, displayDetails});      } +    /** +     * Sets the custom styles for the popup content. +     * @param {string} css The CSS rules. +     * @returns {Promise<void>} +     */      setCustomCss(css) {          return this._invokeSafe('setCustomCss', {id: this._id, css});      } +    /** +     * Stops the audio auto-play timer, if one has started. +     * @returns {Promise<void>} +     */      clearAutoPlayTimer() {          return this._invokeSafe('clearAutoPlayTimer', {id: this._id});      } +    /** +     * Sets the scaling factor of the popup content. +     * @param {number} scale The scaling factor. +     * @returns {Promise<void>} +     */      setContentScale(scale) {          return this._invokeSafe('setContentScale', {id: this._id, scale});      } +    /** +     * Returns whether or not the popup is currently visible, synchronously. +     * @returns {boolean} `true` if the popup is visible, `false` otherwise. +     * @throws An exception is thrown for `PopupProxy` since it cannot synchronously detect visibility. +     */      isVisibleSync() {          throw new Error('Not supported on PopupProxy');      } +    /** +     * Updates the outer theme of the popup. +     * @returns {Promise<void>} +     */      updateTheme() {          return this._invokeSafe('updateTheme', {id: this._id});      } +    /** +     * Sets the custom styles for the outer popup container. +     * @param {string} css The CSS rules. +     * @param {boolean} useWebExtensionApi Whether or not web extension APIs should be used to inject the rules. +     *   When web extension APIs are used, a DOM node is not generated, making it harder to detect the changes. +     * @returns {Promise<void>} +     */      setCustomOuterCss(css, useWebExtensionApi) {          return this._invokeSafe('setCustomOuterCss', {id: this._id, css, useWebExtensionApi});      } +    /** +     * Gets the rectangle of the DOM frame, synchronously. +     * @returns {{x: number, y: number, width: number, height: number, valid: boolean}} The rect. +     *   `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame. +     */      getFrameRect() {          return {x: 0, y: 0, width: 0, height: 0, valid: false};      } +    /** +     * Gets the size of the DOM frame. +     * @returns {Promise<{width: number, height: number, valid: boolean}>} The size and whether or not it is valid. +     */      getFrameSize() {          return this._invokeSafe('popup.getFrameSize', {id: this._id}, {width: 0, height: 0, valid: false});      } +    /** +     * Sets the size of the DOM frame. +     * @param {number} width The desired width of the popup. +     * @param {number} height The desired height of the popup. +     * @returns {Promise<boolean>} `true` if the size assignment was successful, `false` otherwise. +     */      setFrameSize(width, height) {          return this._invokeSafe('popup.setFrameSize', {id: this._id, width, height});      } diff --git a/ext/js/app/popup-window.js b/ext/js/app/popup-window.js index 6f86c61e..61c627b8 100644 --- a/ext/js/app/popup-window.js +++ b/ext/js/app/popup-window.js @@ -15,7 +15,17 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/** + * This class represents a popup that is hosted in a new native window. + */  class PopupWindow extends EventDispatcher { +    /** +     * Creates a new instance. +     * @param {object} details +     * @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. +     */      constructor({          id,          depth, @@ -30,6 +40,10 @@ class PopupWindow extends EventDispatcher {      // Public properties +    /** +     * The ID of the popup. +     * @type {string} +     */      get id() {          return this._id;      } @@ -38,30 +52,63 @@ class PopupWindow extends EventDispatcher {          return null;      } +    /** +     * The parent of the popup, which is always `null` for `PopupWindow` instances, +     * since any potential parent popups are in a different frame. +     * @param {Popup} value +     * @type {Popup} +     */      set parent(value) {          throw new Error('Not supported on PopupProxy');      } +    /** +     * The popup child popup, which is always null for `PopupWindow` instances, +     * since any potential child popups are in a different frame. +     * @type {Popup} +     */      get child() {          return null;      } +    /** +     * Attempts to set the child popup. +     * @param {Popup} value +     * @throws Throws an error, since this class doesn't support children. +     */      set child(value) {          throw new Error('Not supported on PopupProxy');      } +    /** +     * The depth of the popup. +     * @type {numer} +     */      get depth() {          return this._depth;      } +    /** +     * Gets the content window of the frame. This value is null, +     * since the window is hosted in a different frame. +     * @type {Window} +     */      get frameContentWindow() {          return null;      } +    /** +     * Gets the DOM node that contains the frame. +     * @type {Element} +     */      get container() {          return null;      } +    /** +     * Gets the ID of the frame. +     * @type {number} +     */      get frameId() {          return this._frameId;      } @@ -69,67 +116,145 @@ class PopupWindow extends EventDispatcher {      // Public functions +    /** +     * Sets the options context for the popup. +     * @param {object} optionsContext The options context object. +     * @returns {Promise<void>} +     */      setOptionsContext(optionsContext, source) {          return this._invoke(false, 'setOptionsContext', {id: this._id, optionsContext, source});      } +    /** +     * Hides the popup. This does nothing for `PopupWindow`. +     * @param {boolean} _changeFocus Whether or not the parent popup or host frame should be focused. +     */      hide(_changeFocus) {          // NOP      } +    /** +     * Returns whether or not the popup is currently visible. +     * @returns {Promise<boolean>} `true` if the popup is visible, `false` otherwise. +     */      async isVisible() {          return (this._popupTabId !== null && await yomichan.api.isTabSearchPopup(this._popupTabId));      } +    /** +     * 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`, +     *   or null if the override wasn't assigned. +     */      async setVisibleOverride(_value, _priority) {          return null;      } +    /** +     * Clears a visibility override that was generated by `setVisibleOverride`. +     * @param {string} _token The token returned from `setVisibleOverride`. +     * @returns {boolean} `true` if the override existed and was removed, `false` otherwise. +     */      clearVisibleOverride(_token) {          return false;      } +    /** +     * Checks whether a point is contained within the popup's rect. +     * @param {number} _x The x coordinate. +     * @param {number} _y The y coordinate. +     * @returns {Promise<boolean>} `true` if the point is contained within the popup's rect, `false` otherwise. +     */      async containsPoint(_x, _y) {          return false;      } +    /** +     * Shows and updates the positioning and content of the popup. +     * @param {{optionsContext: object, elementRect: {x: number, y: number, width: number, height: number}, writingMode: string}} details Settings for the outer popup. +     * @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. +     * @returns {Promise<void>} +     */      async showContent(_details, displayDetails) {          if (displayDetails === null) { return; }          await this._invoke(true, 'setContent', {id: this._id, details: displayDetails});      } +    /** +     * Sets the custom styles for the popup content. +     * @param {string} css The CSS rules. +     */      setCustomCss(css) {          return this._invoke(false, 'setCustomCss', {id: this._id, css});      } +    /** +     * Stops the audio auto-play timer, if one has started. +     */      clearAutoPlayTimer() {          return this._invoke(false, 'clearAutoPlayTimer', {id: this._id});      } +    /** +     * Sets the scaling factor of the popup content. +     * @param {number} scale The scaling factor. +     */      setContentScale(_scale) {          // NOP      } +    /** +     * Returns whether or not the popup is currently visible, synchronously. +     * @returns {boolean} `true` if the popup is visible, `false` otherwise. +     * @throws An exception is thrown for `PopupWindow` since it cannot synchronously detect visibility. +     */      isVisibleSync() {          throw new Error('Not supported on PopupWindow');      } +    /** +     * Updates the outer theme of the popup. +     */      updateTheme() {          // NOP      } +    /** +     * Sets the custom styles for the outer popup container. +     * This does nothing for `PopupWindow`. +     * @param {string} _css The CSS rules. +     * @param {boolean} _useWebExtensionApi Whether or not web extension APIs should be used to inject the rules. +     *   When web extension APIs are used, a DOM node is not generated, making it harder to detect the changes. +     */      async setCustomOuterCss(_css, _useWebExtensionApi) {          // NOP      } +    /** +     * Gets the rectangle of the DOM frame, synchronously. +     * @returns {{x: number, y: number, width: number, height: number, valid: boolean}} The rect. +     *   `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame. +     */      getFrameRect() {          return {x: 0, y: 0, width: 0, height: 0, valid: false};      } +    /** +     * Gets the size of the DOM frame. +     * @returns {Promise<{width: number, height: number, valid: boolean}>} The size and whether or not it is valid. +     */      async getFrameSize() {          return {width: 0, height: 0, valid: false};      } +    /** +     * Sets the size of the DOM frame. +     * @param {number} _width The desired width of the popup. +     * @param {number} _height The desired height of the popup. +     * @returns {Promise<boolean>} `true` if the size assignment was successful, `false` otherwise. +     */      async setFrameSize(_width, _height) {          return false;      } diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index 3a29563d..7581c8d7 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -21,7 +21,18 @@   * dynamicLoader   */ +/** + * This class is the container which hosts the display of search results. + */  class Popup extends EventDispatcher { +    /** +     * Creates a new instance. +     * @param {object} details +     * @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. +     */      constructor({          id,          depth, @@ -59,44 +70,84 @@ class Popup extends EventDispatcher {      // Public properties +    /** +     * The ID of the popup. +     * @type {string} +     */      get id() {          return this._id;      } +    /** +     * The parent of the popup. +     * @type {Popup} +     */      get parent() {          return this._parent;      } +    /** +     * Sets the parent popup. +     * @param {Popup} value The parent popup to assign. +     */      set parent(value) {          this._parent = value;      } +    /** +     * The child of the popup. +     * @type {Popup} +     */      get child() {          return this._child;      } +    /** +     * Sets the child popup. +     * @param {Popup} value The child popup to assign. +     */      set child(value) {          this._child = value;      } +    /** +     * The depth of the popup. +     * @type {numer} +     */      get depth() {          return this._depth;      } +    /** +     * Gets the content window of the frame, which can be `null` +     * depending on the current state of the frame. +     * @type {?Window} +     */      get frameContentWindow() {          return this._frame.contentWindow;      } +    /** +     * Gets the DOM node that contains the frame. +     * @type {Element} +     */      get container() {          return this._container;      } +    /** +     * Gets the ID of the frame. +     * @type {number} +     */      get frameId() {          return this._frameId;      }      // Public functions +    /** +     * Prepares the popup for use. +     */      prepare() {          this._frame.addEventListener('mouseover', this._onFrameMouseOver.bind(this));          this._frame.addEventListener('mouseout', this._onFrameMouseOut.bind(this)); @@ -108,11 +159,19 @@ class Popup extends EventDispatcher {          this._onVisibleChange({value: this.isVisibleSync()});      } +    /** +     * Sets the options context for the popup. +     * @param {object} optionsContext The options context object. +     */      async setOptionsContext(optionsContext) {          await this._setOptionsContext(optionsContext);          await this._invokeSafe('setOptionsContext', {optionsContext});      } +    /** +     * Hides the popup. +     * @param {boolean} changeFocus Whether or not the parent popup or host frame should be focused. +     */      hide(changeFocus) {          if (!this.isVisibleSync()) {              return; @@ -127,18 +186,40 @@ class Popup extends EventDispatcher {          }      } +    /** +     * Returns whether or not the popup is currently visible. +     * @returns {Promise<boolean>} `true` if the popup is visible, `false` otherwise. +     */      async isVisible() {          return this.isVisibleSync();      } +    /** +     * 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`, +     *   or null if the override wasn't assigned. +     */      async setVisibleOverride(value, priority) {          return this._visible.setOverride(value, priority);      } +    /** +     * Clears a visibility override that was generated by `setVisibleOverride`. +     * @param {string} token The token returned from `setVisibleOverride`. +     * @returns {Promise<boolean>} `true` if the override existed and was removed, `false` otherwise. +     */      async clearVisibleOverride(token) {          return this._visible.clearOverride(token);      } +    /** +     * Checks whether a point is contained within the popup's rect. +     * @param {number} x The x coordinate. +     * @param {number} y The y coordinate. +     * @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) {              const rect = popup.getFrameRect(); @@ -149,6 +230,12 @@ class Popup extends EventDispatcher {          return false;      } +    /** +     * Shows and updates the positioning and content of the popup. +     * @param {{optionsContext: object, elementRect: {x: number, y: number, width: number, height: number}, writingMode: string}} details Settings for the outer popup. +     * @param {object} displayDetails The details parameter passed to `Display.setContent`; see that function for details. +     * @returns {Promise<void>} +     */      async showContent(details, displayDetails) {          if (this._options === null) { throw new Error('Options not assigned'); } @@ -166,24 +253,43 @@ class Popup extends EventDispatcher {          }      } +    /** +     * Sets the custom styles for the popup content. +     * @param {string} css The CSS rules. +     */      setCustomCss(css) {          this._invokeSafe('setCustomCss', {css});      } +    /** +     * Stops the audio auto-play timer, if one has started. +     */      clearAutoPlayTimer() {          this._invokeSafe('clearAutoPlayTimer');      } +    /** +     * Sets the scaling factor of the popup content. +     * @param {number} scale The scaling factor. +     */      setContentScale(scale) {          this._contentScale = scale;          this._frame.style.fontSize = `${scale}px`;          this._invokeSafe('setContentScale', {scale});      } +    /** +     * Returns whether or not the popup is currently visible, synchronously. +     * @returns {boolean} `true` if the popup is visible, `false` otherwise. +     */      isVisibleSync() {          return this._visible.value;      } +    /** +     * Updates the outer theme of the popup. +     * @returns {Promise<void>} +     */      updateTheme() {          const {popupTheme, popupOuterTheme} = this._options.general;          this._frame.dataset.theme = popupTheme; @@ -191,6 +297,12 @@ class Popup extends EventDispatcher {          this._frame.dataset.siteColor = this._getSiteColor();      } +    /** +     * Sets the custom styles for the outer popup container. +     * @param {string} css The CSS rules. +     * @param {boolean} useWebExtensionApi Whether or not web extension APIs should be used to inject the rules. +     *   When web extension APIs are used, a DOM node is not generated, making it harder to detect the changes. +     */      async setCustomOuterCss(css, useWebExtensionApi) {          let parentNode = null;          const inShadow = (this._shadow !== null); @@ -202,16 +314,31 @@ class Popup extends EventDispatcher {          this.trigger('customOuterCssChanged', {node, useWebExtensionApi, inShadow});      } +    /** +     * Gets the rectangle of the DOM frame, synchronously. +     * @returns {{x: number, y: number, width: number, height: number, valid: boolean}} The rect. +     *   `valid` is `false` for `PopupProxy`, since the DOM node is hosted in a different frame. +     */      getFrameRect() {          const {left, top, width, height} = this._frame.getBoundingClientRect();          return {x: left, y: top, width, height, valid: true};      } +    /** +     * Gets the size of the DOM frame. +     * @returns {Promise<{width: number, height: number, valid: boolean}>} The size and whether or not it is valid. +     */      async getFrameSize() {          const {width, height} = this._frame.getBoundingClientRect();          return {width, height, valid: true};      } +    /** +     * Sets the size of the DOM frame. +     * @param {number} width The desired width of the popup. +     * @param {number} height The desired height of the popup. +     * @returns {Promise<boolean>} `true` if the size assignment was successful, `false` otherwise. +     */      async setFrameSize(width, height) {          this._setFrameSize(width, height);          return true; diff --git a/ext/js/core.js b/ext/js/core.js index c43d7828..09468be7 100644 --- a/ext/js/core.js +++ b/ext/js/core.js @@ -17,8 +17,8 @@  /**   * Converts an `Error` object to a serializable JSON object. - * @param error An error object to convert. - * @returns A simple object which can be serialized by `JSON.stringify()`. + * @param {*} error An error object to convert. + * @returns {{name: string, message: string, stack: string, data?: *}|{value: *, hasValue: boolean}} A simple object which can be serialized by `JSON.stringify()`.   */  function serializeError(error) {      try { @@ -44,8 +44,8 @@ function serializeError(error) {  /**   * Converts a serialized erorr into a standard `Error` object. - * @param serializedError A simple object which was initially generated by serializeError. - * @returns A new `Error` instance. + * @param {{name: string, message: string, stack: string, data?: *}|{value: *, hasValue: boolean}} serializedError A simple object which was initially generated by serializeError. + * @returns {Error|*} A new `Error` instance.   */  function deserializeError(serializedError) {      if (serializedError.hasValue) { @@ -62,8 +62,8 @@ function deserializeError(serializedError) {  /**   * Checks whether a given value is a non-array object. - * @param value The value to check. - * @returns `true` if the value is an object and not an array, `false` otherwise. + * @param {*} value The value to check. + * @returns {boolean} `true` if the value is an object and not an array, `false` otherwise.   */  function isObject(value) {      return typeof value === 'object' && value !== null && !Array.isArray(value); @@ -72,8 +72,8 @@ function isObject(value) {  /**   * Converts any string into a form that can be passed into the RegExp constructor.   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - * @param string The string to convert to a valid regular expression. - * @returns The escaped string. + * @param {string} string The string to convert to a valid regular expression. + * @returns {string} The escaped string.   */  function escapeRegExp(string) {      return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); @@ -81,8 +81,8 @@ function escapeRegExp(string) {  /**   * Reverses a string. - * @param string The string to reverse. - * @returns The returned string, which retains proper UTF-16 surrogate pair order. + * @param {string} string The string to reverse. + * @returns {string} The returned string, which retains proper UTF-16 surrogate pair order.   */  function stringReverse(string) {      return [...string].reverse().join(''); @@ -90,8 +90,8 @@ function stringReverse(string) {  /**   * Creates a deep clone of an object or value. This is similar to `JSON.parse(JSON.stringify(value))`. - * @param value The value to clone. - * @returns A new clone of the value. + * @param {*} value The value to clone. + * @returns {*} A new clone of the value.   * @throws An error if the value is circular and cannot be cloned.   */  const clone = (() => { @@ -163,9 +163,9 @@ const clone = (() => {  /**   * Checks if an object or value is deeply equal to another object or value. - * @param value1 The first value to check. - * @param value2 The second value to check. - * @returns `true` if the values are the same object, or deeply equal without cycles. `false` otherwise. + * @param {*} value1 The first value to check. + * @param {*} value2 The second value to check. + * @returns {boolean} `true` if the values are the same object, or deeply equal without cycles. `false` otherwise.   */  const deepEqual = (() => {      // eslint-disable-next-line no-shadow @@ -235,8 +235,8 @@ const deepEqual = (() => {  /**   * Creates a new base-16 (lower case) string of a sequence of random bytes of the given length. - * @param length The number of bytes the string represents. The returned string's length will be twice as long. - * @returns A string of random characters. + * @param {number} length The number of bytes the string represents. The returned string's length will be twice as long. + * @returns {string} A string of random characters.   */  function generateId(length) {      const array = new Uint8Array(length); @@ -250,7 +250,7 @@ function generateId(length) {  /**   * Creates an unresolved promise that can be resolved later, outside the promise's executor function. - * @returns An object `{promise, resolve, reject}`, containing the promise and the resolve/reject functions. + * @returns {{promise: Promise, resolve: function, reject: function}} An object `{promise, resolve, reject}`, containing the promise and the resolve/reject functions.   */  function deferPromise() {      let resolve; @@ -264,9 +264,9 @@ function deferPromise() {  /**   * Creates a promise that is resolved after a set delay. - * @param delay How many milliseconds until the promise should be resolved. If 0, the promise is immediately resolved. - * @param resolveValue Optional; the value returned when the promise is resolved. - * @returns A promise with two additional properties: `resolve` and `reject`, which can be used to complete the promise early. + * @param {number} delay How many milliseconds until the promise should be resolved. If 0, the promise is immediately resolved. + * @param {*} [resolveValue] The value returned when the promise is resolved. + * @returns {Promise} A promise with two additional properties: `resolve` and `reject`, which can be used to complete the promise early.   */  function promiseTimeout(delay, resolveValue) {      if (delay <= 0) { @@ -306,8 +306,8 @@ function promiseTimeout(delay, resolveValue) {  /**   * Creates a promise that will resolve after the next animation frame, using `requestAnimationFrame`. - * @param timeout Optional; a maximum duration (in milliseconds) to wait until the promise resolves. If null or omitted, no timeout is used. - * @returns A promise that is resolved with `{time, timeout}`, where `time` is the timestamp from `requestAnimationFrame`, + * @param {number} [timeout] A maximum duration (in milliseconds) to wait until the promise resolves. If null or omitted, no timeout is used. + * @returns {Promise<{time: number, timeout: number}>} A promise that is resolved with `{time, timeout}`, where `time` is the timestamp from `requestAnimationFrame`,   *  and `timeout` is a boolean indicating whether the cause was a timeout or not.   * @throws The promise throws an error if animation is not supported in this context, such as in a service worker.   */ @@ -349,16 +349,17 @@ function promiseAnimationFrame(timeout=null) {  /**   * Invokes a standard message handler. This function is used to react and respond   * to communication messages within the extension. - * @param handler A handler function which is passed `params` and `...extraArgs` as arguments. - * @param async Whether or not the handler is async or not. Values include `false`, `true`, or `'dynamic'`. + * @param {object} details + * @param {function} details.handler A handler function which is passed `params` and `...extraArgs` as arguments. + * @param {boolean|string} details.async Whether or not the handler is async or not. Values include `false`, `true`, or `'dynamic'`.   *   When the value is `'dynamic'`, the handler should return an object of the format `{async: boolean, result: any}`. - * @param params Information which was passed with the original message. - * @param callback A callback function which is invoked after the handler has completed. The value passed + * @param {object} params Information which was passed with the original message. + * @param {function} callback A callback function which is invoked after the handler has completed. The value passed   *   to the function is in the format:   *   * `{result: any}` if the handler invoked successfully.   *   * `{error: object}` if the handler thew an error. The error is serialized. - * @param extraArgs Additional arguments which are passed to the `handler` function. - * @returns `true` if the function is invoked asynchronously, `false` otherwise. + * @param {...*} extraArgs Additional arguments which are passed to the `handler` function. + * @returns {boolean} `true` if the function is invoked asynchronously, `false` otherwise.   */  function invokeMessageHandler({handler, async}, params, callback, ...extraArgs) {      try { @@ -395,9 +396,9 @@ class EventDispatcher {      /**       * Triggers an event with the given name and specified argument. -     * @param eventName The string representing the event's name. -     * @param details Optional; the argument passed to the callback functions. -     * @returns `true` if any callbacks were registered, `false` otherwise. +     * @param {string} eventName The string representing the event's name. +     * @param {*} [details] The argument passed to the callback functions. +     * @returns {boolean} `true` if any callbacks were registered, `false` otherwise.       */      trigger(eventName, details) {          const callbacks = this._eventMap.get(eventName); @@ -411,8 +412,8 @@ class EventDispatcher {      /**       * Adds a single event listener to a specific event. -     * @param eventName The string representing the event's name. -     * @param callback The event listener callback to add. +     * @param {string} eventName The string representing the event's name. +     * @param {function} callback The event listener callback to add.       */      on(eventName, callback) {          let callbacks = this._eventMap.get(eventName); @@ -425,9 +426,9 @@ class EventDispatcher {      /**       * Removes a single event listener from a specific event. -     * @param eventName The string representing the event's name. -     * @param callback The event listener callback to add. -     * @returns `true` if the callback was removed, `false` otherwise. +     * @param {string} eventName The string representing the event's name. +     * @param {function} callback The event listener callback to add. +     * @returns {boolean} `true` if the callback was removed, `false` otherwise.       */      off(eventName, callback) {          const callbacks = this._eventMap.get(eventName); @@ -448,8 +449,8 @@ class EventDispatcher {      /**       * Checks if an event has any listeners. -     * @param eventName The string representing the event's name. -     * @returns `true` if the event has listeners, `false` otherwise. +     * @param {string} eventName The string representing the event's name. +     * @returns {boolean} `true` if the event has listeners, `false` otherwise.       */      hasListeners(eventName) {          const callbacks = this._eventMap.get(eventName); @@ -470,7 +471,7 @@ class EventListenerCollection {      /**       * Returns the number of event listeners that are currently in the object. -     * @returns The number of event listeners that are currently in the object. +     * @type {number}       */      get size() {          return this._eventListeners.length; @@ -478,9 +479,9 @@ class EventListenerCollection {      /**       * Adds an event listener of a generic type. -     * @param type The type of event listener, which can be 'addEventListener', 'addListener', or 'on'. -     * @param object The object to add the event listener to. -     * @param args The argument array passed to the object's event listener adding function. +     * @param {string} type The type of event listener, which can be 'addEventListener', 'addListener', or 'on'. +     * @param {object} object The object to add the event listener to. +     * @param {...*} args The argument array passed to the object's event listener adding function.       * @throws An error if type is not an expected value.       */      addGeneric(type, object, ...args) { @@ -494,8 +495,8 @@ class EventListenerCollection {      /**       * Adds an event listener using `object.addEventListener`. The listener will later be removed using `object.removeEventListener`. -     * @param object The object to add the event listener to. -     * @param args The argument array passed to the `addEventListener`/`removeEventListener` functions. +     * @param {object} object The object to add the event listener to. +     * @param {...*} args The argument array passed to the `addEventListener`/`removeEventListener` functions.       */      addEventListener(object, ...args) {          object.addEventListener(...args); @@ -504,8 +505,8 @@ class EventListenerCollection {      /**       * Adds an event listener using `object.addListener`. The listener will later be removed using `object.removeListener`. -     * @param object The object to add the event listener to. -     * @param args The argument array passed to the `addListener`/`removeListener` function. +     * @param {object} object The object to add the event listener to. +     * @param {...*} args The argument array passed to the `addListener`/`removeListener` function.       */      addListener(object, ...args) {          object.addListener(...args); @@ -514,8 +515,8 @@ class EventListenerCollection {      /**       * Adds an event listener using `object.on`. The listener will later be removed using `object.off`. -     * @param object The object to add the event listener to. -     * @param args The argument array passed to the `on`/`off` function. +     * @param {object} object The object to add the event listener to. +     * @param {...*} args The argument array passed to the `on`/`off` function.       */      on(object, ...args) {          object.on(...args); @@ -553,7 +554,7 @@ class EventListenerCollection {  class DynamicProperty extends EventDispatcher {      /**       * Creates a new instance with the specified value. -     * @param value The value to assign. +     * @param {*} value The value to assign.       */      constructor(value) {          super(); @@ -565,6 +566,7 @@ class DynamicProperty extends EventDispatcher {      /**       * Gets the default value for the property, which is assigned to the       * public value property when no overrides are present. +     * @type {*}       */      get defaultValue() {          return this._defaultValue; @@ -574,7 +576,7 @@ class DynamicProperty extends EventDispatcher {       * Assigns the default value for the property. If no overrides are present       * and if the value is different than the current default value,       * the 'change' event will be triggered. -     * @param value The value to assign. +     * @param {*} value The value to assign.       */      set defaultValue(value) {          this._defaultValue = value; @@ -583,6 +585,7 @@ class DynamicProperty extends EventDispatcher {      /**       * Gets the current value for the property, taking any overrides into account. +     * @type {*}       */      get value() {          return this._value; @@ -590,6 +593,7 @@ class DynamicProperty extends EventDispatcher {      /**       * Gets the number of overrides added to the property. +     * @type {*}       */      get overrideCount() {          return this._overrides.length; @@ -602,9 +606,9 @@ class DynamicProperty extends EventDispatcher {       * If the newly added override has the highest priority of all overrides       * and if the override value is different from the current value,       * the 'change' event will be fired. -     * @param value The override value to assign. -     * @param priority The priority value to use, as a number. -     * @returns A string token which can be passed to the clearOverride function +     * @param {*} value The override value to assign. +     * @param {number} [priority] The priority value to use, as a number. +     * @returns {string} A string token which can be passed to the clearOverride function       *  to remove the override.       */      setOverride(value, priority=0) { @@ -623,8 +627,8 @@ class DynamicProperty extends EventDispatcher {       * Removes a specific override value. If the removed override       * had the highest priority, and the new value is different from       * the previous value, the 'change' event will be fired. -     * @param token The token for the corresponding override which is to be removed. -     * @returns true if an override was returned, false otherwise. +     * @param {string} token The token for the corresponding override which is to be removed. +     * @returns {boolean} `true` if an override was returned, `false` otherwise.       */      clearOverride(token) {          for (let i = 0, ii = this._overrides.length; i < ii; ++i) { @@ -670,10 +674,10 @@ class Logger extends EventDispatcher {      /**       * Logs a generic error. This will trigger the 'log' event with the same arguments as the function invocation. -     * @param error The error to log. This is typically an `Error` or `Error`-like object. -     * @param level The level to log at. Values include `'info'`, `'debug'`, `'warn'`, and `'error'`. +     * @param {Error|object|*} error The error to log. This is typically an `Error` or `Error`-like object. +     * @param {string} level The level to log at. Values include `'info'`, `'debug'`, `'warn'`, and `'error'`.       *   Other values will be logged at a non-error level. -     * @param context An optional context object for the error which should typically include a `url` field. +     * @param {?object} [context] An optional context object for the error which should typically include a `url` field.       */      log(error, level, context=null) {          if (!isObject(context)) { @@ -735,8 +739,8 @@ class Logger extends EventDispatcher {      /**       * Logs a warning. This function invokes `log` internally. -     * @param error The error to log. This is typically an `Error` or `Error`-like object. -     * @param context An optional context object for the error which should typically include a `url` field. +     * @param {Error|object|*} error The error to log. This is typically an `Error` or `Error`-like object. +     * @param {?object} context An optional context object for the error which should typically include a `url` field.       */      warn(error, context=null) {          this.log(error, 'warn', context); @@ -744,8 +748,8 @@ class Logger extends EventDispatcher {      /**       * Logs an error. This function invokes `log` internally. -     * @param error The error to log. This is typically an `Error` or `Error`-like object. -     * @param context An optional context object for the error which should typically include a `url` field. +     * @param {Error|object|*} error The error to log. This is typically an `Error` or `Error`-like object. +     * @param {?object} context An optional context object for the error which should typically include a `url` field.       */      error(error, context=null) {          this.log(error, 'error', context); diff --git a/ext/js/yomichan.js b/ext/js/yomichan.js index 1eae2d4d..c65e2f7d 100644 --- a/ext/js/yomichan.js +++ b/ext/js/yomichan.js @@ -39,7 +39,13 @@ if ((() => {      chrome = browser;  } +/** + * The Yomichan class is a core component through which various APIs are handled and invoked. + */  class Yomichan extends EventDispatcher { +    /** +     * Creates a new instance. The instance should not be used until it has been fully prepare()'d. +     */      constructor() {          super(); @@ -72,24 +78,44 @@ class Yomichan extends EventDispatcher {          ]);      } -    // Public - +    /** +     * Whether the current frame is the background page/service worker or not. +     * @type {boolean} +     */      get isBackground() {          return this._isBackground;      } +    /** +     * Whether or not the extension is unloaded. +     * @type {boolean} +     */      get isExtensionUnloaded() {          return this._isExtensionUnloaded;      } +    /** +     * Gets the API instance for communicating with the backend. +     * This value will be null on the background page/service worker. +     * @type {API} +     */      get api() {          return this._api;      } +    /** +     * Gets the CrossFrameAPI instance for communicating with different frames. +     * This value will be null on the background page/service worker. +     * @type {CrossFrameAPI} +     */      get crossFrame() {          return this._crossFrame;      } +    /** +     * Prepares the instance for use. +     * @param {boolean} [isBackground=false] Assigns whether this instance is being used from the background page/service worker. +     */      async prepare(isBackground=false) {          this._isBackground = isBackground;          chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); @@ -107,11 +133,20 @@ class Yomichan extends EventDispatcher {          }      } +    /** +     * Sends a message to the backend indicating that the frame is ready and all script +     * setup has completed. +     */      ready() {          this._isReady = true;          this.sendMessage({action: 'yomichanReady'});      } +    /** +     * Checks whether or not a URL is an extension URL. +     * @param {string} url The URL to check. +     * @returns true if the URL is an extension URL, false otherwise. +     */      isExtensionUrl(url) {          try {              return url.startsWith(chrome.runtime.getURL('/')); @@ -120,6 +155,11 @@ class Yomichan extends EventDispatcher {          }      } +    /** +     * Runs chrome.runtime.sendMessage() with additional exception handling events. +     * @param {...*} args The arguments to be passed to chrome.runtime.sendMessage(). +     * @returns {void} The result of the chrome.runtime.sendMessage() call. +     */      sendMessage(...args) {          try {              return chrome.runtime.sendMessage(...args); @@ -129,6 +169,11 @@ class Yomichan extends EventDispatcher {          }      } +    /** +     * Runs chrome.runtime.connect() with additional exception handling events. +     * @param {...*} args The arguments to be passed to chrome.runtime.connect(). +     * @returns {Port} The resulting port. +     */      connect(...args) {          try {              return chrome.runtime.connect(...args); @@ -138,6 +183,9 @@ class Yomichan extends EventDispatcher {          }      } +    /** +     * Runs chrome.runtime.connect() with additional exception handling events. +     */      triggerExtensionUnloaded() {          this._isExtensionUnloaded = true;          if (this._isTriggeringExtensionUnloaded) { return; } @@ -200,4 +248,7 @@ class Yomichan extends EventDispatcher {      }  } +/** + * The default Yomichan class instance. + */  const yomichan = new Yomichan(); |