diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-02-13 23:13:53 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-02-13 23:13:53 -0500 | 
| commit | 7a74c3c31ece7788e82c46f22cb4327ffe08307a (patch) | |
| tree | 7d4aee53b1dab15bdf317729ee1559291c04a4b2 /ext/fg/js | |
| parent | 6a271e067fa917614f4c81f473533e24c6d04404 (diff) | |
Move fg/js (#1384)
* Move fg/js/frame-ancestry-handler.js to js/comm/frame-ancestry-handler.js
* Move fg/js/frame-offset-forwarder.js to js/comm/frame-offset-forwarder.js
* Move fg/js/dom-text-scanner.js to js/dom/dom-text-scanner.js
* Move fg/js/text-source-element.js to js/dom/text-source-element.js
* Move fg/js/text-source-range.js to js/dom/text-source-range.js
* Move fg/js/float-main.js to js/display/popup-main.js
* Move fg/js/content-script-main.js to js/app/content-script-main.js
* Move fg/js/frontend.js to js/app/frontend.js
* Move fg/js/popup-factory.js to js/app/popup-factory.js
* Move fg/js/popup-proxy.js to js/app/popup-proxy.js
* Move fg/js/popup-window.js to js/app/popup-window.js
* Move fg/js/popup.js to js/app/popup.js
Diffstat (limited to 'ext/fg/js')
| -rw-r--r-- | ext/fg/js/content-script-main.js | 59 | ||||
| -rw-r--r-- | ext/fg/js/dom-text-scanner.js | 551 | ||||
| -rw-r--r-- | ext/fg/js/float-main.js | 56 | ||||
| -rw-r--r-- | ext/fg/js/frame-ancestry-handler.js | 269 | ||||
| -rw-r--r-- | ext/fg/js/frame-offset-forwarder.js | 70 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 691 | ||||
| -rw-r--r-- | ext/fg/js/popup-factory.js | 319 | ||||
| -rw-r--r-- | ext/fg/js/popup-proxy.js | 218 | ||||
| -rw-r--r-- | ext/fg/js/popup-window.js | 169 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 687 | ||||
| -rw-r--r-- | ext/fg/js/text-source-element.js | 139 | ||||
| -rw-r--r-- | ext/fg/js/text-source-range.js | 170 | 
12 files changed, 0 insertions, 3398 deletions
| diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js deleted file mode 100644 index 5dee4c56..00000000 --- a/ext/fg/js/content-script-main.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2019-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Frontend - * HotkeyHandler - * PopupFactory - * api - */ - -(async () => { -    try { -        api.forwardLogsToBackend(); -        await yomichan.backendReady(); - -        const {tabId, frameId} = await api.frameInformationGet(); -        if (typeof frameId !== 'number') { -            throw new Error('Failed to get frameId'); -        } - -        const hotkeyHandler = new HotkeyHandler(); -        hotkeyHandler.prepare(); - -        const popupFactory = new PopupFactory(frameId); -        popupFactory.prepare(); - -        const frontend = new Frontend({ -            tabId, -            frameId, -            popupFactory, -            depth: 0, -            parentPopupId: null, -            parentFrameId: null, -            useProxyPopup: false, -            pageType: 'web', -            allowRootFramePopupProxy: true, -            hotkeyHandler -        }); -        await frontend.prepare(); - -        yomichan.ready(); -    } catch (e) { -        yomichan.logError(e); -    } -})(); diff --git a/ext/fg/js/dom-text-scanner.js b/ext/fg/js/dom-text-scanner.js deleted file mode 100644 index 71e74fc3..00000000 --- a/ext/fg/js/dom-text-scanner.js +++ /dev/null @@ -1,551 +0,0 @@ -/* - * Copyright (C) 2020-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/** - * A class used to scan text in a document. - */ -class DOMTextScanner { -    /** -     * Creates a new instance of a DOMTextScanner. -     * @param node The DOM Node to start at. -     * @param offset The character offset in to start at when node is a text node. -     *   Use 0 for non-text nodes. -     */ -    constructor(node, offset, forcePreserveWhitespace=false, generateLayoutContent=true) { -        const ruby = DOMTextScanner.getParentRubyElement(node); -        const resetOffset = (ruby !== null); -        if (resetOffset) { node = ruby; } - -        this._node = node; -        this._offset = offset; -        this._content = ''; -        this._remainder = 0; -        this._resetOffset = resetOffset; -        this._newlines = 0; -        this._lineHasWhitespace = false; -        this._lineHasContent = false; -        this._forcePreserveWhitespace = forcePreserveWhitespace; -        this._generateLayoutContent = generateLayoutContent; -    } - -    /** -     * Gets the current node being scanned. -     * @returns A DOM Node. -     */ -    get node() { -        return this._node; -    } - -    /** -     * Gets the current offset corresponding to the node being scanned. -     * This value is only applicable for text nodes. -     * @returns An integer. -     */ -    get offset() { -        return this._offset; -    } - -    /** -     * Gets the remaining number of characters that weren't scanned in the last seek() call. -     * This value is usually 0 unless the end of the document was reached. -     * @returns An integer. -     */ -    get remainder() { -        return this._remainder; -    } - -    /** -     * Gets the accumulated content string resulting from calls to seek(). -     * @returns A string. -     */ -    get content() { -        return this._content; -    } - -    /** -     * Seeks a given length in the document and accumulates the text content. -     * @param length A positive or negative integer corresponding to how many characters -     *   should be added to content. Content is only added to the accumulation string, -     *   never removed, so mixing seek calls with differently signed length values -     *   may give unexpected results. -     * @returns this -     */ -    seek(length) { -        const forward = (length >= 0); -        this._remainder = (forward ? length : -length); -        if (length === 0) { return this; } - -        const TEXT_NODE = Node.TEXT_NODE; -        const ELEMENT_NODE = Node.ELEMENT_NODE; - -        const generateLayoutContent = this._generateLayoutContent; -        let node = this._node; -        let lastNode = node; -        let resetOffset = this._resetOffset; -        let newlines = 0; -        while (node !== null) { -            let enterable = false; -            const nodeType = node.nodeType; - -            if (nodeType === TEXT_NODE) { -                lastNode = node; -                if (!( -                    forward ? -                    this._seekTextNodeForward(node, resetOffset) : -                    this._seekTextNodeBackward(node, resetOffset) -                )) { -                    // Length reached -                    break; -                } -            } else if (nodeType === ELEMENT_NODE) { -                lastNode = node; -                this._offset = 0; -                [enterable, newlines] = DOMTextScanner.getElementSeekInfo(node); -                if (newlines > this._newlines && generateLayoutContent) { -                    this._newlines = newlines; -                } -            } - -            const exitedNodes = []; -            node = DOMTextScanner.getNextNode(node, forward, enterable, exitedNodes); - -            for (const exitedNode of exitedNodes) { -                if (exitedNode.nodeType !== ELEMENT_NODE) { continue; } -                newlines = DOMTextScanner.getElementSeekInfo(exitedNode)[1]; -                if (newlines > this._newlines && generateLayoutContent) { -                    this._newlines = newlines; -                } -            } - -            resetOffset = true; -        } - -        this._node = lastNode; -        this._resetOffset = resetOffset; - -        return this; -    } - -    // Private - -    /** -     * Seeks forward in a text node. -     * @param textNode The text node to use. -     * @param resetOffset Whether or not the text offset should be reset. -     * @returns true if scanning should continue, or false if the scan length has been reached. -     */ -    _seekTextNodeForward(textNode, resetOffset) { -        const nodeValue = textNode.nodeValue; -        const nodeValueLength = nodeValue.length; -        const [preserveNewlines, preserveWhitespace] = ( -            this._forcePreserveWhitespace ? -            [true, true] : -            DOMTextScanner.getWhitespaceSettings(textNode) -        ); - -        let lineHasWhitespace = this._lineHasWhitespace; -        let lineHasContent = this._lineHasContent; -        let content = this._content; -        let offset = resetOffset ? 0 : this._offset; -        let remainder = this._remainder; -        let newlines = this._newlines; - -        while (offset < nodeValueLength) { -            const char = nodeValue[offset]; -            const charAttributes = DOMTextScanner.getCharacterAttributes(char, preserveNewlines, preserveWhitespace); -            ++offset; - -            if (charAttributes === 0) { -                // Character should be ignored -                continue; -            } else if (charAttributes === 1) { -                // Character is collapsable whitespace -                lineHasWhitespace = true; -            } else { -                // Character should be added to the content -                if (newlines > 0) { -                    if (content.length > 0) { -                        const useNewlineCount = Math.min(remainder, newlines); -                        content += '\n'.repeat(useNewlineCount); -                        remainder -= useNewlineCount; -                        newlines -= useNewlineCount; -                    } else { -                        newlines = 0; -                    } -                    lineHasContent = false; -                    lineHasWhitespace = false; -                    if (remainder <= 0) { -                        --offset; // Revert character offset -                        break; -                    } -                } - -                lineHasContent = (charAttributes === 2); // 3 = character is a newline - -                if (lineHasWhitespace) { -                    if (lineHasContent) { -                        content += ' '; -                        lineHasWhitespace = false; -                        if (--remainder <= 0) { -                            --offset; // Revert character offset -                            break; -                        } -                    } else { -                        lineHasWhitespace = false; -                    } -                } - -                content += char; - -                if (--remainder <= 0) { break; } -            } -        } - -        this._lineHasWhitespace = lineHasWhitespace; -        this._lineHasContent = lineHasContent; -        this._content = content; -        this._offset = offset; -        this._remainder = remainder; -        this._newlines = newlines; - -        return (remainder > 0); -    } - -    /** -     * Seeks backward in a text node. -     * This function is nearly the same as _seekTextNodeForward, with the following differences: -     * - Iteration condition is reversed to check if offset is greater than 0. -     * - offset is reset to nodeValueLength instead of 0. -     * - offset is decremented instead of incremented. -     * - offset is decremented before getting the character. -     * - offset is reverted by incrementing instead of decrementing. -     * - content string is prepended instead of appended. -     * @param textNode The text node to use. -     * @param resetOffset Whether or not the text offset should be reset. -     * @returns true if scanning should continue, or false if the scan length has been reached. -     */ -    _seekTextNodeBackward(textNode, resetOffset) { -        const nodeValue = textNode.nodeValue; -        const nodeValueLength = nodeValue.length; -        const [preserveNewlines, preserveWhitespace] = ( -            this._forcePreserveWhitespace ? -            [true, true] : -            DOMTextScanner.getWhitespaceSettings(textNode) -        ); - -        let lineHasWhitespace = this._lineHasWhitespace; -        let lineHasContent = this._lineHasContent; -        let content = this._content; -        let offset = resetOffset ? nodeValueLength : this._offset; -        let remainder = this._remainder; -        let newlines = this._newlines; - -        while (offset > 0) { -            --offset; -            const char = nodeValue[offset]; -            const charAttributes = DOMTextScanner.getCharacterAttributes(char, preserveNewlines, preserveWhitespace); - -            if (charAttributes === 0) { -                // Character should be ignored -                continue; -            } else if (charAttributes === 1) { -                // Character is collapsable whitespace -                lineHasWhitespace = true; -            } else { -                // Character should be added to the content -                if (newlines > 0) { -                    if (content.length > 0) { -                        const useNewlineCount = Math.min(remainder, newlines); -                        content = '\n'.repeat(useNewlineCount) + content; -                        remainder -= useNewlineCount; -                        newlines -= useNewlineCount; -                    } else { -                        newlines = 0; -                    } -                    lineHasContent = false; -                    lineHasWhitespace = false; -                    if (remainder <= 0) { -                        ++offset; // Revert character offset -                        break; -                    } -                } - -                lineHasContent = (charAttributes === 2); // 3 = character is a newline - -                if (lineHasWhitespace) { -                    if (lineHasContent) { -                        content = ' ' + content; -                        lineHasWhitespace = false; -                        if (--remainder <= 0) { -                            ++offset; // Revert character offset -                            break; -                        } -                    } else { -                        lineHasWhitespace = false; -                    } -                } - -                content = char + content; - -                if (--remainder <= 0) { break; } -            } -        } - -        this._lineHasWhitespace = lineHasWhitespace; -        this._lineHasContent = lineHasContent; -        this._content = content; -        this._offset = offset; -        this._remainder = remainder; -        this._newlines = newlines; - -        return (remainder > 0); -    } - -    // Static helpers - -    /** -     * Gets the next node in the document for a specified scanning direction. -     * @param node The current DOM Node. -     * @param forward Whether to scan forward in the document or backward. -     * @param visitChildren Whether the children of the current node should be visited. -     * @param exitedNodes An array which stores nodes which were exited. -     * @returns The next node in the document, or null if there is no next node. -     */ -    static getNextNode(node, forward, visitChildren, exitedNodes) { -        let next = visitChildren ? (forward ? node.firstChild : node.lastChild) : null; -        if (next === null) { -            while (true) { -                exitedNodes.push(node); - -                next = (forward ? node.nextSibling : node.previousSibling); -                if (next !== null) { break; } - -                next = node.parentNode; -                if (next === null) { break; } - -                node = next; -            } -        } -        return next; -    } - -    /** -     * Gets the parent element of a given Node. -     * @param node The node to check. -     * @returns The parent element if one exists, otherwise null. -     */ -    static getParentElement(node) { -        while (node !== null && node.nodeType !== Node.ELEMENT_NODE) { -            node = node.parentNode; -        } -        return node; -    } - -    /** -     * Gets the parent <ruby> element of a given node, if one exists. For efficiency purposes, -     * this only checks the immediate parent elements and does not check all ancestors, so -     * there are cases where the node may be in a ruby element but it is not returned. -     * @param node The node to check. -     * @returns A <ruby> node if the input node is contained in one, otherwise null. -     */ -    static getParentRubyElement(node) { -        node = DOMTextScanner.getParentElement(node); -        if (node !== null && node.nodeName.toUpperCase() === 'RT') { -            node = node.parentNode; -            if (node !== null && node.nodeName.toUpperCase() === 'RUBY') { -                return node; -            } -        } -        return null; -    } - -    /** -     * @returns [enterable: boolean, newlines: integer] -     *   The enterable value indicates whether the content of this node should be entered. -     *   The newlines value corresponds to the number of newline characters that should be added. -     *     1 newline corresponds to a simple new line in the layout. -     *     2 newlines corresponds to a significant visual distinction since the previous content. -     */ -    static getElementSeekInfo(element) { -        let enterable = true; -        switch (element.nodeName.toUpperCase()) { -            case 'HEAD': -            case 'RT': -            case 'SCRIPT': -            case 'STYLE': -                return [false, 0]; -            case 'BR': -                return [false, 1]; -            case 'TEXTAREA': -            case 'INPUT': -            case 'BUTTON': -                enterable = false; -                break; -        } - -        const style = window.getComputedStyle(element); -        const display = style.display; - -        const visible = (display !== 'none' && DOMTextScanner.isStyleVisible(style)); -        let newlines = 0; - -        if (!visible) { -            enterable = false; -        } else { -            switch (style.position) { -                case 'absolute': -                case 'fixed': -                case 'sticky': -                    newlines = 2; -                    break; -            } -            if (newlines === 0 && DOMTextScanner.doesCSSDisplayChangeLayout(display)) { -                newlines = 1; -            } -        } - -        return [enterable, newlines]; -    } - -    /** -     * Gets information about how whitespace characters are treated. -     * @param textNode The Text node to check. -     * @returns [preserveNewlines: boolean, preserveWhitespace: boolean] -     *   The value of preserveNewlines indicates whether or not newline characters are treated as line breaks. -     *   The value of preserveWhitespace indicates whether or not sequences of whitespace characters are collapsed. -     */ -    static getWhitespaceSettings(textNode) { -        const element = DOMTextScanner.getParentElement(textNode); -        if (element !== null) { -            const style = window.getComputedStyle(element); -            switch (style.whiteSpace) { -                case 'pre': -                case 'pre-wrap': -                case 'break-spaces': -                    return [true, true]; -                case 'pre-line': -                    return [true, false]; -            } -        } -        return [false, false]; -    } - -    /** -     * Gets attributes for the specified character. -     * @param character A string containing a single character. -     * @returns An integer representing the attributes of the character. -     *   0: Character should be ignored. -     *   1: Character is collapsable whitespace. -     *   2: Character should be added to the content. -     *   3: Character should be added to the content and is a newline. -     */ -    static getCharacterAttributes(character, preserveNewlines, preserveWhitespace) { -        switch (character.charCodeAt(0)) { -            case 0x09: // Tab ('\t') -            case 0x0c: // Form feed ('\f') -            case 0x0d: // Carriage return ('\r') -            case 0x20: // Space (' ') -                return preserveWhitespace ? 2 : 1; -            case 0x0a: // Line feed ('\n') -                return preserveNewlines ? 3 : 1; -            case 0x200c: // Zero-width non-joiner ('\u200c') -                return 0; -            default: // Other -                return 2; -        } -    } - -    /** -     * Checks whether a given style is visible or not. -     * This function does not check style.display === 'none'. -     * @param style An object implementing the CSSStyleDeclaration interface. -     * @returns true if the style should result in an element being visible, otherwise false. -     */ -    static isStyleVisible(style) { -        return !( -            style.visibility === 'hidden' || -            parseFloat(style.opacity) <= 0 || -            parseFloat(style.fontSize) <= 0 || -            ( -                !DOMTextScanner.isStyleSelectable(style) && -                ( -                    DOMTextScanner.isCSSColorTransparent(style.color) || -                    DOMTextScanner.isCSSColorTransparent(style.webkitTextFillColor) -                ) -            ) -        ); -    } - -    /** -     * Checks whether a given style is selectable or not. -     * @param style An object implementing the CSSStyleDeclaration interface. -     * @returns true if the style is selectable, otherwise false. -     */ -    static isStyleSelectable(style) { -        return !( -            style.userSelect === 'none' || -            style.webkitUserSelect === 'none' || -            style.MozUserSelect === 'none' || -            style.msUserSelect === 'none' -        ); -    } - -    /** -     * Checks whether a CSS color is transparent or not. -     * @param cssColor A CSS color string, expected to be encoded in rgb(a) form. -     * @returns true if the color is transparent, otherwise false. -     */ -    static isCSSColorTransparent(cssColor) { -        return ( -            typeof cssColor === 'string' && -            cssColor.startsWith('rgba(') && -            /,\s*0.?0*\)$/.test(cssColor) -        ); -    } - -    /** -     * Checks whether a CSS display value will cause a layout change for text. -     * @param cssDisplay A CSS string corresponding to the value of the display property. -     * @returns true if the layout is changed by this value, otherwise false. -     */ -    static doesCSSDisplayChangeLayout(cssDisplay) { -        let pos = cssDisplay.indexOf(' '); -        if (pos >= 0) { -            // Truncate to <display-outside> part -            cssDisplay = cssDisplay.substring(0, pos); -        } - -        pos = cssDisplay.indexOf('-'); -        if (pos >= 0) { -            // Truncate to first part of kebab-case value -            cssDisplay = cssDisplay.substring(0, pos); -        } - -        switch (cssDisplay) { -            case 'block': -            case 'flex': -            case 'grid': -            case 'list': // list-item -            case 'table': // table, table-* -                return true; -            case 'ruby': // rubt-* -                return (pos >= 0); -            default: -                return false; -        } -    } -} diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js deleted file mode 100644 index 7c048b62..00000000 --- a/ext/fg/js/float-main.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2020-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Display - * DisplayProfileSelection - * DocumentFocusController - * HotkeyHandler - * JapaneseUtil - * api - */ - -(async () => { -    try { -        const documentFocusController = new DocumentFocusController(); -        documentFocusController.prepare(); - -        api.forwardLogsToBackend(); -        await yomichan.backendReady(); - -        const {tabId, frameId} = await api.frameInformationGet(); - -        const japaneseUtil = new JapaneseUtil(null); - -        const hotkeyHandler = new HotkeyHandler(); -        hotkeyHandler.prepare(); - -        const display = new Display(tabId, frameId, 'popup', japaneseUtil, documentFocusController, hotkeyHandler); -        await display.prepare(); - -        const displayProfileSelection = new DisplayProfileSelection(display); -        displayProfileSelection.prepare(); - -        display.initializeState(); - -        document.documentElement.dataset.loaded = 'true'; - -        yomichan.ready(); -    } catch (e) { -        yomichan.logError(e); -    } -})(); diff --git a/ext/fg/js/frame-ancestry-handler.js b/ext/fg/js/frame-ancestry-handler.js deleted file mode 100644 index b1ed7114..00000000 --- a/ext/fg/js/frame-ancestry-handler.js +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright (C) 2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * api - */ - -/** - * This class is used to return the ancestor frame IDs for the current frame. - * This is a workaround to using the `webNavigation.getAllFrames` API, which - * would require an additional permission that is otherwise unnecessary. - * It is also used to track the correlation between child frame elements and their IDs. - */ -class FrameAncestryHandler { -    /** -     * Creates a new instance. -     * @param frameId The frame ID of the current frame the instance is instantiated in. -     */ -    constructor(frameId) { -        this._frameId = frameId; -        this._isPrepared = false; -        this._requestMessageId = 'FrameAncestryHandler.requestFrameInfo'; -        this._responseMessageIdBase = `${this._requestMessageId}.response.`; -        this._getFrameAncestryInfoPromise = null; -        this._childFrameMap = new Map(); -    } - -    /** -     * Gets the frame ID that the instance is instantiated in. -     */ -    get frameId() { -        return this._frameId; -    } - -    /** -     * Initializes event event listening. -     */ -    prepare() { -        if (this._isPrepared) { return; } -        window.addEventListener('message', this._onWindowMessage.bind(this), false); -        this._isPrepared = true; -    } - -    /** -     * Returns whether or not this frame is the root frame in the tab. -     * @returns `true` if it is the root, otherwise `false`. -     */ -    isRootFrame() { -        return (window === window.parent); -    } - -    /** -     * Gets the frame ancestry information for the current frame. If the frame is the -     * root frame, an empty array is returned. Otherwise, an array of frame IDs is returned, -     * starting from the nearest ancestor. -     * @param timeout The maximum time to wait to receive a response to frame information requests. -     * @returns An array of frame IDs corresponding to the ancestors of the current frame. -     */ -    async getFrameAncestryInfo() { -        if (this._getFrameAncestryInfoPromise === null) { -            this._getFrameAncestryInfoPromise = this._getFrameAncestryInfo(5000); -        } -        return await this._getFrameAncestryInfoPromise; -    } - -    /** -     * Gets the frame element of a child frame given a frame ID. -     * For this function to work, the `getFrameAncestryInfo` function needs to have -     * been invoked previously. -     * @param frameId The frame ID of the child frame to get. -     * @returns The element corresponding to the frame with ID `frameId`, otherwise `null`. -     */ -    getChildFrameElement(frameId) { -        const frameInfo = this._childFrameMap.get(frameId); -        if (typeof frameInfo === 'undefined') { return null; } - -        let {frameElement} = frameInfo; -        if (typeof frameElement === 'undefined') { -            frameElement = this._findFrameElementWithContentWindow(frameInfo.window); -            frameInfo.frameElement = frameElement; -        } - -        return frameElement; -    } - -    // Private - -    _getFrameAncestryInfo(timeout=5000) { -        return new Promise((resolve, reject) => { -            const targetWindow = window.parent; -            if (window === targetWindow) { -                resolve([]); -                return; -            } - -            const uniqueId = generateId(16); -            let nonce = generateId(16); -            const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; -            const results = []; -            let timer = null; - -            const cleanup = () => { -                if (timer !== null) { -                    clearTimeout(timer); -                    timer = null; -                } -                api.crossFrame.unregisterHandler(responseMessageId); -            }; -            const onMessage = (params) => { -                if (params.nonce !== nonce) { return null; } - -                // Add result -                const {frameId, more} = params; -                results.push(frameId); -                nonce = generateId(16); - -                if (!more) { -                    // Cleanup -                    cleanup(); - -                    // Finish -                    resolve(results); -                } -                return {nonce}; -            }; -            const onTimeout = () => { -                timer = null; -                cleanup(); -                reject(new Error(`Request for parent frame ID timed out after ${timeout}ms`)); -            }; -            const resetTimeout = () => { -                if (timer !== null) { clearTimeout(timer); } -                timer = setTimeout(onTimeout, timeout); -            }; - -            // Start -            api.crossFrame.registerHandlers([[responseMessageId, {async: false, handler: onMessage}]]); -            resetTimeout(); -            const frameId = this._frameId; -            this._requestFrameInfo(targetWindow, frameId, frameId, uniqueId, nonce); -        }); -    } - -    _onWindowMessage(event) { -        const {source} = event; -        if (source === window || source.parent !== window) { return; } - -        const {data} = event; -        if ( -            typeof data === 'object' && -            data !== null && -            data.action === this._requestMessageId -        ) { -            this._onRequestFrameInfo(data.params, source); -        } -    } - -    async _onRequestFrameInfo(params, source) { -        try { -            let {originFrameId, childFrameId, uniqueId, nonce} = params; -            if ( -                !this._isNonNegativeInteger(originFrameId) || -                typeof uniqueId !== 'string' || -                typeof nonce !== 'string' -            ) { -                return; -            } - -            const frameId = this._frameId; -            const {parent} = window; -            const more = (window !== parent); -            const responseParams = {frameId, nonce, more}; -            const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; - -            try { -                const response = await api.crossFrame.invoke(originFrameId, responseMessageId, responseParams); -                if (response === null) { return; } -                nonce = response.nonce; -            } catch (e) { -                return; -            } - -            if (!this._childFrameMap.has(childFrameId)) { -                this._childFrameMap.set(childFrameId, {window: source, frameElement: void 0}); -            } - -            if (more) { -                this._requestFrameInfo(parent, originFrameId, frameId, uniqueId, nonce); -            } -        } catch (e) { -            // NOP -        } -    } - -    _requestFrameInfo(targetWindow, originFrameId, childFrameId, uniqueId, nonce) { -        targetWindow.postMessage({ -            action: this._requestMessageId, -            params: {originFrameId, childFrameId, uniqueId, nonce} -        }, '*'); -    } - -    _isNonNegativeInteger(value) { -        return ( -            typeof value === 'number' && -            Number.isFinite(value) && -            value >= 0 && -            Math.floor(value) === value -        ); -    } - -    _findFrameElementWithContentWindow(contentWindow) { -        // Check frameElement, for non-null same-origin frames -        try { -            const {frameElement} = contentWindow; -            if (frameElement !== null) { return frameElement; } -        } catch (e) { -            // NOP -        } - -        // Check frames -        const frameTypes = ['iframe', 'frame', 'embed']; -        for (const frameType of frameTypes) { -            for (const frame of document.getElementsByTagName(frameType)) { -                if (frame.contentWindow === contentWindow) { -                    return frame; -                } -            } -        } - -        // Check for shadow roots -        const rootElements = [document.documentElement]; -        while (rootElements.length > 0) { -            const rootElement = rootElements.shift(); -            const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT); -            while (walker.nextNode()) { -                const element = walker.currentNode; - -                if (element.contentWindow === contentWindow) { -                    return element; -                } - -                const shadowRoot = ( -                    element.shadowRoot || -                    element.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions -                ); -                if (shadowRoot) { -                    rootElements.push(shadowRoot); -                } -            } -        } - -        // Not found -        return null; -    } -} diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js deleted file mode 100644 index 0a0b4a18..00000000 --- a/ext/fg/js/frame-offset-forwarder.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2020-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * FrameAncestryHandler - * api - */ - -class FrameOffsetForwarder { -    constructor(frameId) { -        this._frameId = frameId; -        this._frameAncestryHandler = new FrameAncestryHandler(frameId); -    } - -    prepare() { -        this._frameAncestryHandler.prepare(); -        api.crossFrame.registerHandlers([ -            ['FrameOffsetForwarder.getChildFrameRect', {async: false, handler: this._onMessageGetChildFrameRect.bind(this)}] -        ]); -    } - -    async getOffset() { -        if (this._frameAncestryHandler.isRootFrame()) { -            return [0, 0]; -        } - -        const ancestorFrameIds = await this._frameAncestryHandler.getFrameAncestryInfo(); - -        let childFrameId = this._frameId; -        const promises = []; -        for (const frameId of ancestorFrameIds) { -            promises.push(api.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId})); -            childFrameId = frameId; -        } - -        const results = await Promise.all(promises); - -        let xOffset = 0; -        let yOffset = 0; -        for (const {x, y} of results) { -            xOffset += x; -            yOffset += y; -        } -        return [xOffset, yOffset]; -    } - -    // Private - -    _onMessageGetChildFrameRect({frameId}) { -        const frameElement = this._frameAncestryHandler.getChildFrameElement(frameId); -        if (frameElement === null) { return null; } - -        const {x, y, width, height} = frameElement.getBoundingClientRect(); -        return {x, y, width, height}; -    } -} diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js deleted file mode 100644 index a62b06bf..00000000 --- a/ext/fg/js/frontend.js +++ /dev/null @@ -1,691 +0,0 @@ -/* - * Copyright (C) 2016-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * DocumentUtil - * TextScanner - * TextSourceElement - * TextSourceRange - * api - */ - -class Frontend { -    constructor({ -        pageType, -        popupFactory, -        depth, -        tabId, -        frameId, -        parentPopupId, -        parentFrameId, -        useProxyPopup, -        canUseWindowPopup=true, -        allowRootFramePopupProxy, -        childrenSupported=true, -        hotkeyHandler -    }) { -        this._pageType = pageType; -        this._popupFactory = popupFactory; -        this._depth = depth; -        this._tabId = tabId; -        this._frameId = frameId; -        this._parentPopupId = parentPopupId; -        this._parentFrameId = parentFrameId; -        this._useProxyPopup = useProxyPopup; -        this._canUseWindowPopup = canUseWindowPopup; -        this._allowRootFramePopupProxy = allowRootFramePopupProxy; -        this._childrenSupported = childrenSupported; -        this._hotkeyHandler = hotkeyHandler; -        this._popup = null; -        this._disabledOverride = false; -        this._options = null; -        this._pageZoomFactor = 1.0; -        this._contentScale = 1.0; -        this._lastShowPromise = Promise.resolve(); -        this._documentUtil = new DocumentUtil(); -        this._textScanner = new TextScanner({ -            node: window, -            ignoreElements: this._ignoreElements.bind(this), -            ignorePoint: this._ignorePoint.bind(this), -            getSearchContext: this._getSearchContext.bind(this), -            documentUtil: this._documentUtil, -            searchTerms: true, -            searchKanji: true -        }); -        this._popupCache = new Map(); -        this._popupEventListeners = new EventListenerCollection(); -        this._updatePopupToken = null; -        this._clearSelectionTimer = null; -        this._isPointerOverPopup = false; -        this._optionsContextOverride = null; - -        this._runtimeMessageHandlers = new Map([ -            ['requestFrontendReadyBroadcast', {async: false, handler: this._onMessageRequestFrontendReadyBroadcast.bind(this)}], -            ['setAllVisibleOverride',         {async: true,  handler: this._onApiSetAllVisibleOverride.bind(this)}], -            ['clearAllVisibleOverride',       {async: true,  handler: this._onApiClearAllVisibleOverride.bind(this)}] -        ]); - -        this._hotkeyHandler.registerActions([ -            ['scanSelectedText', this._onActionScanSelectedText.bind(this)] -        ]); -    } - -    get canClearSelection() { -        return this._textScanner.canClearSelection; -    } - -    set canClearSelection(value) { -        this._textScanner.canClearSelection = value; -    } - -    get popup() { -        return this._popup; -    } - -    async prepare() { -        await this.updateOptions(); -        try { -            const {zoomFactor} = await api.getZoom(); -            this._pageZoomFactor = zoomFactor; -        } catch (e) { -            // Ignore exceptions which may occur due to being on an unsupported page (e.g. about:blank) -        } - -        this._textScanner.prepare(); - -        window.addEventListener('resize', this._onResize.bind(this), false); -        DocumentUtil.addFullscreenChangeEventListener(this._updatePopup.bind(this)); - -        const visualViewport = window.visualViewport; -        if (visualViewport !== null && typeof visualViewport === 'object') { -            visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this)); -            visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this)); -        } - -        yomichan.on('optionsUpdated', this.updateOptions.bind(this)); -        yomichan.on('zoomChanged', this._onZoomChanged.bind(this)); -        yomichan.on('closePopups', this._onClosePopups.bind(this)); -        chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this)); - -        this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); -        this._textScanner.on('searched', this._onSearched.bind(this)); - -        api.crossFrame.registerHandlers([ -            ['closePopup',              {async: false, handler: this._onApiClosePopup.bind(this)}], -            ['copySelection',           {async: false, handler: this._onApiCopySelection.bind(this)}], -            ['getSelectionText',        {async: false, handler: this._onApiGetSelectionText.bind(this)}], -            ['getPopupInfo',            {async: false, handler: this._onApiGetPopupInfo.bind(this)}], -            ['getPageInfo',             {async: false, handler: this._onApiGetPageInfo.bind(this)}], -            ['getFrameSize',            {async: true,  handler: this._onApiGetFrameSize.bind(this)}], -            ['setFrameSize',            {async: true,  handler: this._onApiSetFrameSize.bind(this)}] -        ]); - -        this._updateContentScale(); -        this._signalFrontendReady(); -    } - -    setDisabledOverride(disabled) { -        this._disabledOverride = disabled; -        this._updateTextScannerEnabled(); -    } - -    setOptionsContextOverride(optionsContext) { -        this._optionsContextOverride = optionsContext; -    } - -    async setTextSource(textSource) { -        this._textScanner.setCurrentTextSource(null); -        await this._textScanner.search(textSource); -    } - -    async updateOptions() { -        try { -            await this._updateOptionsInternal(); -        } catch (e) { -            if (!yomichan.isExtensionUnloaded) { -                throw e; -            } -        } -    } - -    showContentCompleted() { -        return this._lastShowPromise; -    } - -    // Message handlers - -    _onMessageRequestFrontendReadyBroadcast({frameId}) { -        this._signalFrontendReady(frameId); -    } - -    // Action handlers - -    _onActionScanSelectedText() { -        this._scanSelectedText(); -    } - -    // API message handlers - -    _onApiGetUrl() { -        return window.location.href; -    } - -    _onApiClosePopup() { -        this._clearSelection(false); -    } - -    _onApiCopySelection() { -        // This will not work on Firefox if a popup has focus, which is usually the case when this function is called. -        document.execCommand('copy'); -    } - -    _onApiGetSelectionText() { -        return document.getSelection().toString(); -    } - -    _onApiGetPopupInfo() { -        return { -            popupId: (this._popup !== null ? this._popup.id : null) -        }; -    } - -    _onApiGetPageInfo() { -        return { -            url: window.location.href, -            documentTitle: document.title -        }; -    } - -    async _onApiSetAllVisibleOverride({value, priority, awaitFrame}) { -        const result = await this._popupFactory.setAllVisibleOverride(value, priority); -        if (awaitFrame) { -            await promiseAnimationFrame(100); -        } -        return result; -    } - -    async _onApiClearAllVisibleOverride({token}) { -        return await this._popupFactory.clearAllVisibleOverride(token); -    } - -    async _onApiGetFrameSize() { -        return await this._popup.getFrameSize(); -    } - -    async _onApiSetFrameSize({width, height}) { -        return await this._popup.setFrameSize(width, height); -    } - -    // Private - -    _onResize() { -        this._updatePopupPosition(); -    } - -    _onRuntimeMessage({action, params}, sender, callback) { -        const messageHandler = this._runtimeMessageHandlers.get(action); -        if (typeof messageHandler === 'undefined') { return false; } -        return yomichan.invokeMessageHandler(messageHandler, params, callback, sender); -    } - -    _onZoomChanged({newZoomFactor}) { -        this._pageZoomFactor = newZoomFactor; -        this._updateContentScale(); -    } - -    _onClosePopups() { -        this._clearSelection(true); -    } - -    _onVisualViewportScroll() { -        this._updatePopupPosition(); -    } - -    _onVisualViewportResize() { -        this._updateContentScale(); -    } - -    _onClearSelection({passive}) { -        this._stopClearSelectionDelayed(); -        if (this._popup !== null) { -            this._popup.hide(!passive); -            this._popup.clearAutoPlayTimer(); -            this._isPointerOverPopup = false; -        } -    } - -    _onSearched({type, definitions, sentence, inputInfo: {eventType, passive, detail}, textSource, optionsContext, detail: {documentTitle}, error}) { -        const scanningOptions = this._options.scanning; - -        if (error !== null) { -            if (yomichan.isExtensionUnloaded) { -                if (textSource !== null && !passive) { -                    this._showExtensionUnloaded(textSource); -                } -            } else { -                yomichan.logError(error); -            } -        } if (type !== null) { -            this._stopClearSelectionDelayed(); -            let focus = (eventType === 'mouseMove'); -            if (isObject(detail)) { -                const focus2 = detail.focus; -                if (typeof focus2 === 'boolean') { focus = focus2; } -            } -            this._showContent(textSource, focus, definitions, type, sentence, documentTitle, optionsContext); -        } else { -            if (scanningOptions.autoHideResults) { -                this._clearSelectionDelayed(scanningOptions.hideDelay, false); -            } -        } -    } - -    _onPopupFramePointerOver() { -        this._isPointerOverPopup = true; -        this._stopClearSelectionDelayed(); -    } - -    _onPopupFramePointerOut() { -        this._isPointerOverPopup = false; -    } - -    _clearSelection(passive) { -        this._stopClearSelectionDelayed(); -        this._textScanner.clearSelection(passive); -    } - -    _clearSelectionDelayed(delay, restart, passive) { -        if (!this._textScanner.hasSelection()) { return; } -        if (delay > 0) { -            if (this._clearSelectionTimer !== null && !restart) { return; } // Already running -            this._stopClearSelectionDelayed(); -            this._clearSelectionTimer = setTimeout(() => { -                this._clearSelectionTimer = null; -                if (this._isPointerOverPopup) { return; } -                this._clearSelection(passive); -            }, delay); -        } else { -            this._clearSelection(passive); -        } -    } - -    _stopClearSelectionDelayed() { -        if (this._clearSelectionTimer !== null) { -            clearTimeout(this._clearSelectionTimer); -            this._clearSelectionTimer = null; -        } -    } - -    async _updateOptionsInternal() { -        const optionsContext = await this._getOptionsContext(); -        const options = await api.optionsGet(optionsContext); -        const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options; -        this._options = options; - -        this._hotkeyHandler.setHotkeys('web', options.inputs.hotkeys); - -        await this._updatePopup(); - -        const preventMiddleMouse = this._getPreventMiddleMouseValueForPageType(scanningOptions.preventMiddleMouse); -        this._textScanner.setOptions({ -            inputs: scanningOptions.inputs, -            deepContentScan: scanningOptions.deepDomScan, -            selectText: scanningOptions.selectText, -            delay: scanningOptions.delay, -            touchInputEnabled: scanningOptions.touchInputEnabled, -            pointerEventsEnabled: scanningOptions.pointerEventsEnabled, -            scanLength: scanningOptions.length, -            layoutAwareScan: scanningOptions.layoutAwareScan, -            preventMiddleMouse, -            sentenceParsingOptions -        }); -        this._updateTextScannerEnabled(); - -        if (this._pageType !== 'web') { -            const excludeSelectors = ['.scan-disable', '.scan-disable *']; -            if (!scanningOptions.enableOnPopupExpressions) { -                excludeSelectors.push('.source-text', '.source-text *'); -            } -            this._textScanner.excludeSelector = excludeSelectors.join(','); -        } - -        this._updateContentScale(); - -        await this._textScanner.searchLast(); -    } - -    async _updatePopup() { -        const {usePopupWindow, showIframePopupsInRootFrame} = this._options.general; -        const isIframe = !this._useProxyPopup && (window !== window.parent); - -        const currentPopup = this._popup; - -        let popupPromise; -        if (usePopupWindow && this._canUseWindowPopup) { -            popupPromise = this._popupCache.get('window'); -            if (typeof popupPromise === 'undefined') { -                popupPromise = this._getPopupWindow(); -                this._popupCache.set('window', popupPromise); -            } -        } else if ( -            isIframe && -            showIframePopupsInRootFrame && -            DocumentUtil.getFullscreenElement() === null && -            this._allowRootFramePopupProxy -        ) { -            popupPromise = this._popupCache.get('iframe'); -            if (typeof popupPromise === 'undefined') { -                popupPromise = this._getIframeProxyPopup(); -                this._popupCache.set('iframe', popupPromise); -            } -        } else if (this._useProxyPopup) { -            popupPromise = this._popupCache.get('proxy'); -            if (typeof popupPromise === 'undefined') { -                popupPromise = this._getProxyPopup(); -                this._popupCache.set('proxy', popupPromise); -            } -        } else { -            popupPromise = this._popupCache.get('default'); -            if (typeof popupPromise === 'undefined') { -                popupPromise = this._getDefaultPopup(); -                this._popupCache.set('default', popupPromise); -            } -        } - -        // The token below is used as a unique identifier to ensure that a new _updatePopup call -        // hasn't been started during the await. -        const token = {}; -        this._updatePopupToken = token; -        const popup = await popupPromise; -        const optionsContext = await this._getOptionsContext(); -        if (this._updatePopupToken !== token) { return; } -        if (popup !== null) { -            await popup.setOptionsContext(optionsContext); -        } -        if (this._updatePopupToken !== token) { return; } - -        if (popup !== currentPopup) { -            this._clearSelection(true); -        } - -        this._popupEventListeners.removeAllEventListeners(); -        this._popup = popup; -        if (popup !== null) { -            this._popupEventListeners.on(popup, 'framePointerOver', this._onPopupFramePointerOver.bind(this)); -            this._popupEventListeners.on(popup, 'framePointerOut', this._onPopupFramePointerOut.bind(this)); -        } -        this._isPointerOverPopup = false; -    } - -    async _getDefaultPopup() { -        const isXmlDocument = (typeof XMLDocument !== 'undefined' && document instanceof XMLDocument); -        if (isXmlDocument) { -            return null; -        } - -        return await this._popupFactory.getOrCreatePopup({ -            frameId: this._frameId, -            depth: this._depth, -            childrenSupported: this._childrenSupported -        }); -    } - -    async _getProxyPopup() { -        return await this._popupFactory.getOrCreatePopup({ -            frameId: this._parentFrameId, -            depth: this._depth, -            parentPopupId: this._parentPopupId, -            childrenSupported: this._childrenSupported -        }); -    } - -    async _getIframeProxyPopup() { -        const targetFrameId = 0; // Root frameId -        try { -            await this._waitForFrontendReady(targetFrameId); -        } catch (e) { -            // Root frame not available -            return await this._getDefaultPopup(); -        } - -        const {popupId} = await api.crossFrame.invoke(targetFrameId, 'getPopupInfo'); -        if (popupId === null) { -            return null; -        } - -        const popup = await this._popupFactory.getOrCreatePopup({ -            frameId: targetFrameId, -            id: popupId, -            childrenSupported: this._childrenSupported -        }); -        popup.on('offsetNotFound', () => { -            this._allowRootFramePopupProxy = false; -            this._updatePopup(); -        }); -        return popup; -    } - -    async _getPopupWindow() { -        return await this._popupFactory.getOrCreatePopup({ -            depth: this._depth, -            popupWindow: true, -            childrenSupported: this._childrenSupported -        }); -    } - -    _ignoreElements() { -        if (this._popup !== null) { -            const container = this._popup.container; -            if (container !== null) { -                return [container]; -            } -        } -        return []; -    } - -    async _ignorePoint(x, y) { -        try { -            return this._popup !== null && await this._popup.containsPoint(x, y); -        } catch (e) { -            if (!yomichan.isExtensionUnloaded) { -                throw e; -            } -            return false; -        } -    } - -    _showExtensionUnloaded(textSource) { -        if (textSource === null) { -            textSource = this._textScanner.getCurrentTextSource(); -            if (textSource === null) { return; } -        } -        this._showPopupContent(textSource, null); -    } - -    _showContent(textSource, focus, definitions, type, sentence, documentTitle, optionsContext) { -        const query = textSource.text(); -        const {url} = optionsContext; -        const details = { -            focus, -            history: false, -            params: { -                type, -                query, -                wildcards: 'off' -            }, -            state: { -                focusEntry: 0, -                optionsContext, -                url, -                sentence, -                documentTitle -            }, -            content: { -                definitions, -                contentOrigin: { -                    tabId: this._tabId, -                    frameId: this._frameId -                } -            } -        }; -        if (textSource instanceof TextSourceElement && textSource.fullContent !== query) { -            details.params.full = textSource.fullContent; -            details.params['full-visible'] = 'true'; -        } -        this._showPopupContent(textSource, optionsContext, details); -    } - -    _showPopupContent(textSource, optionsContext, details=null) { -        this._lastShowPromise = ( -            this._popup !== null ? -            this._popup.showContent( -                { -                    optionsContext, -                    elementRect: textSource.getRect(), -                    writingMode: textSource.getWritingMode() -                }, -                details -            ) : -            Promise.resolve() -        ); -        this._lastShowPromise.catch((error) => { -            if (yomichan.isExtensionUnloaded) { return; } -            yomichan.logError(error); -        }); -        return this._lastShowPromise; -    } - -    _updateTextScannerEnabled() { -        const enabled = (this._options !== null && this._options.general.enable && !this._disabledOverride); -        this._textScanner.setEnabled(enabled); -    } - -    _updateContentScale() { -        const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this._options.general; -        let contentScale = popupScalingFactor; -        if (popupScaleRelativeToPageZoom) { -            contentScale /= this._pageZoomFactor; -        } -        if (popupScaleRelativeToVisualViewport) { -            const visualViewport = window.visualViewport; -            const visualViewportScale = (visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0); -            contentScale /= visualViewportScale; -        } -        if (contentScale === this._contentScale) { return; } - -        this._contentScale = contentScale; -        if (this._popup !== null) { -            this._popup.setContentScale(this._contentScale); -        } -        this._updatePopupPosition(); -    } - -    async _updatePopupPosition() { -        const textSource = this._textScanner.getCurrentTextSource(); -        if ( -            textSource !== null && -            this._popup !== null && -            await this._popup.isVisible() -        ) { -            this._showPopupContent(textSource, null); -        } -    } - -    _signalFrontendReady(targetFrameId=null) { -        const params = {frameId: this._frameId}; -        if (targetFrameId === null) { -            api.broadcastTab('frontendReady', params); -        } else { -            api.sendMessageToFrame(targetFrameId, 'frontendReady', params); -        } -    } - -    async _waitForFrontendReady(frameId) { -        const promise = yomichan.getTemporaryListenerResult( -            chrome.runtime.onMessage, -            ({action, params}, {resolve}) => { -                if ( -                    action === 'frontendReady' && -                    params.frameId === frameId -                ) { -                    resolve(); -                } -            }, -            10000 -        ); -        api.broadcastTab('requestFrontendReadyBroadcast', {frameId: this._frameId}); -        await promise; -    } - -    _getPreventMiddleMouseValueForPageType(preventMiddleMouseOptions) { -        switch (this._pageType) { -            case 'web': return preventMiddleMouseOptions.onWebPages; -            case 'popup': return preventMiddleMouseOptions.onPopupPages; -            case 'search': return preventMiddleMouseOptions.onSearchPages; -            default: return false; -        } -    } - -    async _getOptionsContext() { -        let optionsContext = this._optionsContextOverride; -        if (optionsContext === null) { -            optionsContext = (await this._getSearchContext()).optionsContext; -        } -        return optionsContext; -    } - -    async _getSearchContext() { -        let url = window.location.href; -        let documentTitle = document.title; -        if (this._useProxyPopup) { -            try { -                ({url, documentTitle} = await api.crossFrame.invoke(this._parentFrameId, 'getPageInfo', {})); -            } catch (e) { -                // NOP -            } -        } - -        let optionsContext = this._optionsContextOverride; -        if (optionsContext === null) { -            optionsContext = {depth: this._depth, url}; -        } - -        return { -            optionsContext, -            detail: {documentTitle} -        }; -    } - -    async _scanSelectedText() { -        const range = this._getFirstNonEmptySelectionRange(); -        if (range === null) { return false; } -        const source = new TextSourceRange(range, range.toString(), null, null); -        await this._textScanner.search(source, {focus: true}); -        return true; -    } - -    _getFirstNonEmptySelectionRange() { -        const selection = window.getSelection(); -        for (let i = 0, ii = selection.rangeCount; i < ii; ++i) { -            const range = selection.getRangeAt(i); -            if (range.toString().length > 0) { -                return range; -            } -        } -        return null; -    } -} diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js deleted file mode 100644 index 7571d7ab..00000000 --- a/ext/fg/js/popup-factory.js +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2019-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * FrameOffsetForwarder - * Popup - * PopupProxy - * PopupWindow - * api - */ - -class PopupFactory { -    constructor(frameId) { -        this._frameId = frameId; -        this._frameOffsetForwarder = new FrameOffsetForwarder(frameId); -        this._popups = new Map(); -        this._allPopupVisibilityTokenMap = new Map(); -    } - -    // Public functions - -    prepare() { -        this._frameOffsetForwarder.prepare(); -        api.crossFrame.registerHandlers([ -            ['getOrCreatePopup',     {async: true,  handler: this._onApiGetOrCreatePopup.bind(this)}], -            ['setOptionsContext',    {async: true,  handler: this._onApiSetOptionsContext.bind(this)}], -            ['hide',                 {async: false, handler: this._onApiHide.bind(this)}], -            ['isVisible',            {async: true,  handler: this._onApiIsVisibleAsync.bind(this)}], -            ['setVisibleOverride',   {async: true,  handler: this._onApiSetVisibleOverride.bind(this)}], -            ['clearVisibleOverride', {async: true,  handler: this._onApiClearVisibleOverride.bind(this)}], -            ['containsPoint',        {async: true,  handler: this._onApiContainsPoint.bind(this)}], -            ['showContent',          {async: true,  handler: this._onApiShowContent.bind(this)}], -            ['setCustomCss',         {async: false, handler: this._onApiSetCustomCss.bind(this)}], -            ['clearAutoPlayTimer',   {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}], -            ['setContentScale',      {async: false, handler: this._onApiSetContentScale.bind(this)}], -            ['updateTheme',          {async: false, handler: this._onApiUpdateTheme.bind(this)}], -            ['setCustomOuterCss',    {async: false, handler: this._onApiSetCustomOuterCss.bind(this)}], -            ['popup.getFrameSize',   {async: true,  handler: this._onApiGetFrameSize.bind(this)}], -            ['popup.setFrameSize',   {async: true,  handler: this._onApiSetFrameSize.bind(this)}] -        ]); -    } - -    async getOrCreatePopup({ -        frameId=null, -        id=null, -        parentPopupId=null, -        depth=null, -        popupWindow=false, -        childrenSupported=false -    }) { -        // Find by existing id -        if (id !== null) { -            const popup = this._popups.get(id); -            if (typeof popup !== 'undefined') { -                return popup; -            } -        } - -        // Find by existing parent id -        let parent = null; -        if (parentPopupId !== null) { -            parent = this._popups.get(parentPopupId); -            if (typeof parent !== 'undefined') { -                const popup = parent.child; -                if (popup !== null) { -                    return popup; -                } -            } else { -                parent = null; -            } -        } - -        // Depth -        if (parent !== null) { -            if (depth !== null) { -                throw new Error('Depth cannot be set when parent exists'); -            } -            depth = parent.depth + 1; -        } else if (depth === null) { -            depth = 0; -        } - -        if (popupWindow) { -            // New unique id -            if (id === null) { -                id = generateId(16); -            } -            const popup = new PopupWindow({ -                id, -                depth, -                frameId: this._frameId -            }); -            this._popups.set(id, popup); -            return popup; -        } else if (frameId === this._frameId) { -            // New unique id -            if (id === null) { -                id = generateId(16); -            } -            const popup = new Popup({ -                id, -                depth, -                frameId: this._frameId, -                childrenSupported -            }); -            if (parent !== null) { -                if (parent.child !== null) { -                    throw new Error('Parent popup already has a child'); -                } -                popup.parent = parent; -                parent.child = popup; -            } -            this._popups.set(id, popup); -            popup.prepare(); -            return popup; -        } else { -            if (frameId === null) { -                throw new Error('Invalid frameId'); -            } -            const useFrameOffsetForwarder = (parentPopupId === null); -            ({id, depth, frameId} = await api.crossFrame.invoke(frameId, 'getOrCreatePopup', { -                id, -                parentPopupId, -                frameId, -                childrenSupported -            })); -            const popup = new PopupProxy({ -                id, -                depth, -                frameId, -                frameOffsetForwarder: useFrameOffsetForwarder ? this._frameOffsetForwarder : null -            }); -            this._popups.set(id, popup); -            return popup; -        } -    } - -    async setAllVisibleOverride(value, priority) { -        const promises = []; -        const errors = []; -        for (const popup of this._popups.values()) { -            const promise = popup.setVisibleOverride(value, priority) -                .then( -                    (token) => ({popup, token}), -                    (error) => { errors.push(error); return null; } -                ); -            promises.push(promise); -        } - -        const results = (await Promise.all(promises)).filter(({token}) => token !== null); - -        if (errors.length === 0) { -            const token = generateId(16); -            this._allPopupVisibilityTokenMap.set(token, results); -            return token; -        } - -        // Revert on error -        await this._revertPopupVisibilityOverrides(results); -        throw errors[0]; -    } - -    async clearAllVisibleOverride(token) { -        const results = this._allPopupVisibilityTokenMap.get(token); -        if (typeof results === 'undefined') { return false; } - -        this._allPopupVisibilityTokenMap.delete(token); -        await this._revertPopupVisibilityOverrides(results); -        return true; -    } - -    // API message handlers - -    async _onApiGetOrCreatePopup(details) { -        const popup = await this.getOrCreatePopup(details); -        return { -            id: popup.id, -            depth: popup.depth, -            frameId: popup.frameId -        }; -    } - -    async _onApiSetOptionsContext({id, optionsContext, source}) { -        const popup = this._getPopup(id); -        return await popup.setOptionsContext(optionsContext, source); -    } - -    _onApiHide({id, changeFocus}) { -        const popup = this._getPopup(id); -        return popup.hide(changeFocus); -    } - -    async _onApiIsVisibleAsync({id}) { -        const popup = this._getPopup(id); -        return await popup.isVisible(); -    } - -    async _onApiSetVisibleOverride({id, value, priority}) { -        const popup = this._getPopup(id); -        return await popup.setVisibleOverride(value, priority); -    } - -    async _onApiClearVisibleOverride({id, token}) { -        const popup = this._getPopup(id); -        return await popup.clearVisibleOverride(token); -    } - -    async _onApiContainsPoint({id, x, y}) { -        const popup = this._getPopup(id); -        [x, y] = this._convertPopupPointToRootPagePoint(popup, x, y); -        return await popup.containsPoint(x, y); -    } - -    async _onApiShowContent({id, details, displayDetails}) { -        const popup = this._getPopup(id); -        if (!this._popupCanShow(popup)) { return; } - -        const {elementRect} = details; -        if (typeof elementRect !== 'undefined') { -            details.elementRect = this._convertJsonRectToDOMRect(popup, elementRect); -        } - -        return await popup.showContent(details, displayDetails); -    } - -    _onApiSetCustomCss({id, css}) { -        const popup = this._getPopup(id); -        return popup.setCustomCss(css); -    } - -    _onApiClearAutoPlayTimer({id}) { -        const popup = this._getPopup(id); -        return popup.clearAutoPlayTimer(); -    } - -    _onApiSetContentScale({id, scale}) { -        const popup = this._getPopup(id); -        return popup.setContentScale(scale); -    } - -    _onApiUpdateTheme({id}) { -        const popup = this._getPopup(id); -        return popup.updateTheme(); -    } - -    _onApiSetCustomOuterCss({id, css, useWebExtensionApi}) { -        const popup = this._getPopup(id); -        return popup.setCustomOuterCss(css, useWebExtensionApi); -    } - -    async _onApiGetFrameSize({id}) { -        const popup = this._getPopup(id); -        return await popup.getFrameSize(); -    } - -    async _onApiSetFrameSize({id, width, height}) { -        const popup = this._getPopup(id); -        return await popup.setFrameSize(width, height); -    } - -    // Private functions - -    _getPopup(id) { -        const popup = this._popups.get(id); -        if (typeof popup === 'undefined') { -            throw new Error(`Invalid popup ID ${id}`); -        } -        return popup; -    } - -    _convertJsonRectToDOMRect(popup, jsonRect) { -        const [x, y] = this._convertPopupPointToRootPagePoint(popup, jsonRect.x, jsonRect.y); -        return new DOMRect(x, y, jsonRect.width, jsonRect.height); -    } - -    _convertPopupPointToRootPagePoint(popup, x, y) { -        const parent = popup.parent; -        if (parent !== null) { -            const popupRect = parent.getFrameRect(); -            x += popupRect.x; -            y += popupRect.y; -        } -        return [x, y]; -    } - -    _popupCanShow(popup) { -        const parent = popup.parent; -        return parent === null || parent.isVisibleSync(); -    } - -    async _revertPopupVisibilityOverrides(overrides) { -        const promises = []; -        for (const value of overrides) { -            if (value === null) { continue; } -            const {popup, token} = value; -            const promise = popup.clearVisibleOverride(token) -                .then( -                    (v) => v, -                    () => false -                ); -            promises.push(promise); -        } -        return await Promise.all(promises); -    } -} diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js deleted file mode 100644 index b2e81824..00000000 --- a/ext/fg/js/popup-proxy.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2019-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * api - */ - -class PopupProxy extends EventDispatcher { -    constructor({ -        id, -        depth, -        frameId, -        frameOffsetForwarder -    }) { -        super(); -        this._id = id; -        this._depth = depth; -        this._frameId = frameId; -        this._frameOffsetForwarder = frameOffsetForwarder; - -        this._frameOffset = [0, 0]; -        this._frameOffsetPromise = null; -        this._frameOffsetUpdatedAt = null; -        this._frameOffsetExpireTimeout = 1000; -    } - -    // Public properties - -    get id() { -        return this._id; -    } - -    get parent() { -        return null; -    } - -    set parent(value) { -        throw new Error('Not supported on PopupProxy'); -    } - -    get child() { -        return null; -    } - -    set child(value) { -        throw new Error('Not supported on PopupProxy'); -    } - -    get depth() { -        return this._depth; -    } - -    get frameContentWindow() { -        return null; -    } - -    get container() { -        return null; -    } - -    get frameId() { -        return this._frameId; -    } - -    // Public functions - -    setOptionsContext(optionsContext, source) { -        return this._invokeSafe('setOptionsContext', {id: this._id, optionsContext, source}); -    } - -    hide(changeFocus) { -        return this._invokeSafe('hide', {id: this._id, changeFocus}); -    } - -    isVisible() { -        return this._invokeSafe('isVisible', {id: this._id}, false); -    } - -    setVisibleOverride(value, priority) { -        return this._invokeSafe('setVisibleOverride', {id: this._id, value, priority}, null); -    } - -    clearVisibleOverride(token) { -        return this._invokeSafe('clearVisibleOverride', {id: this._id, token}, false); -    } - -    async containsPoint(x, y) { -        if (this._frameOffsetForwarder !== null) { -            await this._updateFrameOffset(); -            [x, y] = this._applyFrameOffset(x, y); -        } -        return await this._invokeSafe('containsPoint', {id: this._id, x, y}, false); -    } - -    async showContent(details, displayDetails) { -        const {elementRect} = details; -        if (typeof elementRect !== 'undefined') { -            let {x, y, width, height} = elementRect; -            if (this._frameOffsetForwarder !== null) { -                await this._updateFrameOffset(); -                [x, y] = this._applyFrameOffset(x, y); -            } -            details.elementRect = {x, y, width, height}; -        } -        return await this._invokeSafe('showContent', {id: this._id, details, displayDetails}); -    } - -    setCustomCss(css) { -        return this._invokeSafe('setCustomCss', {id: this._id, css}); -    } - -    clearAutoPlayTimer() { -        return this._invokeSafe('clearAutoPlayTimer', {id: this._id}); -    } - -    setContentScale(scale) { -        return this._invokeSafe('setContentScale', {id: this._id, scale}); -    } - -    isVisibleSync() { -        throw new Error('Not supported on PopupProxy'); -    } - -    updateTheme() { -        return this._invokeSafe('updateTheme', {id: this._id}); -    } - -    setCustomOuterCss(css, useWebExtensionApi) { -        return this._invokeSafe('setCustomOuterCss', {id: this._id, css, useWebExtensionApi}); -    } - -    getFrameRect() { -        return new DOMRect(0, 0, 0, 0); -    } - -    getFrameSize() { -        return this._invokeSafe('popup.getFrameSize', {id: this._id}, {width: 0, height: 0, valid: false}); -    } - -    setFrameSize(width, height) { -        return this._invokeSafe('popup.setFrameSize', {id: this._id, width, height}); -    } - -    // Private - -    _invoke(action, params={}) { -        return api.crossFrame.invoke(this._frameId, action, params); -    } - -    async _invokeSafe(action, params={}, defaultReturnValue) { -        try { -            return await this._invoke(action, params); -        } catch (e) { -            if (!yomichan.isExtensionUnloaded) { throw e; } -            return defaultReturnValue; -        } -    } - -    async _updateFrameOffset() { -        const now = Date.now(); -        const firstRun = this._frameOffsetUpdatedAt === null; -        const expired = firstRun || this._frameOffsetUpdatedAt < now - this._frameOffsetExpireTimeout; -        if (this._frameOffsetPromise === null && !expired) { return; } - -        if (this._frameOffsetPromise !== null) { -            if (firstRun) { -                await this._frameOffsetPromise; -            } -            return; -        } - -        const promise = this._updateFrameOffsetInner(now); -        if (firstRun) { -            await promise; -        } -    } - -    async _updateFrameOffsetInner(now) { -        this._frameOffsetPromise = this._frameOffsetForwarder.getOffset(); -        try { -            let offset = null; -            try { -                offset = await this._frameOffsetPromise; -            } catch (e) { -                // NOP -            } -            this._frameOffset = offset !== null ? offset : [0, 0]; -            if (offset === null) { -                this.trigger('offsetNotFound'); -                return; -            } -            this._frameOffsetUpdatedAt = now; -        } catch (e) { -            yomichan.logError(e); -        } finally { -            this._frameOffsetPromise = null; -        } -    } - -    _applyFrameOffset(x, y) { -        const [offsetX, offsetY] = this._frameOffset; -        return [x + offsetX, y + offsetY]; -    } -} diff --git a/ext/fg/js/popup-window.js b/ext/fg/js/popup-window.js deleted file mode 100644 index 5fa0c647..00000000 --- a/ext/fg/js/popup-window.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2020-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * api - */ - -class PopupWindow extends EventDispatcher { -    constructor({ -        id, -        depth, -        frameId -    }) { -        super(); -        this._id = id; -        this._depth = depth; -        this._frameId = frameId; -        this._popupTabId = null; -    } - -    // Public properties - -    get id() { -        return this._id; -    } - -    get parent() { -        return null; -    } - -    set parent(value) { -        throw new Error('Not supported on PopupProxy'); -    } - -    get child() { -        return null; -    } - -    set child(value) { -        throw new Error('Not supported on PopupProxy'); -    } - -    get depth() { -        return this._depth; -    } - -    get frameContentWindow() { -        return null; -    } - -    get container() { -        return null; -    } - -    get frameId() { -        return this._frameId; -    } - - -    // Public functions - -    setOptionsContext(optionsContext, source) { -        return this._invoke(false, 'setOptionsContext', {id: this._id, optionsContext, source}); -    } - -    hide(_changeFocus) { -        // NOP -    } - -    async isVisible() { -        return (this._popupTabId !== null && await api.isTabSearchPopup(this._popupTabId)); -    } - -    async setVisibleOverride(_value, _priority) { -        return null; -    } - -    clearVisibleOverride(_token) { -        return false; -    } - -    async containsPoint(_x, _y) { -        return false; -    } - -    async showContent(_details, displayDetails) { -        if (displayDetails === null) { return; } -        await this._invoke(true, 'setContent', {id: this._id, details: displayDetails}); -    } - -    setCustomCss(css) { -        return this._invoke(false, 'setCustomCss', {id: this._id, css}); -    } - -    clearAutoPlayTimer() { -        return this._invoke(false, 'clearAutoPlayTimer', {id: this._id}); -    } - -    setContentScale(_scale) { -        // NOP -    } - -    isVisibleSync() { -        throw new Error('Not supported on PopupWindow'); -    } - -    updateTheme() { -        // NOP -    } - -    async setCustomOuterCss(_css, _useWebExtensionApi) { -        // NOP -    } - -    getFrameRect() { -        return new DOMRect(0, 0, 0, 0); -    } - -    async getFrameSize() { -        return {width: 0, height: 0, valid: false}; -    } - -    async setFrameSize(_width, _height) { -        return false; -    } - -    // Private - -    async _invoke(open, action, params={}, defaultReturnValue) { -        if (yomichan.isExtensionUnloaded) { -            return defaultReturnValue; -        } - -        const frameId = 0; -        if (this._popupTabId !== null) { -            try { -                return await api.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); -            } catch (e) { -                if (yomichan.isExtensionUnloaded) { -                    open = false; -                } -            } -            this._popupTabId = null; -        } - -        if (!open) { -            return defaultReturnValue; -        } - -        const {tabId} = await api.getOrCreateSearchPopup({focus: 'ifCreated'}); -        this._popupTabId = tabId; - -        return await api.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); -    } -} diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js deleted file mode 100644 index 75b74257..00000000 --- a/ext/fg/js/popup.js +++ /dev/null @@ -1,687 +0,0 @@ -/* - * Copyright (C) 2016-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * DocumentUtil - * FrameClient - * api - * dynamicLoader - */ - -class Popup extends EventDispatcher { -    constructor({ -        id, -        depth, -        frameId, -        childrenSupported -    }) { -        super(); -        this._id = id; -        this._depth = depth; -        this._frameId = frameId; -        this._childrenSupported = childrenSupported; -        this._parent = null; -        this._child = null; -        this._injectPromise = null; -        this._injectPromiseComplete = false; -        this._visible = new DynamicProperty(false); -        this._options = null; -        this._optionsContext = null; -        this._contentScale = 1.0; -        this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); - -        this._frameSizeContentScale = null; -        this._frameClient = null; -        this._frame = document.createElement('iframe'); -        this._frame.className = 'yomichan-popup'; -        this._frame.style.width = '0'; -        this._frame.style.height = '0'; - -        this._container = this._frame; -        this._shadow = null; - -        this._fullscreenEventListeners = new EventListenerCollection(); -    } - -    // Public properties - -    get id() { -        return this._id; -    } - -    get parent() { -        return this._parent; -    } - -    set parent(value) { -        this._parent = value; -    } - -    get child() { -        return this._child; -    } - -    set child(value) { -        this._child = value; -    } - -    get depth() { -        return this._depth; -    } - -    get frameContentWindow() { -        return this._frame.contentWindow; -    } - -    get container() { -        return this._container; -    } - -    get frameId() { -        return this._frameId; -    } - -    // Public functions - -    prepare() { -        this._frame.addEventListener('mouseover', this._onFrameMouseOver.bind(this)); -        this._frame.addEventListener('mouseout', this._onFrameMouseOut.bind(this)); -        this._frame.addEventListener('mousedown', (e) => e.stopPropagation()); -        this._frame.addEventListener('scroll', (e) => e.stopPropagation()); -        this._frame.addEventListener('load', this._onFrameLoad.bind(this)); -        this._visible.on('change', this._onVisibleChange.bind(this)); -        yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this)); -        this._onVisibleChange({value: this.isVisibleSync()}); -    } - -    async setOptionsContext(optionsContext) { -        await this._setOptionsContext(optionsContext); -        await this._invokeSafe('setOptionsContext', {optionsContext}); -    } - -    hide(changeFocus) { -        if (!this.isVisibleSync()) { -            return; -        } - -        this._setVisible(false); -        if (this._child !== null) { -            this._child.hide(false); -        } -        if (changeFocus) { -            this._focusParent(); -        } -    } - -    async isVisible() { -        return this.isVisibleSync(); -    } - -    async setVisibleOverride(value, priority) { -        return this._visible.setOverride(value, priority); -    } - -    async clearVisibleOverride(token) { -        return this._visible.clearOverride(token); -    } - -    async containsPoint(x, y) { -        for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup.child) { -            const rect = popup.getFrameRect(); -            if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { -                return true; -            } -        } -        return false; -    } - -    async showContent(details, displayDetails) { -        if (this._options === null) { throw new Error('Options not assigned'); } - -        const {optionsContext, elementRect, writingMode} = details; -        if (optionsContext !== null) { -            await this._setOptionsContextIfDifferent(optionsContext); -        } - -        if (typeof elementRect !== 'undefined' && typeof writingMode !== 'undefined') { -            await this._show(elementRect, writingMode); -        } - -        if (displayDetails !== null) { -            this._invokeSafe('setContent', {details: displayDetails}); -        } -    } - -    setCustomCss(css) { -        this._invokeSafe('setCustomCss', {css}); -    } - -    clearAutoPlayTimer() { -        this._invokeSafe('clearAutoPlayTimer'); -    } - -    setContentScale(scale) { -        this._contentScale = scale; -        this._frame.style.fontSize = `${scale}px`; -        this._invokeSafe('setContentScale', {scale}); -    } - -    isVisibleSync() { -        return this._visible.value; -    } - -    updateTheme() { -        const {popupTheme, popupOuterTheme} = this._options.general; -        this._frame.dataset.theme = popupTheme; -        this._frame.dataset.outerTheme = popupOuterTheme; -        this._frame.dataset.siteColor = this._getSiteColor(); -    } - -    async setCustomOuterCss(css, useWebExtensionApi) { -        let parentNode = null; -        const inShadow = (this._shadow !== null); -        if (inShadow) { -            useWebExtensionApi = false; -            parentNode = this._shadow; -        } -        const node = await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi, parentNode); -        this.trigger('customOuterCssChanged', {node, useWebExtensionApi, inShadow}); -    } - -    getFrameRect() { -        return this._frame.getBoundingClientRect(); -    } - -    async getFrameSize() { -        const rect = this._frame.getBoundingClientRect(); -        return {width: rect.width, height: rect.height, valid: true}; -    } - -    async setFrameSize(width, height) { -        this._setFrameSize(width, height); -        return true; -    } - -    // Private functions - -    _onFrameMouseOver() { -        this.trigger('framePointerOver', {}); -    } - -    _onFrameMouseOut() { -        this.trigger('framePointerOut', {}); -    } - -    _inject() { -        let injectPromise = this._injectPromise; -        if (injectPromise === null) { -            injectPromise = this._createInjectPromise(); -            this._injectPromise = injectPromise; -            injectPromise.then( -                () => { -                    if (injectPromise !== this._injectPromise) { return; } -                    this._injectPromiseComplete = true; -                }, -                () => { this._resetFrame(); } -            ); -        } -        return injectPromise; -    } - -    async _createInjectPromise() { -        if (this._options === null) { -            throw new Error('Options not initialized'); -        } - -        const {useSecurePopupFrameUrl, usePopupShadowDom} = this._options.general; - -        await this._setUpContainer(usePopupShadowDom); - -        const setupFrame = (frame) => { -            frame.removeAttribute('src'); -            frame.removeAttribute('srcdoc'); -            this._observeFullscreen(true); -            this._onFullscreenChanged(); -            const url = chrome.runtime.getURL('/popup.html'); -            if (useSecurePopupFrameUrl) { -                frame.contentDocument.location.href = url; -            } else { -                frame.setAttribute('src', url); -            } -        }; - -        const frameClient = new FrameClient(); -        this._frameClient = frameClient; -        await frameClient.connect(this._frame, this._targetOrigin, this._frameId, setupFrame); - -        // Configure -        await this._invokeSafe('configure', { -            depth: this._depth, -            parentPopupId: this._id, -            parentFrameId: this._frameId, -            childrenSupported: this._childrenSupported, -            scale: this._contentScale, -            optionsContext: this._optionsContext -        }); -    } - -    _onFrameLoad() { -        if (!this._injectPromiseComplete) { return; } -        this._resetFrame(); -    } - -    _resetFrame() { -        const parent = this._container.parentNode; -        if (parent !== null) { -            parent.removeChild(this._container); -        } -        this._frame.removeAttribute('src'); -        this._frame.removeAttribute('srcdoc'); - -        this._frameClient = null; -        this._injectPromise = null; -        this._injectPromiseComplete = false; -    } - -    async _setUpContainer(usePopupShadowDom) { -        if (usePopupShadowDom && typeof this._frame.attachShadow === 'function') { -            const container = document.createElement('div'); -            container.style.setProperty('all', 'initial', 'important'); -            const shadow = container.attachShadow({mode: 'closed', delegatesFocus: true}); -            shadow.appendChild(this._frame); - -            this._container = container; -            this._shadow = shadow; -        } else { -            const frameParentNode = this._frame.parentNode; -            if (frameParentNode !== null) { -                frameParentNode.removeChild(this._frame); -            } - -            this._container = this._frame; -            this._shadow = null; -        } - -        await this._injectStyles(); -    } - -    async _injectStyles() { -        try { -            await this._injectPopupOuterStylesheet(); -        } catch (e) { -            // NOP -        } - -        try { -            await this.setCustomOuterCss(this._options.general.customPopupOuterCss, true); -        } catch (e) { -            // NOP -        } -    } - -    async _injectPopupOuterStylesheet() { -        let fileType = 'file'; -        let useWebExtensionApi = true; -        let parentNode = null; -        if (this._shadow !== null) { -            fileType = 'file-content'; -            useWebExtensionApi = false; -            parentNode = this._shadow; -        } -        await dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', fileType, '/css/popup-outer.css', useWebExtensionApi, parentNode); -    } - -    _observeFullscreen(observe) { -        if (!observe) { -            this._fullscreenEventListeners.removeAllEventListeners(); -            return; -        } - -        if (this._fullscreenEventListeners.size > 0) { -            // Already observing -            return; -        } - -        DocumentUtil.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners); -    } - -    _onFullscreenChanged() { -        const parent = this._getFrameParentElement(); -        if (parent !== null && this._container.parentNode !== parent) { -            parent.appendChild(this._container); -        } -    } - -    async _show(elementRect, writingMode) { -        await this._inject(); - -        const optionsGeneral = this._options.general; -        const {popupDisplayMode} = optionsGeneral; -        const frame = this._frame; -        const frameRect = frame.getBoundingClientRect(); - -        const viewport = this._getViewport(optionsGeneral.popupScaleRelativeToVisualViewport); -        const scale = this._contentScale; -        const scaleRatio = this._frameSizeContentScale === null ? 1.0 : scale / this._frameSizeContentScale; -        this._frameSizeContentScale = scale; -        const getPositionArgs = [ -            elementRect, -            Math.max(frameRect.width * scaleRatio, optionsGeneral.popupWidth * scale), -            Math.max(frameRect.height * scaleRatio, optionsGeneral.popupHeight * scale), -            viewport, -            scale, -            optionsGeneral, -            writingMode -        ]; -        let [x, y, width, height, below] = ( -            writingMode === 'horizontal-tb' || optionsGeneral.popupVerticalTextPosition === 'default' ? -            this._getPositionForHorizontalText(...getPositionArgs) : -            this._getPositionForVerticalText(...getPositionArgs) -        ); - -        frame.dataset.popupDisplayMode = popupDisplayMode; -        frame.dataset.below = `${below}`; - -        if (popupDisplayMode === 'full-width') { -            x = viewport.left; -            y = below ? viewport.bottom - height : viewport.top; -            width = viewport.right - viewport.left; -        } - -        frame.style.left = `${x}px`; -        frame.style.top = `${y}px`; -        this._setFrameSize(width, height); - -        this._setVisible(true); -        if (this._child !== null) { -            this._child.hide(true); -        } -    } - -    _setFrameSize(width, height) { -        const {style} = this._frame; -        style.width = `${width}px`; -        style.height = `${height}px`; -    } - -    _setVisible(visible) { -        this._visible.defaultValue = visible; -    } - -    _onVisibleChange({value}) { -        this._frame.style.setProperty('visibility', value ? 'visible' : 'hidden', 'important'); -    } - -    _focusParent() { -        if (this._parent !== null) { -            // Chrome doesn't like focusing iframe without contentWindow. -            const contentWindow = this._parent.frameContentWindow; -            if (contentWindow !== null) { -                contentWindow.focus(); -            } -        } else { -            // Firefox doesn't like focusing window without first blurring the iframe. -            // this._frame.contentWindow.blur() doesn't work on Firefox for some reason. -            this._frame.blur(); -            // This is needed for Chrome. -            window.focus(); -        } -    } - -    _getSiteColor() { -        const color = [255, 255, 255]; -        const {documentElement, body} = document; -        if (documentElement !== null) { -            this._addColor(color, window.getComputedStyle(documentElement).backgroundColor); -        } -        if (body !== null) { -            this._addColor(color, window.getComputedStyle(body).backgroundColor); -        } -        const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); -        return dark ? 'dark' : 'light'; -    } - -    async _invoke(action, params={}) { -        const contentWindow = this._frame.contentWindow; -        if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; } - -        const message = this._frameClient.createMessage({action, params}); -        return await api.crossFrame.invoke(this._frameClient.frameId, 'popupMessage', message); -    } - -    async _invokeSafe(action, params={}, defaultReturnValue) { -        try { -            return await this._invoke(action, params); -        } catch (e) { -            if (!yomichan.isExtensionUnloaded) { throw e; } -            return defaultReturnValue; -        } -    } - -    _invokeWindow(action, params={}) { -        const contentWindow = this._frame.contentWindow; -        if (this._frameClient === null || !this._frameClient.isConnected() || contentWindow === null) { return; } - -        const message = this._frameClient.createMessage({action, params}); -        contentWindow.postMessage(message, this._targetOrigin); -    } - -    _onExtensionUnloaded() { -        this._invokeWindow('extensionUnloaded'); -    } - -    _getFrameParentElement() { -        const defaultParent = document.body; -        const fullscreenElement = DocumentUtil.getFullscreenElement(); -        if ( -            fullscreenElement === null || -            fullscreenElement.shadowRoot || -            fullscreenElement.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions -        ) { -            return defaultParent; -        } - -        switch (fullscreenElement.nodeName.toUpperCase()) { -            case 'IFRAME': -            case 'FRAME': -                return defaultParent; -        } - -        return fullscreenElement; -    } - -    _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { -        const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); -        const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; -        const verticalOffset = optionsGeneral.popupVerticalOffset * offsetScale; - -        const [x, w] = this._getConstrainedPosition( -            elementRect.right - horizontalOffset, -            elementRect.left + horizontalOffset, -            width, -            viewport.left, -            viewport.right, -            true -        ); -        const [y, h, below] = this._getConstrainedPositionBinary( -            elementRect.top - verticalOffset, -            elementRect.bottom + verticalOffset, -            height, -            viewport.top, -            viewport.bottom, -            preferBelow -        ); -        return [x, y, w, h, below]; -    } - -    _getPositionForVerticalText(elementRect, width, height, viewport, offsetScale, optionsGeneral, writingMode) { -        const preferRight = this._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); -        const horizontalOffset = optionsGeneral.popupHorizontalOffset2 * offsetScale; -        const verticalOffset = optionsGeneral.popupVerticalOffset2 * offsetScale; - -        const [x, w] = this._getConstrainedPositionBinary( -            elementRect.left - horizontalOffset, -            elementRect.right + horizontalOffset, -            width, -            viewport.left, -            viewport.right, -            preferRight -        ); -        const [y, h, below] = this._getConstrainedPosition( -            elementRect.bottom - verticalOffset, -            elementRect.top + verticalOffset, -            height, -            viewport.top, -            viewport.bottom, -            true -        ); -        return [x, y, w, h, below]; -    } - -    _isVerticalTextPopupOnRight(positionPreference, writingMode) { -        switch (positionPreference) { -            case 'before': -                return !this._isWritingModeLeftToRight(writingMode); -            case 'after': -                return this._isWritingModeLeftToRight(writingMode); -            case 'left': -                return false; -            case 'right': -                return true; -            default: -                return false; -        } -    } - -    _isWritingModeLeftToRight(writingMode) { -        switch (writingMode) { -            case 'vertical-lr': -            case 'sideways-lr': -                return true; -            default: -                return false; -        } -    } - -    _getConstrainedPosition(positionBefore, positionAfter, size, minLimit, maxLimit, after) { -        size = Math.min(size, maxLimit - minLimit); - -        let position; -        if (after) { -            position = Math.max(minLimit, positionAfter); -            position = position - Math.max(0, (position + size) - maxLimit); -        } else { -            position = Math.min(maxLimit, positionBefore) - size; -            position = position + Math.max(0, minLimit - position); -        } - -        return [position, size, after]; -    } - -    _getConstrainedPositionBinary(positionBefore, positionAfter, size, minLimit, maxLimit, after) { -        const overflowBefore = minLimit - (positionBefore - size); -        const overflowAfter = (positionAfter + size) - maxLimit; - -        if (overflowAfter > 0 || overflowBefore > 0) { -            after = (overflowAfter < overflowBefore); -        } - -        let position; -        if (after) { -            size -= Math.max(0, overflowAfter); -            position = Math.max(minLimit, positionAfter); -        } else { -            size -= Math.max(0, overflowBefore); -            position = Math.min(maxLimit, positionBefore) - size; -        } - -        return [position, size, after]; -    } - -    _addColor(target, cssColor) { -        if (typeof cssColor !== 'string') { return; } - -        const color = this._getColorInfo(cssColor); -        if (color === null) { return; } - -        const a = color[3]; -        if (a <= 0.0) { return; } - -        const aInv = 1.0 - a; -        for (let i = 0; i < 3; ++i) { -            target[i] = target[i] * aInv + color[i] * a; -        } -    } - -    _getColorInfo(cssColor) { -        const m = /^\s*rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)\s*$/.exec(cssColor); -        if (m === null) { return null; } - -        const m4 = m[4]; -        return [ -            Number.parseInt(m[1], 10), -            Number.parseInt(m[2], 10), -            Number.parseInt(m[3], 10), -            m4 ? Math.max(0.0, Math.min(1.0, Number.parseFloat(m4))) : 1.0 -        ]; -    } - -    _getViewport(useVisualViewport) { -        const visualViewport = window.visualViewport; -        if (visualViewport !== null && typeof visualViewport === 'object') { -            const left = visualViewport.offsetLeft; -            const top = visualViewport.offsetTop; -            const width = visualViewport.width; -            const height = visualViewport.height; -            if (useVisualViewport) { -                return { -                    left, -                    top, -                    right: left + width, -                    bottom: top + height -                }; -            } else { -                const scale = visualViewport.scale; -                return { -                    left: 0, -                    top: 0, -                    right: Math.max(left + width, width * scale), -                    bottom: Math.max(top + height, height * scale) -                }; -            } -        } - -        const body = document.body; -        return { -            left: 0, -            top: 0, -            right: (body !== null ? body.clientWidth : 0), -            bottom: window.innerHeight -        }; -    } - -    async _setOptionsContext(optionsContext) { -        this._optionsContext = optionsContext; -        this._options = await api.optionsGet(optionsContext); -        this.updateTheme(); -    } - -    async _setOptionsContextIfDifferent(optionsContext) { -        if (deepEqual(this._optionsContext, optionsContext)) { return; } -        await this._setOptionsContext(optionsContext); -    } -} diff --git a/ext/fg/js/text-source-element.js b/ext/fg/js/text-source-element.js deleted file mode 100644 index 45186636..00000000 --- a/ext/fg/js/text-source-element.js +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2016-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -class TextSourceElement { -    constructor(element, fullContent=null, startOffset=0, endOffset=0) { -        this._element = element; -        this._fullContent = (typeof fullContent === 'string' ? fullContent : TextSourceElement.getElementContent(element)); -        this._startOffset = startOffset; -        this._endOffset = endOffset; -        this._content = this._fullContent.substring(this._startOffset, this._endOffset); -    } - -    get element() { -        return this._element; -    } - -    get fullContent() { -        return this._fullContent; -    } - -    get startOffset() { -        return this._startOffset; -    } - -    get endOffset() { -        return this._endOffset; -    } - -    get isConnected() { -        return this._element.isConnected; -    } - -    clone() { -        return new TextSourceElement(this._element, this._fullContent, this._startOffset, this._endOffset); -    } - -    cleanup() { -        // NOP -    } - -    text() { -        return this._content; -    } - -    setEndOffset(length, fromEnd=false) { -        if (fromEnd) { -            const delta = Math.min(this._fullContent.length - this._endOffset, length); -            this._endOffset += delta; -            this._content = this._fullContent.substring(this._startOffset, this._endOffset); -            return delta; -        } else { -            const delta = Math.min(this._fullContent.length - this._startOffset, length); -            this._endOffset = this._startOffset + delta; -            this._content = this._fullContent.substring(this._startOffset, this._endOffset); -            return delta; -        } -    } - -    setStartOffset(length) { -        const delta = Math.min(this._startOffset, length); -        this._startOffset -= delta; -        this._content = this._fullContent.substring(this._startOffset, this._endOffset); -        return delta; -    } - -    collapse(toStart) { -        if (toStart) { -            this._endOffset = this._startOffset; -        } else { -            this._startOffset = this._endOffset; -        } -        this._content = ''; -    } - -    getRect() { -        return this._element.getBoundingClientRect(); -    } - -    getWritingMode() { -        return 'horizontal-tb'; -    } - -    select() { -        // NOP -    } - -    deselect() { -        // NOP -    } - -    hasSameStart(other) { -        return ( -            typeof other === 'object' && -            other !== null && -            other instanceof TextSourceElement && -            this._element === other.element && -            this._fullContent === other.fullContent && -            this._startOffset === other.startOffset -        ); -    } - -    getNodesInRange() { -        return [this._element]; -    } - -    static getElementContent(element) { -        let content; -        switch (element.nodeName.toUpperCase()) { -            case 'BUTTON': -                content = element.textContent; -                break; -            case 'IMG': -                content = element.getAttribute('alt') || ''; -                break; -            default: -                content = `${element.value}`; -                break; -        } - -        // Remove zero-width non-joiner -        content = content.replace(/\u200c/g, ''); - -        return content; -    } -} diff --git a/ext/fg/js/text-source-range.js b/ext/fg/js/text-source-range.js deleted file mode 100644 index 377016da..00000000 --- a/ext/fg/js/text-source-range.js +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2016-2021  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * DOMTextScanner - * DocumentUtil - */ - -class TextSourceRange { -    constructor(range, content, imposterContainer, imposterSourceElement) { -        this._range = range; -        this._rangeStartOffset = range.startOffset; -        this._content = content; -        this._imposterContainer = imposterContainer; -        this._imposterSourceElement = imposterSourceElement; -    } - -    get range() { -        return this._range; -    } - -    get rangeStartOffset() { -        return this._rangeStartOffset; -    } - -    get imposterSourceElement() { -        return this._imposterSourceElement; -    } - -    get isConnected() { -        return ( -            this._range.startContainer.isConnected && -            this._range.endContainer.isConnected -        ); -    } - -    clone() { -        return new TextSourceRange(this._range.cloneRange(), this._content, this._imposterContainer, this._imposterSourceElement); -    } - -    cleanup() { -        if (this._imposterContainer !== null && this._imposterContainer.parentNode !== null) { -            this._imposterContainer.parentNode.removeChild(this._imposterContainer); -        } -    } - -    text() { -        return this._content; -    } - -    setEndOffset(length, layoutAwareScan, fromEnd=false) { -        const state = ( -            fromEnd ? -            new DOMTextScanner(this._range.endContainer, this._range.endOffset, !layoutAwareScan, layoutAwareScan).seek(length) : -            new DOMTextScanner(this._range.startContainer, this._range.startOffset, !layoutAwareScan, layoutAwareScan).seek(length) -        ); -        this._range.setEnd(state.node, state.offset); -        this._content = (fromEnd ? this._content + state.content : state.content); -        return length - state.remainder; -    } - -    setStartOffset(length, layoutAwareScan) { -        const state = new DOMTextScanner(this._range.startContainer, this._range.startOffset, !layoutAwareScan, layoutAwareScan).seek(-length); -        this._range.setStart(state.node, state.offset); -        this._rangeStartOffset = this._range.startOffset; -        this._content = state.content + this._content; -        return length - state.remainder; -    } - -    collapse(toStart) { -        this._range.collapse(toStart); -        this._content = ''; -    } - -    getRect() { -        return this._range.getBoundingClientRect(); -    } - -    getWritingMode() { -        return TextSourceRange.getElementWritingMode(TextSourceRange.getParentElement(this._range.startContainer)); -    } - -    select() { -        const selection = window.getSelection(); -        selection.removeAllRanges(); -        selection.addRange(this._range); -    } - -    deselect() { -        const selection = window.getSelection(); -        selection.removeAllRanges(); -    } - -    hasSameStart(other) { -        if (!( -            typeof other === 'object' && -            other !== null && -            other instanceof TextSourceRange -        )) { -            return false; -        } -        if (this._imposterSourceElement !== null) { -            return ( -                this._imposterSourceElement === other.imposterSourceElement && -                this._rangeStartOffset === other.rangeStartOffset -            ); -        } else { -            try { -                return this._range.compareBoundaryPoints(Range.START_TO_START, other.range) === 0; -            } catch (e) { -                if (e.name === 'WrongDocumentError') { -                    // This can happen with shadow DOMs if the ranges are in different documents. -                    return false; -                } -                throw e; -            } -        } -    } - -    getNodesInRange() { -        return DocumentUtil.getNodesInRange(this._range); -    } - -    static getParentElement(node) { -        while (node !== null && node.nodeType !== Node.ELEMENT_NODE) { -            node = node.parentNode; -        } -        return node; -    } - -    static getElementWritingMode(element) { -        if (element !== null) { -            const style = window.getComputedStyle(element); -            const writingMode = style.writingMode; -            if (typeof writingMode === 'string') { -                return TextSourceRange.normalizeWritingMode(writingMode); -            } -        } -        return 'horizontal-tb'; -    } - -    static normalizeWritingMode(writingMode) { -        switch (writingMode) { -            case 'lr': -            case 'lr-tb': -            case 'rl': -                return 'horizontal-tb'; -            case 'tb': -                return 'vertical-lr'; -            case 'tb-rl': -                return 'vertical-rl'; -            default: -                return writingMode; -        } -    } -} |