diff options
Diffstat (limited to 'ext/fg/js')
-rw-r--r-- | ext/fg/js/float.js | 44 | ||||
-rw-r--r-- | ext/fg/js/frame-offset-forwarder.js | 102 | ||||
-rw-r--r-- | ext/fg/js/frontend-initialize.js | 34 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 38 | ||||
-rw-r--r-- | ext/fg/js/popup-nested.js | 7 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy-host.js | 15 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy.js | 108 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 87 |
8 files changed, 312 insertions, 123 deletions
diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 393c2719..01055ca6 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -28,6 +28,8 @@ class DisplayFloat extends Display { super(document.querySelector('#spinner'), document.querySelector('#definitions')); this.autoPlayAudioTimer = null; + this._popupId = null; + this.optionsContext = { depth: 0, url: window.location.href @@ -53,7 +55,7 @@ class DisplayFloat extends Display { ['setContent', ({type, details}) => this.setContent(type, details)], ['clearAutoPlayTimer', () => this.clearAutoPlayTimer()], ['setCustomCss', ({css}) => this.setCustomCss(css)], - ['prepare', ({options, popupInfo, url, childrenSupported, scale, uniqueId}) => this.prepare(options, popupInfo, url, childrenSupported, scale, uniqueId)], + ['prepare', ({popupInfo, url, childrenSupported, scale}) => this.prepare(popupInfo, url, childrenSupported, scale)], ['setContentScale', ({scale}) => this.setContentScale(scale)] ]); @@ -61,23 +63,24 @@ class DisplayFloat extends Display { window.addEventListener('message', this.onMessage.bind(this), false); } - async prepare(options, popupInfo, url, childrenSupported, scale, uniqueId) { + async prepare(popupInfo, url, childrenSupported, scale) { if (this._prepareInvoked) { return; } this._prepareInvoked = true; - await super.prepare(options); - const {id, depth, parentFrameId} = popupInfo; + this._popupId = id; this.optionsContext.depth = depth; this.optionsContext.url = url; + await super.prepare(); + if (childrenSupported) { popupNestedInitialize(id, depth, parentFrameId, url); } this.setContentScale(scale); - apiForward('popupPrepareCompleted', {uniqueId}); + apiForward('popupPrepareCompleted', {targetPopupId: this._popupId}); } onError(error) { @@ -144,10 +147,6 @@ class DisplayFloat extends Display { handler(params); } - getOptionsContext() { - return this.optionsContext; - } - autoPlayAudio() { this.clearAutoPlayTimer(); this.autoPlayAudioTimer = window.setTimeout(() => super.autoPlayAudio(), 400); @@ -163,6 +162,33 @@ class DisplayFloat extends Display { setContentScale(scale) { document.body.style.fontSize = `${scale}em`; } + + async getDocumentTitle() { + try { + const uniqueId = yomichan.generateId(16); + + const promise = yomichan.getTemporaryListenerResult( + chrome.runtime.onMessage, + ({action, params}, {resolve}) => { + if ( + action === 'documentInformationBroadcast' && + isObject(params) && + params.uniqueId === uniqueId && + params.frameId === 0 + ) { + resolve(params); + } + }, + 2000 + ); + apiForward('requestDocumentInformationBroadcast', {uniqueId}); + + const {title} = await promise; + return title; + } catch (e) { + return ''; + } + } } DisplayFloat.instance = new DisplayFloat(); diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js new file mode 100644 index 00000000..7b417b6e --- /dev/null +++ b/ext/fg/js/frame-offset-forwarder.js @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 + * apiForward + */ + +class FrameOffsetForwarder { + constructor() { + this._started = false; + + this._forwardFrameOffset = ( + window !== window.parent ? + this._forwardFrameOffsetParent.bind(this) : + this._forwardFrameOffsetOrigin.bind(this) + ); + + this._windowMessageHandlers = new Map([ + ['getFrameOffset', ({offset, uniqueId}, e) => this._onGetFrameOffset(offset, uniqueId, e)] + ]); + } + + start() { + if (this._started) { return; } + window.addEventListener('message', this.onMessage.bind(this), false); + this._started = true; + } + + async getOffset() { + const uniqueId = yomichan.generateId(16); + + const frameOffsetPromise = yomichan.getTemporaryListenerResult( + chrome.runtime.onMessage, + ({action, params}, {resolve}) => { + if (action === 'frameOffset' && isObject(params) && params.uniqueId === uniqueId) { + resolve(params); + } + }, + 5000 + ); + + window.parent.postMessage({ + action: 'getFrameOffset', + params: { + uniqueId, + offset: [0, 0] + } + }, '*'); + + const {offset} = await frameOffsetPromise; + return offset; + } + + onMessage(e) { + const {action, params} = e.data; + const handler = this._windowMessageHandlers.get(action); + if (typeof handler !== 'function') { return; } + handler(params, e); + } + + _onGetFrameOffset(offset, uniqueId, e) { + let sourceFrame = null; + for (const frame of document.querySelectorAll('frame, iframe:not(.yomichan-float)')) { + if (frame.contentWindow !== e.source) { continue; } + sourceFrame = frame; + break; + } + if (sourceFrame === null) { + this._forwardFrameOffsetOrigin(null, uniqueId); + return; + } + + const [forwardedX, forwardedY] = offset; + const {x, y} = sourceFrame.getBoundingClientRect(); + offset = [forwardedX + x, forwardedY + y]; + + this._forwardFrameOffset(offset, uniqueId); + } + + _forwardFrameOffsetParent(offset, uniqueId) { + window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); + } + + _forwardFrameOffsetOrigin(offset, uniqueId) { + apiForward('frameOffset', {offset, uniqueId}); + } +} diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 8424b21d..4a1409db 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -17,28 +17,56 @@ */ /* global + * FrameOffsetForwarder * Frontend * PopupProxy * PopupProxyHost + * apiForward + * apiOptionsGet */ async function main() { await yomichan.prepare(); const data = window.frontendInitializationData || {}; - const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data; + const {id, depth=0, parentFrameId, url, proxy=false} = data; + + const optionsContext = {depth, url}; + const options = await apiOptionsGet(optionsContext); let popup; - if (proxy) { + if (!proxy && (window !== window.parent) && options.general.showIframePopupsInRootFrame) { + const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( + chrome.runtime.onMessage, + ({action, params}, {resolve}) => { + if (action === 'rootPopupInformation') { + resolve(params); + } + } + ); + apiForward('rootPopupRequestInformationBroadcast'); + const {popupId, frameId} = await rootPopupInformationPromise; + + const frameOffsetForwarder = new FrameOffsetForwarder(); + frameOffsetForwarder.start(); + const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); + + popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset); + await popup.prepare(); + } else if (proxy) { popup = new PopupProxy(null, depth + 1, id, parentFrameId, url); + await popup.prepare(); } else { + const frameOffsetForwarder = new FrameOffsetForwarder(); + frameOffsetForwarder.start(); + const popupHost = new PopupProxyHost(); await popupHost.prepare(); popup = popupHost.getOrCreatePopup(null, null, depth); } - const frontend = new Frontend(popup, ignoreNodes); + const frontend = new Frontend(popup); await frontend.prepare(); } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 768b9326..31843212 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -18,6 +18,7 @@ /* global * TextScanner + * apiForward * apiGetZoom * apiKanjiFind * apiOptionsGet @@ -26,10 +27,9 @@ */ class Frontend extends TextScanner { - constructor(popup, ignoreNodes) { + constructor(popup) { super( window, - ignoreNodes, popup.isProxy() ? [] : [popup.getContainer()], [(x, y) => this.popup.containsPoint(x, y)] ); @@ -53,7 +53,9 @@ class Frontend extends TextScanner { ]); this._runtimeMessageHandlers = new Map([ - ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }] + ['popupSetVisibleOverride', ({visible}) => { this.popup.setVisibleOverride(visible); }], + ['rootPopupRequestInformationBroadcast', () => { this._broadcastRootPopupInformation(); }], + ['requestDocumentInformationBroadcast', ({uniqueId}) => { this._broadcastDocumentInformation(uniqueId); }] ]); } @@ -77,6 +79,7 @@ class Frontend extends TextScanner { chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); this._updateContentScale(); + this._broadcastRootPopupInformation(); } catch (e) { this.onError(e); } @@ -95,6 +98,9 @@ class Frontend extends TextScanner { } onRuntimeMessage({action, params}, sender, callback) { + const {targetPopupId} = params || {}; + if (typeof targetPopupId !== 'undefined' && targetPopupId !== this.popup.id) { return; } + const handler = this._runtimeMessageHandlers.get(action); if (typeof handler !== 'function') { return false; } @@ -129,8 +135,20 @@ class Frontend extends TextScanner { async updateOptions() { this.setOptions(await apiOptionsGet(this.getOptionsContext())); + + const ignoreNodes = ['.scan-disable', '.scan-disable *']; + if (!this.options.scanning.enableOnPopupExpressions) { + ignoreNodes.push('.source-text', '.source-text *'); + } + this.ignoreNodes = ignoreNodes.join(','); + await this.popup.setOptions(this.options); + this._updateContentScale(); + + if (this.textSourceCurrent !== null && this.causeCurrent !== null) { + await this.onSearchSource(this.textSourceCurrent, this.causeCurrent); + } } async onSearchSource(textSource, cause) { @@ -241,6 +259,20 @@ class Frontend extends TextScanner { this._updatePopupPosition(); } + _broadcastRootPopupInformation() { + if (!this.popup.isProxy() && this.popup.depth === 0) { + apiForward('rootPopupInformation', {popupId: this.popup.id, frameId: this.popup.frameId}); + } + } + + _broadcastDocumentInformation(uniqueId) { + apiForward('documentInformationBroadcast', { + uniqueId, + frameId: this.popup.frameId, + title: document.title + }); + } + async _updatePopupPosition() { const textSource = this.getCurrentTextSource(); if (textSource !== null && await this.popup.isVisible()) { diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index 06f8fc4b..39d91fd8 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -36,12 +36,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { return; } - const ignoreNodes = ['.scan-disable', '.scan-disable *']; - if (!options.scanning.enableOnPopupExpressions) { - ignoreNodes.push('.source-text', '.source-text *'); - } - - window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true}; + window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true}; const scriptSrcs = [ '/mixed/js/text-scanner.js', diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 793d3949..4b136e41 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -25,19 +25,18 @@ class PopupProxyHost { constructor() { this._popups = new Map(); - this._nextId = 0; this._apiReceiver = null; - this._frameIdPromise = null; + this._frameId = null; } // Public functions async prepare() { - this._frameIdPromise = apiFrameInformationGet(); - const {frameId} = await this._frameIdPromise; + const {frameId} = await apiFrameInformationGet(); if (typeof frameId !== 'number') { return; } + this._frameId = frameId; - this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([ + this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${this._frameId}`, new Map([ ['getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)], ['setOptions', this._onApiSetOptions.bind(this)], ['hide', this._onApiHide.bind(this)], @@ -76,7 +75,7 @@ class PopupProxyHost { // New unique id if (id === null) { - id = this._nextId++; + id = yomichan.generateId(16); } // Create new popup @@ -88,7 +87,7 @@ class PopupProxyHost { } else if (depth === null) { depth = 0; } - const popup = new Popup(id, depth, this._frameIdPromise); + const popup = new Popup(id, depth, this._frameId); if (parent !== null) { popup.setParent(parent); } @@ -96,7 +95,7 @@ class PopupProxyHost { return popup; } - // Message handlers + // API message handlers async _onApiGetOrCreatePopup({id, parentId}) { const popup = this.getOrCreatePopup(id, parentId); diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index f7cef214..966198a9 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -21,18 +21,26 @@ */ class PopupProxy { - constructor(id, depth, parentId, parentFrameId, url) { + constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null) { this._parentId = parentId; this._parentFrameId = parentFrameId; this._id = id; - this._idPromise = null; this._depth = depth; this._url = url; this._apiSender = new FrontendApiSender(); + this._getFrameOffset = getFrameOffset; + + this._frameOffset = null; + this._frameOffsetPromise = null; + this._frameOffsetUpdatedAt = null; } // Public properties + get id() { + return this._id; + } + get parent() { return null; } @@ -47,92 +55,106 @@ class PopupProxy { // Public functions + async prepare() { + const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId}); + this._id = id; + } + isProxy() { return true; } async setOptions(options) { - const id = await this._getPopupId(); - return await this._invokeHostApi('setOptions', {id, options}); + return await this._invokeHostApi('setOptions', {id: this._id, options}); } hide(changeFocus) { - if (this._id === null) { - return; - } this._invokeHostApi('hide', {id: this._id, changeFocus}); } async isVisible() { - const id = await this._getPopupId(); - return await this._invokeHostApi('isVisible', {id}); + return await this._invokeHostApi('isVisible', {id: this._id}); } setVisibleOverride(visible) { - if (this._id === null) { - return; - } this._invokeHostApi('setVisibleOverride', {id: this._id, visible}); } async containsPoint(x, y) { - if (this._id === null) { - return false; + if (this._getFrameOffset !== null) { + await this._updateFrameOffset(); + [x, y] = this._applyFrameOffset(x, y); } return await this._invokeHostApi('containsPoint', {id: this._id, x, y}); } async showContent(elementRect, writingMode, type=null, details=null) { - const id = await this._getPopupId(); - elementRect = PopupProxy._convertDOMRectToJson(elementRect); - return await this._invokeHostApi('showContent', {id, elementRect, writingMode, type, details}); + let {x, y, width, height} = elementRect; + if (this._getFrameOffset !== null) { + await this._updateFrameOffset(); + [x, y] = this._applyFrameOffset(x, y); + } + elementRect = {x, y, width, height}; + return await this._invokeHostApi('showContent', {id: this._id, elementRect, writingMode, type, details}); } async setCustomCss(css) { - const id = await this._getPopupId(); - return await this._invokeHostApi('setCustomCss', {id, css}); + return await this._invokeHostApi('setCustomCss', {id: this._id, css}); } clearAutoPlayTimer() { - if (this._id === null) { - return; - } this._invokeHostApi('clearAutoPlayTimer', {id: this._id}); } async setContentScale(scale) { - const id = await this._getPopupId(); - this._invokeHostApi('setContentScale', {id, scale}); + this._invokeHostApi('setContentScale', {id: this._id, scale}); } // Private - _getPopupId() { - if (this._idPromise === null) { - this._idPromise = this._getPopupIdAsync(); + _invokeHostApi(action, params={}) { + if (typeof this._parentFrameId !== 'number') { + return Promise.reject(new Error('Invalid frame')); } - return this._idPromise; + return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`); } - async _getPopupIdAsync() { - const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId}); - this._id = id; - return id; + async _updateFrameOffset() { + const now = Date.now(); + const firstRun = this._frameOffsetUpdatedAt === null; + const expired = firstRun || this._frameOffsetUpdatedAt < now - PopupProxy._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; + } } - _invokeHostApi(action, params={}) { - if (typeof this._parentFrameId !== 'number') { - return Promise.reject(new Error('Invalid frame')); + async _updateFrameOffsetInner(now) { + this._frameOffsetPromise = this._getFrameOffset(); + try { + const offset = await this._frameOffsetPromise; + this._frameOffset = offset !== null ? offset : [0, 0]; + this._frameOffsetUpdatedAt = now; + } catch (e) { + logError(e); + } finally { + this._frameOffsetPromise = null; } - return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`); } - static _convertDOMRectToJson(domRect) { - return { - x: domRect.x, - y: domRect.y, - width: domRect.width, - height: domRect.height - }; + _applyFrameOffset(x, y) { + const [offsetX, offsetY] = this._frameOffset; + return [x + offsetX, y + offsetY]; } } + +PopupProxy._frameOffsetExpireTimeout = 1000; diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index d752812e..60dc16dd 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -22,11 +22,10 @@ */ class Popup { - constructor(id, depth, frameIdPromise) { + constructor(id, depth, frameId) { this._id = id; this._depth = depth; - this._frameIdPromise = frameIdPromise; - this._frameId = null; + this._frameId = frameId; this._parent = null; this._child = null; this._childrenSupported = true; @@ -69,6 +68,10 @@ class Popup { return this._depth; } + get frameId() { + return this._frameId; + } + get url() { return window.location.href; } @@ -193,43 +196,42 @@ class Popup { } async _createInjectPromise() { - try { - const {frameId} = await this._frameIdPromise; - if (typeof frameId === 'number') { - this._frameId = frameId; - } - } catch (e) { - // NOP - } - if (this._messageToken === null) { this._messageToken = await apiGetMessageToken(); } - return new Promise((resolve) => { - const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); - this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); - this._container.addEventListener('load', () => { - const uniqueId = yomichan.generateId(32); - Popup._listenForDisplayPrepareCompleted(uniqueId, resolve); - - this._invokeApi('prepare', { - options: this._options, - popupInfo: { - id: this._id, - depth: this._depth, - parentFrameId - }, - url: this.url, - childrenSupported: this._childrenSupported, - scale: this._contentScale, - uniqueId - }); + const popupPreparedPromise = yomichan.getTemporaryListenerResult( + chrome.runtime.onMessage, + ({action, params}, {resolve}) => { + if ( + action === 'popupPrepareCompleted' && + isObject(params) && + params.targetPopupId === this._id + ) { + resolve(); + } + } + ); + + const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); + this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); + this._container.addEventListener('load', () => { + this._invokeApi('prepare', { + popupInfo: { + id: this._id, + depth: this._depth, + parentFrameId + }, + url: this.url, + childrenSupported: this._childrenSupported, + scale: this._contentScale }); - this._observeFullscreen(true); - this._onFullscreenChanged(); - this._injectStyles(); }); + this._observeFullscreen(true); + this._onFullscreenChanged(); + this._injectStyles(); + + return popupPreparedPromise; } async _injectStyles() { @@ -374,23 +376,6 @@ class Popup { ); } - static _listenForDisplayPrepareCompleted(uniqueId, resolve) { - const runtimeMessageCallback = ({action, params}, sender, callback) => { - if ( - action === 'popupPrepareCompleted' && - typeof params === 'object' && - params !== null && - params.uniqueId === uniqueId - ) { - chrome.runtime.onMessage.removeListener(runtimeMessageCallback); - callback(); - resolve(); - return false; - } - }; - chrome.runtime.onMessage.addListener(runtimeMessageCallback); - } - static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; |