diff options
Diffstat (limited to 'ext/fg')
-rw-r--r-- | ext/fg/float.html | 6 | ||||
-rw-r--r-- | ext/fg/js/content-script-main.js | 148 | ||||
-rw-r--r-- | ext/fg/js/document.js | 11 | ||||
-rw-r--r-- | ext/fg/js/float-main.js | 47 | ||||
-rw-r--r-- | ext/fg/js/float.js | 76 | ||||
-rw-r--r-- | ext/fg/js/frame-offset-forwarder.js | 34 | ||||
-rw-r--r-- | ext/fg/js/frontend-api-receiver.js | 76 | ||||
-rw-r--r-- | ext/fg/js/frontend-api-sender.js | 128 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 253 | ||||
-rw-r--r-- | ext/fg/js/popup-factory.js | 16 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy.js | 28 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 88 | ||||
-rw-r--r-- | ext/fg/js/source.js | 224 |
13 files changed, 417 insertions, 718 deletions
diff --git a/ext/fg/float.html b/ext/fg/float.html index 89952524..3e41cde5 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -17,8 +17,8 @@ <div id="spinner" hidden><img src="/mixed/img/spinner.gif"></div> <div id="navigation-header" class="navigation-header" hidden><div class="navigation-header-actions"> - <button class="action-button action-previous"><img src="/mixed/img/source-term.svg" class="icon-image" title="Source term (Alt + B)" alt></button> - <button class="action-button action-next"><img src="/mixed/img/source-term.svg" class="icon-image" title="Next term (Alt + F)" alt></button> + <button class="action-button action-previous" data-icon="source-term" title="Source term (Alt + B)"></button> + <button class="action-button action-next" data-icon="source-term" title="Next term (Alt + F)"></button> </div></div><div class="navigation-header-spacer"></div> <div id="definitions"></div> @@ -40,11 +40,13 @@ </div> <script src="/mixed/js/core.js"></script> + <script src="/mixed/js/comm.js"></script> <script src="/mixed/js/dom.js"></script> <script src="/mixed/js/api.js"></script> <script src="/mixed/js/japanese.js"></script> <script src="/fg/js/document.js"></script> + <script src="/fg/js/dom-text-scanner.js"></script> <script src="/fg/js/source.js"></script> <script src="/mixed/js/audio-system.js"></script> <script src="/mixed/js/display-context.js"></script> diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index 57386b85..1f3a69e5 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -16,141 +16,31 @@ */ /* global - * DOM - * FrameOffsetForwarder * Frontend * PopupFactory - * PopupProxy - * apiBroadcastTab - * apiForwardLogsToBackend - * apiFrameInformationGet - * apiOptionsGet + * api */ -async function createIframePopupProxy(frameOffsetForwarder, setDisabled) { - const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( - chrome.runtime.onMessage, - ({action, params}, {resolve}) => { - if (action === 'rootPopupInformation') { - resolve(params); - } - } - ); - apiBroadcastTab('rootPopupRequestInformationBroadcast'); - const {popupId, frameId: parentFrameId} = await rootPopupInformationPromise; - - const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); - - const popup = new PopupProxy(popupId, 0, null, parentFrameId, getFrameOffset, setDisabled); - await popup.prepare(); - - return popup; -} - -async function getOrCreatePopup(depth) { - const {frameId} = await apiFrameInformationGet(); - if (typeof frameId !== 'number') { - const error = new Error('Failed to get frameId'); - yomichan.logError(error); - throw error; - } - - const popupFactory = new PopupFactory(frameId); - await popupFactory.prepare(); - - const popup = popupFactory.getOrCreatePopup(null, null, depth); - - return popup; -} - -async function createPopupProxy(depth, id, parentFrameId) { - const popup = new PopupProxy(null, depth + 1, id, parentFrameId); - await popup.prepare(); - - return popup; -} - (async () => { - apiForwardLogsToBackend(); - await yomichan.prepare(); - - const data = window.frontendInitializationData || {}; - const {id, depth=0, parentFrameId, url=window.location.href, proxy=false, isSearchPage=false} = data; - - const isIframe = !proxy && (window !== window.parent); + try { + api.forwardLogsToBackend(); + await yomichan.prepare(); - const popups = { - iframe: null, - proxy: null, - normal: null - }; - - let frontend = null; - let frontendPreparePromise = null; - let frameOffsetForwarder = null; - - let iframePopupsInRootFrameAvailable = true; - - const disableIframePopupsInRootFrame = () => { - iframePopupsInRootFrameAvailable = false; - applyOptions(); - }; - - let urlUpdatedAt = 0; - let popupProxyUrlCached = url; - const getPopupProxyUrl = async () => { - const now = Date.now(); - if (popups.proxy !== null && now - urlUpdatedAt > 500) { - popupProxyUrlCached = await popups.proxy.getUrl(); - urlUpdatedAt = now; - } - return popupProxyUrlCached; - }; - - const applyOptions = async () => { - const optionsContext = { - depth: isSearchPage ? 0 : depth, - url: proxy ? await getPopupProxyUrl() : window.location.href - }; - const options = await apiOptionsGet(optionsContext); - - if (!proxy && frameOffsetForwarder === null) { - frameOffsetForwarder = new FrameOffsetForwarder(); - frameOffsetForwarder.start(); + const {frameId} = await api.frameInformationGet(); + if (typeof frameId !== 'number') { + throw new Error('Failed to get frameId'); } - let popup; - if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) { - popup = popups.iframe || await createIframePopupProxy(frameOffsetForwarder, disableIframePopupsInRootFrame); - popups.iframe = popup; - } else if (proxy) { - popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId); - popups.proxy = popup; - } else { - popup = popups.normal || await getOrCreatePopup(depth); - popups.normal = popup; - } - - if (frontend === null) { - const getUrl = proxy ? getPopupProxyUrl : null; - frontend = new Frontend(popup, getUrl); - frontendPreparePromise = frontend.prepare(); - await frontendPreparePromise; - } else { - await frontendPreparePromise; - if (isSearchPage) { - const disabled = !options.scanning.enableOnSearchPage; - frontend.setDisabledOverride(disabled); - } - - if (isIframe) { - await frontend.setPopup(popup); - } - } - }; - - yomichan.on('optionsUpdated', applyOptions); - window.addEventListener('fullscreenchange', applyOptions, false); - - await applyOptions(); + const popupFactory = new PopupFactory(frameId); + popupFactory.prepare(); + + const frontend = new Frontend( + frameId, + popupFactory, + {} + ); + await frontend.prepare(); + } catch (e) { + yomichan.logError(e); + } })(); diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index d639bc86..c288502c 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -17,6 +17,7 @@ /* global * DOM + * DOMTextScanner * TextSourceElement * TextSourceRange */ @@ -152,14 +153,14 @@ function docRangeFromPoint(x, y, deepDomScan) { } } -function docSentenceExtract(source, extent) { +function docSentenceExtract(source, extent, layoutAwareScan) { const quotesFwd = {'「': '」', '『': '』', "'": "'", '"': '"'}; const quotesBwd = {'」': '「', '』': '『', "'": "'", '"': '"'}; const terminators = '…。..??!!'; const sourceLocal = source.clone(); - const position = sourceLocal.setStartOffset(extent); - sourceLocal.setEndOffset(extent * 2 - position, true); + const position = sourceLocal.setStartOffset(extent, layoutAwareScan); + sourceLocal.setEndOffset(extent * 2 - position, layoutAwareScan, true); const content = sourceLocal.text(); let quoteStack = []; @@ -232,7 +233,7 @@ function isPointInRange(x, y, range) { const nodePre = range.endContainer; const offsetPre = range.endOffset; try { - const {node, offset, content} = TextSourceRange.seekForward(range.endContainer, range.endOffset, 1); + const {node, offset, content} = new DOMTextScanner(range.endContainer, range.endOffset, true, false).seek(1); range.setEnd(node, offset); if (!isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { @@ -243,7 +244,7 @@ function isPointInRange(x, y, range) { } // Scan backward - const {node, offset, content} = TextSourceRange.seekBackward(range.startContainer, range.startOffset, 1); + const {node, offset, content} = new DOMTextScanner(range.startContainer, range.startOffset, true, false).seek(-1); range.setStart(node, offset); if (!isWhitespace(content) && DOM.isPointInAnyRect(x, y, range.getClientRects())) { diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 20771910..3bedfe58 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -17,45 +17,16 @@ /* global * DisplayFloat - * apiForwardLogsToBackend - * apiOptionsGet - * dynamicLoader + * api */ -async function injectPopupNested() { - await dynamicLoader.loadScripts([ - '/mixed/js/text-scanner.js', - '/fg/js/frontend-api-sender.js', - '/fg/js/popup.js', - '/fg/js/popup-proxy.js', - '/fg/js/frontend.js', - '/fg/js/content-script-main.js' - ]); -} - -async function popupNestedInitialize(id, depth, parentFrameId, url) { - let optionsApplied = false; - - const applyOptions = async () => { - const optionsContext = {depth, url}; - const options = await apiOptionsGet(optionsContext); - const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth); - if (maxPopupDepthExceeded || optionsApplied) { return; } - - optionsApplied = true; - yomichan.off('optionsUpdated', applyOptions); - - window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true}; - await injectPopupNested(); - }; - - yomichan.on('optionsUpdated', applyOptions); - - await applyOptions(); -} - (async () => { - apiForwardLogsToBackend(); - const display = new DisplayFloat(); - await display.prepare(); + try { + api.forwardLogsToBackend(); + + const display = new DisplayFloat(); + await display.prepare(); + } catch (e) { + yomichan.logError(e); + } })(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 845bf7f6..d7beb675 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -17,9 +17,10 @@ /* global * Display - * apiBroadcastTab - * apiSendMessageToFrame - * popupNestedInitialize + * Frontend + * PopupFactory + * api + * dynamicLoader */ class DisplayFloat extends Display { @@ -31,7 +32,7 @@ class DisplayFloat extends Display { this._token = null; this._orphaned = false; - this._initializedNestedPopups = false; + this._nestedPopupsPrepared = false; this._onKeyDownHandlers = new Map([ ['C', (e) => { @@ -61,7 +62,7 @@ class DisplayFloat extends Display { yomichan.on('orphaned', this.onOrphaned.bind(this)); window.addEventListener('message', this.onMessage.bind(this), false); - apiBroadcastTab('popupPrepared', {secret: this._secret}); + api.broadcastTab('popupPrepared', {secret: this._secret}); } onError(error) { @@ -153,7 +154,7 @@ class DisplayFloat extends Display { }, 2000 ); - apiBroadcastTab('requestDocumentInformationBroadcast', {uniqueId}); + api.broadcastTab('requestDocumentInformationBroadcast', {uniqueId}); const {title} = await promise; return title; @@ -176,7 +177,7 @@ class DisplayFloat extends Display { const {token, frameId} = params; this._token = token; - apiSendMessageToFrame(frameId, 'popupInitialized', {secret, token}); + api.sendMessageToFrame(frameId, 'popupInitialized', {secret, token}); } async _configure({messageId, frameId, popupId, optionsContext, childrenSupported, scale}) { @@ -184,15 +185,15 @@ class DisplayFloat extends Display { await this.updateOptions(); - if (childrenSupported && !this._initializedNestedPopups) { + if (childrenSupported && !this._nestedPopupsPrepared) { const {depth, url} = optionsContext; - popupNestedInitialize(popupId, depth, frameId, url); - this._initializedNestedPopups = true; + this._prepareNestedPopups(popupId, depth, frameId, url); + this._nestedPopupsPrepared = true; } this.setContentScale(scale); - apiSendMessageToFrame(frameId, 'popupConfigured', {messageId}); + api.sendMessageToFrame(frameId, 'popupConfigured', {messageId}); } _isMessageAuthenticated(message) { @@ -202,4 +203,57 @@ class DisplayFloat extends Display { this._secret === message.secret ); } + + async _prepareNestedPopups(id, depth, parentFrameId, url) { + let complete = false; + + const onOptionsUpdated = async () => { + const optionsContext = this.optionsContext; + const options = await api.optionsGet(optionsContext); + const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth); + if (maxPopupDepthExceeded || complete) { return; } + + complete = true; + yomichan.off('optionsUpdated', onOptionsUpdated); + + try { + await this._setupNestedPopups(id, depth, parentFrameId, url); + } catch (e) { + yomichan.logError(e); + } + }; + + yomichan.on('optionsUpdated', onOptionsUpdated); + + await onOptionsUpdated(); + } + + async _setupNestedPopups(id, depth, parentFrameId, url) { + await dynamicLoader.loadScripts([ + '/mixed/js/text-scanner.js', + '/fg/js/popup.js', + '/fg/js/popup-proxy.js', + '/fg/js/popup-factory.js', + '/fg/js/frame-offset-forwarder.js', + '/fg/js/frontend.js' + ]); + + const {frameId} = await api.frameInformationGet(); + + const popupFactory = new PopupFactory(frameId); + popupFactory.prepare(); + + const frontend = new Frontend( + frameId, + popupFactory, + { + id, + depth, + parentFrameId, + url, + proxy: true + } + ); + await frontend.prepare(); + } } diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 9b68d34e..f692364a 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -16,13 +16,12 @@ */ /* global - * apiBroadcastTab + * api */ class FrameOffsetForwarder { constructor() { - this._started = false; - + this._isPrepared = false; this._cacheMaxSize = 1000; this._frameCache = new Set(); this._unreachableContentWindowCache = new Set(); @@ -38,10 +37,10 @@ class FrameOffsetForwarder { ]); } - start() { - if (this._started) { return; } - window.addEventListener('message', this.onMessage.bind(this), false); - this._started = true; + prepare() { + if (this._isPrepared) { return; } + window.addEventListener('message', this._onMessage.bind(this), false); + this._isPrepared = true; } async getOffset() { @@ -69,11 +68,20 @@ class FrameOffsetForwarder { return offset; } - onMessage(e) { - const {action, params} = e.data; - const handler = this._windowMessageHandlers.get(action); - if (typeof handler !== 'function') { return; } - handler(params, e); + // Private + + _onMessage(event) { + const data = event.data; + if (data === null || typeof data !== 'object') { return; } + + try { + const {action, params} = event.data; + const handler = this._windowMessageHandlers.get(action); + if (typeof handler !== 'function') { return; } + handler(params, event); + } catch (e) { + // NOP + } } _onGetFrameOffset(offset, uniqueId, e) { @@ -161,6 +169,6 @@ class FrameOffsetForwarder { } _forwardFrameOffsetOrigin(offset, uniqueId) { - apiBroadcastTab('frameOffset', {offset, uniqueId}); + api.broadcastTab('frameOffset', {offset, uniqueId}); } } diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js deleted file mode 100644 index 3fa9e8b6..00000000 --- a/ext/fg/js/frontend-api-receiver.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2019-2020 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 FrontendApiReceiver { - constructor(source, messageHandlers) { - this._source = source; - this._messageHandlers = messageHandlers; - } - - prepare() { - chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); - } - - _onConnect(port) { - if (port.name !== 'frontend-api-receiver') { return; } - - port.onMessage.addListener(this._onMessage.bind(this, port)); - } - - _onMessage(port, {id, action, params, target, senderId}) { - if (target !== this._source) { return; } - - const messageHandler = this._messageHandlers.get(action); - if (typeof messageHandler === 'undefined') { return; } - - const {handler, async} = messageHandler; - - this._sendAck(port, id, senderId); - if (async) { - this._invokeHandlerAsync(handler, params, port, id, senderId); - } else { - this._invokeHandler(handler, params, port, id, senderId); - } - } - - _invokeHandler(handler, params, port, id, senderId) { - try { - const result = handler(params); - this._sendResult(port, id, senderId, {result}); - } catch (error) { - this._sendResult(port, id, senderId, {error: errorToJson(error)}); - } - } - - async _invokeHandlerAsync(handler, params, port, id, senderId) { - try { - const result = await handler(params); - this._sendResult(port, id, senderId, {result}); - } catch (error) { - this._sendResult(port, id, senderId, {error: errorToJson(error)}); - } - } - - _sendAck(port, id, senderId) { - port.postMessage({type: 'ack', id, senderId}); - } - - _sendResult(port, id, senderId, data) { - port.postMessage({type: 'result', id, senderId, data}); - } -} diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js deleted file mode 100644 index 4dcde638..00000000 --- a/ext/fg/js/frontend-api-sender.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2019-2020 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 FrontendApiSender { - constructor(target) { - this._target = target; - this._senderId = yomichan.generateId(16); - this._ackTimeout = 3000; // 3 seconds - this._responseTimeout = 10000; // 10 seconds - this._callbacks = new Map(); - this._disconnected = false; - this._nextId = 0; - this._port = null; - } - - invoke(action, params) { - if (this._disconnected) { - // attempt to reconnect the next time - this._disconnected = false; - return Promise.reject(new Error('Disconnected')); - } - - if (this._port === null) { - this._createPort(); - } - - const id = `${this._nextId}`; - ++this._nextId; - - return new Promise((resolve, reject) => { - const info = {id, resolve, reject, ack: false, timer: null}; - this._callbacks.set(id, info); - info.timer = setTimeout(() => this._onError(id, 'Timeout (ack)'), this._ackTimeout); - - this._port.postMessage({id, action, params, target: this._target, senderId: this._senderId}); - }); - } - - _createPort() { - this._port = chrome.runtime.connect(null, {name: 'backend-api-forwarder'}); - this._port.onDisconnect.addListener(this._onDisconnect.bind(this)); - this._port.onMessage.addListener(this._onMessage.bind(this)); - } - - _onMessage({type, id, data, senderId}) { - if (senderId !== this._senderId) { return; } - switch (type) { - case 'ack': - this._onAck(id); - break; - case 'result': - this._onResult(id, data); - break; - } - } - - _onDisconnect() { - this._disconnected = true; - this._port = null; - - for (const id of this._callbacks.keys()) { - this._onError(id, 'Disconnected'); - } - } - - _onAck(id) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { - yomichan.logWarning(new Error(`ID ${id} not found for ack`)); - return; - } - - if (info.ack) { - yomichan.logWarning(new Error(`Request ${id} already ack'd`)); - return; - } - - info.ack = true; - clearTimeout(info.timer); - info.timer = setTimeout(() => this._onError(id, 'Timeout (response)'), this._responseTimeout); - } - - _onResult(id, data) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { - yomichan.logWarning(new Error(`ID ${id} not found`)); - return; - } - - if (!info.ack) { - yomichan.logWarning(new Error(`Request ${id} not ack'd`)); - return; - } - - this._callbacks.delete(id); - clearTimeout(info.timer); - info.timer = null; - - if (typeof data.error !== 'undefined') { - info.reject(jsonToError(data.error)); - } else { - info.resolve(data.result); - } - } - - _onError(id, reason) { - const info = this._callbacks.get(id); - if (typeof info === 'undefined') { return; } - this._callbacks.delete(id); - info.timer = null; - info.reject(new Error(reason)); - } -} diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 575dc413..f6b0d236 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -16,20 +16,18 @@ */ /* global + * DOM + * FrameOffsetForwarder + * PopupProxy * TextScanner - * apiBroadcastTab - * apiGetZoom - * apiKanjiFind - * apiOptionsGet - * apiTermsFind + * api * docSentenceExtract */ class Frontend { - constructor(popup, getUrl=null) { + constructor(frameId, popupFactory, frontendInitializationData) { this._id = yomichan.generateId(16); - this._popup = popup; - this._getUrl = getUrl; + this._popup = null; this._disabledOverride = false; this._options = null; this._pageZoomFactor = 1.0; @@ -41,11 +39,31 @@ class Frontend { this._optionsUpdatePending = false; this._textScanner = new TextScanner({ node: window, - ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getFrame()], - ignorePoint: (x, y) => this._popup.containsPoint(x, y), + ignoreElements: this._ignoreElements.bind(this), + ignorePoint: this._ignorePoint.bind(this), search: this._search.bind(this) }); + const { + depth=0, + id: proxyPopupId, + parentFrameId, + proxy: useProxyPopup=false, + isSearchPage=false, + allowRootFramePopupProxy=true + } = frontendInitializationData; + this._proxyPopupId = proxyPopupId; + this._parentFrameId = parentFrameId; + this._useProxyPopup = useProxyPopup; + this._isSearchPage = isSearchPage; + this._depth = depth; + this._frameId = frameId; + this._frameOffsetForwarder = new FrameOffsetForwarder(); + this._popupFactory = popupFactory; + this._allowRootFramePopupProxy = allowRootFramePopupProxy; + this._popupCache = new Map(); + this._updatePopupToken = null; + this._windowMessageHandlers = new Map([ ['popupClose', this._onMessagePopupClose.bind(this)], ['selectionCopy', this._onMessageSelectionCopy.bind()] @@ -66,39 +84,46 @@ class Frontend { this._textScanner.canClearSelection = value; } + get popup() { + return this._popup; + } + async prepare() { + this._frameOffsetForwarder.prepare(); + + await this.updateOptions(); try { - await this.updateOptions(); - const {zoomFactor} = await apiGetZoom(); + 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) + } - window.addEventListener('resize', this._onResize.bind(this), false); + this._textScanner.prepare(); - const visualViewport = window.visualViewport; - if (visualViewport !== null && typeof visualViewport === 'object') { - window.visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this)); - window.visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this)); - } + window.addEventListener('resize', this._onResize.bind(this), false); + DOM.addFullscreenChangeEventListener(this._updatePopup.bind(this)); - yomichan.on('orphaned', this._onOrphaned.bind(this)); - yomichan.on('optionsUpdated', this.updateOptions.bind(this)); - yomichan.on('zoomChanged', this._onZoomChanged.bind(this)); - chrome.runtime.onMessage.addListener(this._onRuntimeMessage.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)); + } - this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); - this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this)); + yomichan.on('orphaned', this._onOrphaned.bind(this)); + yomichan.on('optionsUpdated', this.updateOptions.bind(this)); + yomichan.on('zoomChanged', this._onZoomChanged.bind(this)); + chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this)); - this._updateContentScale(); - this._broadcastRootPopupInformation(); - } catch (e) { - yomichan.logError(e); - } - } + this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); + this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this)); - async setPopup(popup) { - this._textScanner.clearSelection(true); - this._popup = popup; - await popup.setOptionsContext(await this.getOptionsContext(), this._id); + api.crossFrame.registerHandlers([ + ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] + ]); + + this._updateContentScale(); + this._broadcastRootPopupInformation(); } setDisabledOverride(disabled) { @@ -112,15 +137,26 @@ class Frontend { } async getOptionsContext() { - const url = this._getUrl !== null ? await this._getUrl() : window.location.href; - const depth = this._popup.depth; + let url = window.location.href; + if (this._useProxyPopup) { + try { + url = await api.crossFrame.invoke(this._parentFrameId, 'getUrl', {}); + } catch (e) { + // NOP + } + } + + const depth = this._depth; const modifierKeys = [...this._activeModifiers]; return {depth, url, modifierKeys}; } async updateOptions() { const optionsContext = await this.getOptionsContext(); - this._options = await apiOptionsGet(optionsContext); + this._options = await api.optionsGet(optionsContext); + + await this._updatePopup(); + this._textScanner.setOptions(this._options); this._updateTextScannerEnabled(); @@ -130,8 +166,6 @@ class Frontend { } this._textScanner.ignoreNodes = ignoreNodes.join(','); - await this._popup.setOptionsContext(optionsContext, this._id); - this._updateContentScale(); const textSourceCurrent = this._textScanner.getCurrentTextSource(); @@ -167,6 +201,12 @@ class Frontend { this._broadcastDocumentInformation(uniqueId); } + // API message handlers + + _onApiGetUrl() { + return window.location.href; + } + // Private _onResize() { @@ -223,6 +263,95 @@ class Frontend { await this.updateOptions(); } + async _updatePopup() { + const showIframePopupsInRootFrame = this._options.general.showIframePopupsInRootFrame; + const isIframe = !this._useProxyPopup && (window !== window.parent); + + let popupPromise; + if ( + isIframe && + showIframePopupsInRootFrame && + DOM.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; } + await popup.setOptionsContext(optionsContext, this._id); + if (this._updatePopupToken !== token) { return; } + + if (this._isSearchPage) { + this.setDisabledOverride(!this._options.scanning.enableOnSearchPage); + } + + this._textScanner.clearSelection(true); + this._popup = popup; + this._depth = popup.depth; + } + + async _getDefaultPopup() { + return this._popupFactory.getOrCreatePopup(null, null, this._depth); + } + + async _getProxyPopup() { + const popup = new PopupProxy(null, this._depth + 1, this._proxyPopupId, this._parentFrameId); + await popup.prepare(); + return popup; + } + + async _getIframeProxyPopup() { + const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( + chrome.runtime.onMessage, + ({action, params}, {resolve}) => { + if (action === 'rootPopupInformation') { + resolve(params); + } + } + ); + api.broadcastTab('rootPopupRequestInformationBroadcast'); + const {popupId, frameId: parentFrameId} = await rootPopupInformationPromise; + + const popup = new PopupProxy(popupId, 0, null, parentFrameId, this._frameOffsetForwarder); + popup.on('offsetNotFound', () => { + this._allowRootFramePopupProxy = false; + this._updatePopup(); + }); + await popup.prepare(); + + return popup; + } + + _ignoreElements() { + return this._popup === null || this._popup.isProxy() ? [] : [this._popup.getContainer()]; + } + + _ignorePoint(x, y) { + return this._popup !== null && this._popup.containsPoint(x, y); + } + async _search(textSource, cause) { await this._updatePendingOptions(); @@ -258,32 +387,36 @@ class Frontend { } async _findTerms(textSource, optionsContext) { - const searchText = this._textScanner.getTextSourceContent(textSource, this._options.scanning.length); + const {length: scanLength, layoutAwareScan} = this._options.scanning; + const searchText = this._textScanner.getTextSourceContent(textSource, scanLength, layoutAwareScan); if (searchText.length === 0) { return null; } - const {definitions, length} = await apiTermsFind(searchText, {}, optionsContext); + const {definitions, length} = await api.termsFind(searchText, {}, optionsContext); if (definitions.length === 0) { return null; } - textSource.setEndOffset(length); + textSource.setEndOffset(length, layoutAwareScan); return {definitions, type: 'terms'}; } async _findKanji(textSource, optionsContext) { - const searchText = this._textScanner.getTextSourceContent(textSource, 1); + const layoutAwareScan = this._options.scanning.layoutAwareScan; + const searchText = this._textScanner.getTextSourceContent(textSource, 1, layoutAwareScan); if (searchText.length === 0) { return null; } - const definitions = await apiKanjiFind(searchText, optionsContext); + const definitions = await api.kanjiFind(searchText, optionsContext); if (definitions.length === 0) { return null; } - textSource.setEndOffset(1); + textSource.setEndOffset(1, layoutAwareScan); return {definitions, type: 'kanji'}; } _showContent(textSource, focus, definitions, type, optionsContext) { const {url} = optionsContext; - const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); + const sentenceExtent = this._options.anki.sentenceExt; + const layoutAwareScan = this._options.scanning.layoutAwareScan; + const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); this._showPopupContent( textSource, optionsContext, @@ -314,7 +447,7 @@ class Frontend { _updateTextScannerEnabled() { const enabled = ( this._options.general.enable && - this._popup.depth <= this._options.scanning.popupNestingMaxDepth && + this._depth <= this._options.scanning.popupNestingMaxDepth && !this._disabledOverride ); this._enabledEventListeners.removeAllEventListeners(); @@ -338,27 +471,41 @@ class Frontend { if (contentScale === this._contentScale) { return; } this._contentScale = contentScale; - this._popup.setContentScale(this._contentScale); + if (this._popup !== null) { + this._popup.setContentScale(this._contentScale); + } this._updatePopupPosition(); } async _updatePopupPosition() { const textSource = this._textScanner.getCurrentTextSource(); - if (textSource !== null && await this._popup.isVisible()) { + if ( + textSource !== null && + this._popup !== null && + await this._popup.isVisible() + ) { this._showPopupContent(textSource, await this.getOptionsContext()); } } _broadcastRootPopupInformation() { - if (!this._popup.isProxy() && this._popup.depth === 0 && this._popup.frameId === 0) { - apiBroadcastTab('rootPopupInformation', {popupId: this._popup.id, frameId: this._popup.frameId}); + if ( + this._popup !== null && + !this._popup.isProxy() && + this._depth === 0 && + this._frameId === 0 + ) { + api.broadcastTab('rootPopupInformation', { + popupId: this._popup.id, + frameId: this._frameId + }); } } _broadcastDocumentInformation(uniqueId) { - apiBroadcastTab('documentInformationBroadcast', { + api.broadcastTab('documentInformationBroadcast', { uniqueId, - frameId: this._popup.frameId, + frameId: this._frameId, title: document.title }); } diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js index b10acbaf..904f18b9 100644 --- a/ext/fg/js/popup-factory.js +++ b/ext/fg/js/popup-factory.js @@ -16,8 +16,8 @@ */ /* global - * FrontendApiReceiver * Popup + * api */ class PopupFactory { @@ -28,8 +28,8 @@ class PopupFactory { // Public functions - async prepare() { - const apiReceiver = new FrontendApiReceiver(`popup-factory#${this._frameId}`, new Map([ + prepare() { + api.crossFrame.registerHandlers([ ['getOrCreatePopup', {async: false, handler: this._onApiGetOrCreatePopup.bind(this)}], ['setOptionsContext', {async: true, handler: this._onApiSetOptionsContext.bind(this)}], ['hide', {async: false, handler: this._onApiHide.bind(this)}], @@ -39,10 +39,8 @@ class PopupFactory { ['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)}], - ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] - ])); - apiReceiver.prepare(); + ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}] + ]); } getOrCreatePopup(id=null, parentId=null, depth=null) { @@ -148,10 +146,6 @@ class PopupFactory { return popup.setContentScale(scale); } - _onApiGetUrl() { - return window.location.href; - } - // Private functions _getPopup(id) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 82da839a..a6602eae 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -16,17 +16,17 @@ */ /* global - * FrontendApiSender + * api */ -class PopupProxy { - constructor(id, depth, parentPopupId, parentFrameId, getFrameOffset=null, setDisabled=null) { +class PopupProxy extends EventDispatcher { + constructor(id, depth, parentPopupId, parentFrameId, frameOffsetForwarder=null) { + super(); this._id = id; this._depth = depth; this._parentPopupId = parentPopupId; - this._apiSender = new FrontendApiSender(`popup-factory#${parentFrameId}`); - this._getFrameOffset = getFrameOffset; - this._setDisabled = setDisabled; + this._parentFrameId = parentFrameId; + this._frameOffsetForwarder = frameOffsetForwarder; this._frameOffset = null; this._frameOffsetPromise = null; @@ -75,7 +75,7 @@ class PopupProxy { } async containsPoint(x, y) { - if (this._getFrameOffset !== null) { + if (this._frameOffsetForwarder !== null) { await this._updateFrameOffset(); [x, y] = this._applyFrameOffset(x, y); } @@ -84,7 +84,7 @@ class PopupProxy { async showContent(elementRect, writingMode, type, details, context) { let {x, y, width, height} = elementRect; - if (this._getFrameOffset !== null) { + if (this._frameOffsetForwarder !== null) { await this._updateFrameOffset(); [x, y] = this._applyFrameOffset(x, y); } @@ -104,14 +104,10 @@ class PopupProxy { this._invoke('setContentScale', {id: this._id, scale}); } - async getUrl() { - return await this._invoke('getUrl', {}); - } - // Private _invoke(action, params={}) { - return this._apiSender.invoke(action, params); + return api.crossFrame.invoke(this._parentFrameId, action, params); } async _updateFrameOffset() { @@ -134,12 +130,12 @@ class PopupProxy { } async _updateFrameOffsetInner(now) { - this._frameOffsetPromise = this._getFrameOffset(); + this._frameOffsetPromise = this._frameOffsetForwarder.getOffset(); try { const offset = await this._frameOffsetPromise; this._frameOffset = offset !== null ? offset : [0, 0]; - if (offset === null && this._setDisabled !== null) { - this._setDisabled(); + if (offset === null) { + this.trigger('offsetNotFound'); return; } this._frameOffsetUpdatedAt = now; diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index b7d4b57e..5ee62c9b 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -17,7 +17,7 @@ /* global * DOM - * apiOptionsGet + * api * dynamicLoader */ @@ -47,6 +47,9 @@ class Popup { this._frame.style.width = '0'; this._frame.style.height = '0'; + this._container = this._frame; + this._shadow = null; + this._fullscreenEventListeners = new EventListenerCollection(); } @@ -89,7 +92,7 @@ class Popup { this._optionsContext = optionsContext; this._previousOptionsContextSource = source; - this._options = await apiOptionsGet(optionsContext); + this._options = await api.optionsGet(optionsContext); this.updateTheme(); this._invokeApi('setOptionsContext', {optionsContext}); @@ -180,7 +183,12 @@ class Popup { } async setCustomOuterCss(css, useWebExtensionApi) { - return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi); + let parentNode = null; + if (this._shadow !== null) { + useWebExtensionApi = false; + parentNode = this._shadow; + } + return await dynamicLoader.loadStyle('yomichan-popup-outer-user-stylesheet', 'code', css, useWebExtensionApi, parentNode); } setChildrenSupported(value) { @@ -195,6 +203,10 @@ class Popup { return this._frame.getBoundingClientRect(); } + getContainer() { + return this._container; + } + // Private functions _inject() { @@ -326,14 +338,25 @@ class Popup { } async _createInjectPromise() { - this._injectStyles(); + if (this._options === null) { + throw new Error('Options not initialized'); + } + + const {useSecurePopupFrameUrl, usePopupShadowDom} = this._options.general; + + await this._setUpContainer(usePopupShadowDom); const {secret, token} = await this._initializeFrame(this._frame, this._targetOrigin, this._frameId, (frame) => { frame.removeAttribute('src'); frame.removeAttribute('srcdoc'); this._observeFullscreen(true); this._onFullscreenChanged(); - frame.contentDocument.location.href = chrome.runtime.getURL('/fg/float.html'); + const url = chrome.runtime.getURL('/fg/float.html'); + if (useSecurePopupFrameUrl) { + frame.contentDocument.location.href = url; + } else { + frame.setAttribute('src', url); + } }); this._frameSecret = secret; this._frameToken = token; @@ -371,9 +394,9 @@ class Popup { } _resetFrame() { - const parent = this._frame.parentNode; + const parent = this._container.parentNode; if (parent !== null) { - parent.removeChild(this._frame); + parent.removeChild(this._container); } this._frame.removeAttribute('src'); this._frame.removeAttribute('srcdoc'); @@ -384,9 +407,31 @@ class Popup { 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 dynamicLoader.loadStyle('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); + await this._injectPopupOuterStylesheet(); } catch (e) { // NOP } @@ -398,6 +443,18 @@ class Popup { } } + 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, '/fg/css/client.css', useWebExtensionApi, parentNode); + } + _observeFullscreen(observe) { if (!observe) { this._fullscreenEventListeners.removeAllEventListeners(); @@ -409,22 +466,13 @@ class Popup { return; } - const fullscreenEvents = [ - 'fullscreenchange', - 'MSFullscreenChange', - 'mozfullscreenchange', - 'webkitfullscreenchange' - ]; - const onFullscreenChanged = this._onFullscreenChanged.bind(this); - for (const eventName of fullscreenEvents) { - this._fullscreenEventListeners.addEventListener(document, eventName, onFullscreenChanged, false); - } + DOM.addFullscreenChangeEventListener(this._onFullscreenChanged.bind(this), this._fullscreenEventListeners); } _onFullscreenChanged() { const parent = this._getFrameParentElement(); - if (parent !== null && this._frame.parentNode !== parent) { - parent.appendChild(this._frame); + if (parent !== null && this._container.parentNode !== parent) { + parent.appendChild(this._container); } } diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index fa4706f2..38810f07 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -15,9 +15,9 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -// \u200c (Zero-width non-joiner) appears on Google Docs from Chrome 76 onwards -const IGNORE_TEXT_PATTERN = /\u200c/; - +/* global + * DOMTextScanner + */ /* * TextSourceRange @@ -46,19 +46,19 @@ class TextSourceRange { return this.content; } - setEndOffset(length, fromEnd=false) { + setEndOffset(length, layoutAwareScan, fromEnd=false) { const state = ( fromEnd ? - TextSourceRange.seekForward(this.range.endContainer, this.range.endOffset, length) : - TextSourceRange.seekForward(this.range.startContainer, this.range.startOffset, length) + 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) { - const state = TextSourceRange.seekBackward(this.range.startContainer, this.range.startOffset, length); + 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; @@ -110,154 +110,6 @@ class TextSourceRange { } } - static shouldEnter(node) { - switch (node.nodeName.toUpperCase()) { - case 'RT': - case 'SCRIPT': - case 'STYLE': - return false; - } - - const style = window.getComputedStyle(node); - return !( - style.visibility === 'hidden' || - style.display === 'none' || - parseFloat(style.fontSize) === 0 - ); - } - - static getRubyElement(node) { - node = TextSourceRange.getParentElement(node); - if (node !== null && node.nodeName.toUpperCase() === 'RT') { - node = node.parentNode; - return (node !== null && node.nodeName.toUpperCase() === 'RUBY') ? node : null; - } - return null; - } - - static seekForward(node, offset, length) { - const state = {node, offset, remainder: length, content: ''}; - if (length <= 0) { - return state; - } - - const TEXT_NODE = Node.TEXT_NODE; - const ELEMENT_NODE = Node.ELEMENT_NODE; - let resetOffset = false; - - const ruby = TextSourceRange.getRubyElement(node); - if (ruby !== null) { - node = ruby; - resetOffset = true; - } - - while (node !== null) { - let visitChildren = true; - const nodeType = node.nodeType; - - if (nodeType === TEXT_NODE) { - state.node = node; - if (TextSourceRange.seekForwardTextNode(state, resetOffset)) { - break; - } - resetOffset = true; - } else if (nodeType === ELEMENT_NODE) { - visitChildren = TextSourceRange.shouldEnter(node); - } - - node = TextSourceRange.getNextNode(node, visitChildren); - } - - return state; - } - - static seekForwardTextNode(state, resetOffset) { - const nodeValue = state.node.nodeValue; - const nodeValueLength = nodeValue.length; - let content = state.content; - let offset = resetOffset ? 0 : state.offset; - let remainder = state.remainder; - let result = false; - - for (; offset < nodeValueLength; ++offset) { - const c = nodeValue[offset]; - if (!IGNORE_TEXT_PATTERN.test(c)) { - content += c; - if (--remainder <= 0) { - result = true; - ++offset; - break; - } - } - } - - state.offset = offset; - state.content = content; - state.remainder = remainder; - return result; - } - - static seekBackward(node, offset, length) { - const state = {node, offset, remainder: length, content: ''}; - if (length <= 0) { - return state; - } - - const TEXT_NODE = Node.TEXT_NODE; - const ELEMENT_NODE = Node.ELEMENT_NODE; - let resetOffset = false; - - const ruby = TextSourceRange.getRubyElement(node); - if (ruby !== null) { - node = ruby; - resetOffset = true; - } - - while (node !== null) { - let visitChildren = true; - const nodeType = node.nodeType; - - if (nodeType === TEXT_NODE) { - state.node = node; - if (TextSourceRange.seekBackwardTextNode(state, resetOffset)) { - break; - } - resetOffset = true; - } else if (nodeType === ELEMENT_NODE) { - visitChildren = TextSourceRange.shouldEnter(node); - } - - node = TextSourceRange.getPreviousNode(node, visitChildren); - } - - return state; - } - - static seekBackwardTextNode(state, resetOffset) { - const nodeValue = state.node.nodeValue; - let content = state.content; - let offset = resetOffset ? nodeValue.length : state.offset; - let remainder = state.remainder; - let result = false; - - for (; offset > 0; --offset) { - const c = nodeValue[offset - 1]; - if (!IGNORE_TEXT_PATTERN.test(c)) { - content = c + content; - if (--remainder <= 0) { - result = true; - --offset; - break; - } - } - } - - state.offset = offset; - state.content = content; - state.remainder = remainder; - return result; - } - static getParentElement(node) { while (node !== null && node.nodeType !== Node.ELEMENT_NODE) { node = node.parentNode; @@ -290,66 +142,6 @@ class TextSourceRange { return writingMode; } } - - static getNodesInRange(range) { - const end = range.endContainer; - const nodes = []; - for (let node = range.startContainer; node !== null; node = TextSourceRange.getNextNode(node, true)) { - nodes.push(node); - if (node === end) { break; } - } - return nodes; - } - - static getNextNode(node, visitChildren) { - let next = visitChildren ? node.firstChild : null; - if (next === null) { - while (true) { - next = node.nextSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (next === null) { break; } - - node = next; - } - } - return next; - } - - static getPreviousNode(node, visitChildren) { - let next = visitChildren ? node.lastChild : null; - if (next === null) { - while (true) { - next = node.previousSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (next === null) { break; } - - node = next; - } - } - return next; - } - - static anyNodeMatchesSelector(nodeList, selector) { - for (const node of nodeList) { - if (TextSourceRange.nodeMatchesSelector(node, selector)) { - return true; - } - } - return false; - } - - static nodeMatchesSelector(node, selector) { - for (; node !== null; node = node.parentNode) { - if (node.nodeType === Node.ELEMENT_NODE) { - return node.matches(selector); - } - } - return false; - } } |