summaryrefslogtreecommitdiff
path: root/ext/fg/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-02-13 23:13:53 -0500
committerGitHub <noreply@github.com>2021-02-13 23:13:53 -0500
commit7a74c3c31ece7788e82c46f22cb4327ffe08307a (patch)
tree7d4aee53b1dab15bdf317729ee1559291c04a4b2 /ext/fg/js
parent6a271e067fa917614f4c81f473533e24c6d04404 (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.js59
-rw-r--r--ext/fg/js/dom-text-scanner.js551
-rw-r--r--ext/fg/js/float-main.js56
-rw-r--r--ext/fg/js/frame-ancestry-handler.js269
-rw-r--r--ext/fg/js/frame-offset-forwarder.js70
-rw-r--r--ext/fg/js/frontend.js691
-rw-r--r--ext/fg/js/popup-factory.js319
-rw-r--r--ext/fg/js/popup-proxy.js218
-rw-r--r--ext/fg/js/popup-window.js169
-rw-r--r--ext/fg/js/popup.js687
-rw-r--r--ext/fg/js/text-source-element.js139
-rw-r--r--ext/fg/js/text-source-range.js170
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;
- }
- }
-}