diff options
Diffstat (limited to 'ext')
-rw-r--r-- | ext/js/dom/document-focus-controller.js | 12 | ||||
-rw-r--r-- | ext/js/dom/document-util.js | 92 | ||||
-rw-r--r-- | ext/js/dom/text-source-element.js | 88 | ||||
-rw-r--r-- | ext/js/dom/text-source-range.js | 110 |
4 files changed, 302 insertions, 0 deletions
diff --git a/ext/js/dom/document-focus-controller.js b/ext/js/dom/document-focus-controller.js index bc6c61b9..d6c24646 100644 --- a/ext/js/dom/document-focus-controller.js +++ b/ext/js/dom/document-focus-controller.js @@ -22,11 +22,19 @@ * focus a dummy element inside the main content, which gives keyboard scroll focus to that element. */ class DocumentFocusController { + /** + * Creates a new instance of the class. + * @param {?string} autofocusElementSelector A selector string which can be used to specify an element which + * should be automatically focused on prepare. + */ constructor(autofocusElementSelector=null) { this._autofocusElement = (autofocusElementSelector !== null ? document.querySelector(autofocusElementSelector) : null); this._contentScrollFocusElement = document.querySelector('#content-scroll-focus'); } + /** + * Initializes the instance. + */ prepare() { window.addEventListener('focus', this._onWindowFocus.bind(this), false); this._updateFocusedElement(false); @@ -35,6 +43,10 @@ class DocumentFocusController { } } + /** + * Removes focus from a given element. + * @param {Element} element The element to remove focus from. + */ blurElement(element) { if (document.activeElement !== element) { return; } element.blur(); diff --git a/ext/js/dom/document-util.js b/ext/js/dom/document-util.js index 73525ff5..ed0a98e7 100644 --- a/ext/js/dom/document-util.js +++ b/ext/js/dom/document-util.js @@ -21,6 +21,9 @@ * TextSourceRange */ +/** + * This class contains utility functions related to the HTML document. + */ class DocumentUtil { /** * Options to configure how element detection is performed. @@ -251,11 +254,23 @@ class DocumentUtil { return scale; } + /** + * Converts a rect based on the CSS zoom scaling for a given node. + * @param {DOMRect} rect The rect to convert. + * @param {Node} node The node to compute the zoom from. + * @returns {DOMRect} The updated rect, or the same rect if no change is needed. + */ static convertRectZoomCoordinates(rect, node) { const scale = this.computeZoomScale(node); return (scale === 1 ? rect : new DOMRect(rect.left * scale, rect.top * scale, rect.width * scale, rect.height * scale)); } + /** + * Converts multiple rects based on the CSS zoom scaling for a given node. + * @param {DOMRect[]} rects The rects to convert. + * @param {Node} node The node to compute the zoom from. + * @returns {DOMRect[]} The updated rects, or the same rects array if no change is needed. + */ static convertMultipleRectZoomCoordinates(rects, node) { const scale = this.computeZoomScale(node); if (scale === 1) { return rects; } @@ -266,6 +281,13 @@ class DocumentUtil { return results; } + /** + * Checks whether a given point is contained within a rect. + * @param {number} x The horizontal coordinate. + * @param {number} y The vertical coordinate. + * @param {DOMRect} rect The rect to check. + * @returns {boolean} `true` if the point is inside the rect, `false` otherwise. + */ static isPointInRect(x, y, rect) { return ( x >= rect.left && x < rect.right && @@ -273,6 +295,13 @@ class DocumentUtil { ); } + /** + * Checks whether a given point is contained within any rect in a list. + * @param {number} x The horizontal coordinate. + * @param {number} y The vertical coordinate. + * @param {DOMRect[]} rects The rect to check. + * @returns {boolean} `true` if the point is inside any of the rects, `false` otherwise. + */ static isPointInAnyRect(x, y, rects) { for (const rect of rects) { if (this.isPointInRect(x, y, rect)) { @@ -282,6 +311,13 @@ class DocumentUtil { return false; } + /** + * Checks whether a given point is contained within a selection range. + * @param {number} x The horizontal coordinate. + * @param {number} y The vertical coordinate. + * @param {Selection} selection The selection to check. + * @returns {boolean} `true` if the point is inside the selection, `false` otherwise. + */ static isPointInSelection(x, y, selection) { for (let i = 0; i < selection.rangeCount; ++i) { const range = selection.getRangeAt(i); @@ -302,6 +338,11 @@ class DocumentUtil { } } + /** + * Gets an array of the active modifier keys. + * @param {KeyboardEvent|MouseEvent|TouchEvent} event The event to check. + * @returns {string[]} An array of modifiers. + */ static getActiveModifiers(event) { const modifiers = []; if (event.altKey) { modifiers.push('alt'); } @@ -311,18 +352,33 @@ class DocumentUtil { return modifiers; } + /** + * Gets an array of the active modifier keys and buttons. + * @param {KeyboardEvent|MouseEvent|TouchEvent} event The event to check. + * @returns {string[]} An array of modifiers and buttons. + */ static getActiveModifiersAndButtons(event) { const modifiers = this.getActiveModifiers(event); this._getActiveButtons(event, modifiers); return modifiers; } + /** + * Gets an array of the active buttons. + * @param {KeyboardEvent|MouseEvent|TouchEvent} event The event to check. + * @returns {string[]} An array of modifiers and buttons. + */ static getActiveButtons(event) { const buttons = []; this._getActiveButtons(event, buttons); return buttons; } + /** + * Adds a fullscreen change event listener. This function handles all of the browser-specific variants. + * @param {Function} onFullscreenChanged The event callback. + * @param {?EventListenerCollection} eventListenerCollection An optional `EventListenerCollection` to add the registration to. + */ static addFullscreenChangeEventListener(onFullscreenChanged, eventListenerCollection=null) { const target = document; const options = false; @@ -341,6 +397,10 @@ class DocumentUtil { } } + /** + * Returns the current fullscreen element. This function handles all of the browser-specific variants. + * @returns {?Element} The current fullscreen element, or `null` if the window is not fullscreen. + */ static getFullscreenElement() { return ( document.fullscreenElement || @@ -351,6 +411,11 @@ class DocumentUtil { ); } + /** + * Gets all of the nodes within a `Range`. + * @param {Range} range The range to check. + * @returns {Node[]} The list of nodes. + */ static getNodesInRange(range) { const end = range.endContainer; const nodes = []; @@ -361,6 +426,11 @@ class DocumentUtil { return nodes; } + /** + * Gets the next node after a specified node. This traverses the DOM in its logical order. + * @param {Node} node The node to start at. + * @returns {?Node} The next node, or `null` if there is no next node. + */ static getNextNode(node) { let next = node.firstChild; if (next === null) { @@ -377,6 +447,12 @@ class DocumentUtil { return next; } + /** + * Checks whether any node in a list of nodes matches a selector. + * @param {Node[]} nodes The list of ndoes to check. + * @param {string} selector The selector to test. + * @returns {boolean} `true` if any element node matches the selector, `false` otherwise. + */ static anyNodeMatchesSelector(nodes, selector) { const ELEMENT_NODE = Node.ELEMENT_NODE; for (let node of nodes) { @@ -389,6 +465,12 @@ class DocumentUtil { return false; } + /** + * Checks whether every node in a list of nodes matches a selector. + * @param {Node[]} nodes The list of ndoes to check. + * @param {string} selector The selector to test. + * @returns {boolean} `true` if every element node matches the selector, `false` otherwise. + */ static everyNodeMatchesSelector(nodes, selector) { const ELEMENT_NODE = Node.ELEMENT_NODE; for (let node of nodes) { @@ -401,10 +483,20 @@ class DocumentUtil { return true; } + /** + * Checks whether the meta key is supported in the browser on the specified operating system. + * @param {string} os The operating system to check. + * @param {string} browser The browser to check. + * @returns {boolean} `true` if supported, `false` otherwise. + */ static isMetaKeySupported(os, browser) { return !(browser === 'firefox' || browser === 'firefox-mobile') || os === 'mac'; } + /** + * Checks whether an element on the page that can accept input is focused. + * @returns {boolean} `true` if an input element is focused, `false` otherwise. + */ static isInputElementFocused() { const element = document.activeElement; if (element === null) { return false; } diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js index 13c5cd86..1c60a5b8 100644 --- a/ext/js/dom/text-source-element.js +++ b/ext/js/dom/text-source-element.js @@ -20,7 +20,18 @@ * StringUtil */ +/** + * This class represents a text source that is attached to a HTML element, such as an <img> + * with alt text or a <button>. + */ class TextSourceElement { + /** + * Creates a new instance of the class. + * @param {Element} element The source element. + * @param {string} fullContent The string representing the element's full text value. + * @param {number} startOffset The text start offset position within the full content. + * @param {number} endOffset The text end offset position within the full content. + */ constructor(element, fullContent, startOffset, endOffset) { this._element = element; this._fullContent = fullContent; @@ -29,38 +40,76 @@ class TextSourceElement { this._content = this._fullContent.substring(this._startOffset, this._endOffset); } + /** + * Gets the type name of this instance. + * @type {string} + */ get type() { return 'element'; } + /** + * The source element. + * @type {Element} + */ get element() { return this._element; } + /** + * The string representing the element's full text value. + * @type {string} + */ get fullContent() { return this._fullContent; } + /** + * The text start offset position within the full content. + * @type {number} + */ get startOffset() { return this._startOffset; } + /** + * The text end offset position within the full content. + * @type {number} + */ get endOffset() { return this._endOffset; } + /** + * Creates a clone of the instance. + * @returns {TextSourceElement} The new clone. + */ clone() { return new TextSourceElement(this._element, this._fullContent, this._startOffset, this._endOffset); } + /** + * Performs any cleanup that is necessary after the element has been used. + */ cleanup() { // NOP } + /** + * Gets the selected text of element, which is a substring of the full content + * starting at `startOffset` and ending at `endOffset`. + * @returns {string} The text content. + */ text() { return this._content; } + /** + * Moves the end offset of the text by a set amount of unicode codepoints. + * @param {number} length The maximum number of codepoints to move by. + * @param {boolean} fromEnd Whether to move the offset from the current end position (if `true`) or the start position (if `false`). + * @returns {number} The actual number of characters (not codepoints) that were read. + */ setEndOffset(length, fromEnd) { const offset = fromEnd ? this._endOffset : this._startOffset; length = Math.min(this._fullContent.length - offset, length); @@ -72,6 +121,11 @@ class TextSourceElement { return length; } + /** + * Moves the start offset of the text by a set amount of unicode codepoints. + * @param {number} length The maximum number of codepoints to move by. + * @returns {number} The actual number of characters (not codepoints) that were read. + */ setStartOffset(length) { length = Math.min(this._startOffset, length); if (length > 0) { @@ -82,22 +136,42 @@ class TextSourceElement { return length; } + /** + * Gets the rects that represent the position and bounds of the text source. + * @returns {DOMRect[]} The rects. + */ getRects() { return DocumentUtil.convertMultipleRectZoomCoordinates(this._element.getClientRects(), this._element); } + /** + * Gets writing mode that is used for this element. + * See: https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode. + * @returns {string} The rects. + */ getWritingMode() { return 'horizontal-tb'; } + /** + * Selects the text source in the document. + */ select() { // NOP } + /** + * Deselects the text source in the document. + */ deselect() { // NOP } + /** + * Checks whether another text source has the same starting point. + * @param {TextSourceElement|TextSourceRange} other The other source to test. + * @returns {boolean} `true` if the starting points are equivalent, `false` otherwise. + */ hasSameStart(other) { return ( typeof other === 'object' && @@ -109,14 +183,28 @@ class TextSourceElement { ); } + /** + * Gets a list of the nodes in this text source's range. + * @returns {Node[]} The nodes in the range. + */ getNodesInRange() { return [this._element]; } + /** + * Creates a new instance for a given element. + * @param {Element} element The source element. + * @returns {TextSourceElement} A new instance of the class corresponding to the element. + */ static create(element) { return new TextSourceElement(element, this._getElementContent(element), 0, 0); } + /** + * Gets the full content string for a given element. + * @param {Element} element The element to get the full content of. + * @returns {string} The content string. + */ static _getElementContent(element) { let content; switch (element.nodeName.toUpperCase()) { diff --git a/ext/js/dom/text-source-range.js b/ext/js/dom/text-source-range.js index e03783a5..a0225748 100644 --- a/ext/js/dom/text-source-range.js +++ b/ext/js/dom/text-source-range.js @@ -20,7 +20,29 @@ * DocumentUtil */ +/** + * This class represents a text source that comes from text nodes in the document. + * Sometimes a temporary "imposter" element is created and used to store the text. + * This element is typically hidden from the page and removed after scanning has completed. + */ class TextSourceRange { + /** + * Creates a new instance of the class. + * @param {Range} range The selection range. + * @param {number} rangeStartOffset The `startOffset` of the range. This is somewhat redundant + * with the `range` parameter, but it is used when for when imposter elements are removed. + * @param {string} content The `toString()` value of the range. This is somewhat redundant + * with the `range` parameter, but it is used when for when imposter elements are removed. + * @param {?Element} imposterElement The temporary imposter element. + * @param {?Element} imposterSourceElement The source element which the imposter is imitating. + * Must not be `null` if imposterElement is specified. + * @param {?Rect[]} cachedRects A set of cached `DOMRect`s representing the rects of the text source, + * which can be used after the imposter element is removed from the page. + * Must not be `null` if imposterElement is specified. + * @param {?Rect} cachedSourceRect A cached `DOMRect` representing the rect of the `imposterSourceElement`, + * which can be used after the imposter element is removed from the page. + * Must not be `null` if imposterElement is specified. + */ constructor(range, rangeStartOffset, content, imposterElement, imposterSourceElement, cachedRects, cachedSourceRect) { this._range = range; this._rangeStartOffset = rangeStartOffset; @@ -31,22 +53,42 @@ class TextSourceRange { this._cachedSourceRect = cachedSourceRect; } + /** + * Gets the type name of this instance. + * @type {string} + */ get type() { return 'range'; } + /** + * The internal range object. + * @type {Range} + */ get range() { return this._range; } + /** + * The starting offset for the range. + * @type {Range} + */ get rangeStartOffset() { return this._rangeStartOffset; } + /** + * The source element that the imposter element is imitating, if present. + * @type {?Element} + */ get imposterSourceElement() { return this._imposterSourceElement; } + /** + * Creates a clone of the instance. + * @returns {TextSourceRange} The new clone. + */ clone() { return new TextSourceRange( this._range.cloneRange(), @@ -59,16 +101,31 @@ class TextSourceRange { ); } + /** + * Performs any cleanup that is necessary after the element has been used. + */ cleanup() { if (this._imposterElement !== null && this._imposterElement.parentNode !== null) { this._imposterElement.parentNode.removeChild(this._imposterElement); } } + /** + * Gets the selected text of element, which is the `toString()` version of the range. + * @returns {string} The text content. + */ text() { return this._content; } + /** + * Moves the end offset of the text by a set amount of unicode codepoints. + * @param {number} length The maximum number of codepoints to move by. + * @param {boolean} fromEnd Whether to move the offset from the current end position (if `true`) or the start position (if `false`). + * @param {boolean} layoutAwareScan Whether or not HTML layout information should be used to generate + * the string content when scanning. + * @returns {number} The actual number of characters (not codepoints) that were read. + */ setEndOffset(length, fromEnd, layoutAwareScan) { let node; let offset; @@ -85,6 +142,13 @@ class TextSourceRange { return length - state.remainder; } + /** + * Moves the start offset of the text by a set amount of unicode codepoints. + * @param {number} length The maximum number of codepoints to move by. + * @param {boolean} layoutAwareScan Whether or not HTML layout information should be used to generate + * the string content when scanning. + * @returns {number} The actual number of characters (not codepoints) that were read. + */ setStartOffset(length, layoutAwareScan) { const state = new DOMTextScanner(this._range.startContainer, this._range.startOffset, !layoutAwareScan, layoutAwareScan).seek(-length); this._range.setStart(state.node, state.offset); @@ -93,16 +157,28 @@ class TextSourceRange { return length - state.remainder; } + /** + * Gets the rects that represent the position and bounds of the text source. + * @returns {DOMRect[]} The rects. + */ getRects() { if (this._isImposterDisconnected()) { return this._getCachedRects(); } return DocumentUtil.convertMultipleRectZoomCoordinates(this._range.getClientRects(), this._range.startContainer); } + /** + * Gets writing mode that is used for this element. + * See: https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode. + * @returns {string} The rects. + */ getWritingMode() { const node = this._isImposterDisconnected() ? this._imposterSourceElement : this._range.startContainer; return DocumentUtil.getElementWritingMode(node !== null ? node.parentElement : null); } + /** + * Selects the text source in the document. + */ select() { if (this._imposterElement !== null) { return; } const selection = window.getSelection(); @@ -110,12 +186,22 @@ class TextSourceRange { selection.addRange(this._range); } + /** + * Deselects the text source in the document. + */ deselect() { if (this._imposterElement !== null) { return; } const selection = window.getSelection(); selection.removeAllRanges(); } + /** + * Checks whether another text source has the same starting point. + * @param {TextSourceElement|TextSourceRange} other The other source to test. + * @returns {boolean} `true` if the starting points are equivalent, `false` otherwise. + * @throws {Error} An exception can be thrown if `Range.compareBoundaryPoints` fails, + * which shouldn't happen, but the handler is kept in case of unexpected errors. + */ hasSameStart(other) { if (!( typeof other === 'object' && @@ -142,24 +228,48 @@ class TextSourceRange { } } + /** + * Gets a list of the nodes in this text source's range. + * @returns {Node[]} The nodes in the range. + */ getNodesInRange() { return DocumentUtil.getNodesInRange(this._range); } + /** + * Creates a new instance for a given range. + * @param {Range} range The source range. + * @returns {TextSourceRange} A new instance of the class corresponding to the range. + */ static create(range) { return new TextSourceRange(range, range.startOffset, range.toString(), null, null, null, null); } + /** + * Creates a new instance for a given range using an imposter element. + * @param {Range} range The source range. + * @param {Element} imposterElement The temporary imposter element. + * @param {Element} imposterSourceElement The source element which the imposter is imitating. + * @returns {TextSourceRange} A new instance of the class corresponding to the range. + */ static createFromImposter(range, imposterElement, imposterSourceElement) { const cachedRects = DocumentUtil.convertMultipleRectZoomCoordinates(range.getClientRects(), range.startContainer); const cachedSourceRect = DocumentUtil.convertRectZoomCoordinates(imposterSourceElement.getBoundingClientRect(), imposterSourceElement); return new TextSourceRange(range, range.startOffset, range.toString(), imposterElement, imposterSourceElement, cachedRects, cachedSourceRect); } + /** + * Checks whether the imposter element has been removed, if the instance is using one. + * @returns {boolean} `true` if the instance has an imposter and it's no longer connected to the document, `false` otherwise. + */ _isImposterDisconnected() { return this._imposterElement !== null && !this._imposterElement.isConnected; } + /** + * Gets the cached rects for a disconnected imposter element. + * @returns {Rect[]} The rects for the element. + */ _getCachedRects() { const sourceRect = DocumentUtil.convertRectZoomCoordinates(this._imposterSourceElement.getBoundingClientRect(), this._imposterSourceElement); return DocumentUtil.offsetDOMRects( |