diff options
Diffstat (limited to 'ext/fg')
-rw-r--r-- | ext/fg/css/client.css | 4 | ||||
-rw-r--r-- | ext/fg/float.html | 2 | ||||
-rw-r--r-- | ext/fg/js/api.js | 118 | ||||
-rw-r--r-- | ext/fg/js/document.js | 13 | ||||
-rw-r--r-- | ext/fg/js/float.js | 53 | ||||
-rw-r--r-- | ext/fg/js/frontend-api-receiver.js | 21 | ||||
-rw-r--r-- | ext/fg/js/frontend-api-sender.js | 27 | ||||
-rw-r--r-- | ext/fg/js/frontend-initialize.js | 21 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 425 | ||||
-rw-r--r-- | ext/fg/js/popup-nested.js | 5 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy-host.js | 161 | ||||
-rw-r--r-- | ext/fg/js/popup-proxy.js | 111 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 524 | ||||
-rw-r--r-- | ext/fg/js/source.js | 4 |
14 files changed, 568 insertions, 921 deletions
diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css index 633c88ef..b9c59da7 100644 --- a/ext/fg/css/client.css +++ b/ext/fg/css/client.css @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ diff --git a/ext/fg/float.html b/ext/fg/float.html index 67ee50b4..886e5e8b 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -33,8 +33,8 @@ <script src="/mixed/js/core.js"></script> <script src="/mixed/js/dom.js"></script> + <script src="/mixed/js/api.js"></script> - <script src="/fg/js/api.js"></script> <script src="/fg/js/document.js"></script> <script src="/fg/js/source.js"></script> <script src="/mixed/js/audio.js"></script> diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js deleted file mode 100644 index 0e100b59..00000000 --- a/ext/fg/js/api.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2016-2017 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 <http://www.gnu.org/licenses/>. - */ - - -function apiOptionsGet(optionsContext) { - return _apiInvoke('optionsGet', {optionsContext}); -} - -function apiOptionsSet(changedOptions, optionsContext, source) { - return _apiInvoke('optionsSet', {changedOptions, optionsContext, source}); -} - -function apiTermsFind(text, details, optionsContext) { - return _apiInvoke('termsFind', {text, details, optionsContext}); -} - -function apiTextParse(text, optionsContext) { - return _apiInvoke('textParse', {text, optionsContext}); -} - -function apiTextParseMecab(text, optionsContext) { - return _apiInvoke('textParseMecab', {text, optionsContext}); -} - -function apiKanjiFind(text, optionsContext) { - return _apiInvoke('kanjiFind', {text, optionsContext}); -} - -function apiDefinitionAdd(definition, mode, context, optionsContext) { - return _apiInvoke('definitionAdd', {definition, mode, context, optionsContext}); -} - -function apiDefinitionsAddable(definitions, modes, optionsContext) { - return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}).catch(() => null); -} - -function apiNoteView(noteId) { - return _apiInvoke('noteView', {noteId}); -} - -function apiTemplateRender(template, data, dynamic) { - return _apiInvoke('templateRender', {data, template, dynamic}); -} - -function apiAudioGetUrl(definition, source, optionsContext) { - return _apiInvoke('audioGetUrl', {definition, source, optionsContext}); -} - -function apiCommandExec(command, params) { - return _apiInvoke('commandExec', {command, params}); -} - -function apiScreenshotGet(options) { - return _apiInvoke('screenshotGet', {options}); -} - -function apiForward(action, params) { - return _apiInvoke('forward', {action, params}); -} - -function apiFrameInformationGet() { - return _apiInvoke('frameInformationGet'); -} - -function apiInjectStylesheet(css) { - return _apiInvoke('injectStylesheet', {css}); -} - -function apiGetEnvironmentInfo() { - return _apiInvoke('getEnvironmentInfo'); -} - -function apiClipboardGet() { - return _apiInvoke('clipboardGet'); -} - -function _apiInvoke(action, params={}) { - const data = {action, params}; - return new Promise((resolve, reject) => { - try { - chrome.runtime.sendMessage(data, (response) => { - _apiCheckLastError(chrome.runtime.lastError); - if (response !== null && typeof response === 'object') { - if (typeof response.error !== 'undefined') { - reject(jsonToError(response.error)); - } else { - resolve(response.result); - } - } else { - const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; - reject(new Error(`${message} (${JSON.stringify(data)})`)); - } - }); - } catch (e) { - window.yomichan_orphaned = true; - reject(e); - } - }); -} - -function _apiCheckLastError() { - // NOP -} diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 10dea7df..e068e3ba 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -97,15 +97,16 @@ function docImposterCreate(element, isTextarea) { function docElementsFromPoint(x, y, all) { if (all) { - return document.elementsFromPoint(x, y); + // document.elementsFromPoint can return duplicates which must be removed. + const elements = document.elementsFromPoint(x, y); + return elements.filter((e, i) => elements.indexOf(e) === i); } const e = document.elementFromPoint(x, y); return e !== null ? [e] : []; } -function docRangeFromPoint(x, y, options) { - const deepDomScan = options.scanning.deepDomScan; +function docRangeFromPoint(x, y, deepDomScan) { const elements = docElementsFromPoint(x, y, deepDomScan); let imposter = null; let imposterContainer = null; @@ -319,7 +320,7 @@ function disableTransparentElement(elements, i, modifications) { if (isElementTransparent(element)) { const style = element.hasAttribute('style') ? element.getAttribute('style') : null; modifications.push({element, style}); - element.style.pointerEvents = 'none'; + element.style.setProperty('pointer-events', 'none', 'important'); return i; } } diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index ae54be00..513d246b 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -27,17 +27,24 @@ class DisplayFloat extends Display { url: window.location.href }; + this._orphaned = false; + + yomichan.on('orphaned', () => this.onOrphaned()); window.addEventListener('message', (e) => this.onMessage(e), false); } onError(error) { - if (window.yomichan_orphaned) { + if (this._orphaned) { this.setContentOrphaned(); } else { logError(error, true); } } + onOrphaned() { + this._orphaned = true; + } + onSearchClear() { window.parent.postMessage('popupClose', '*'); } @@ -48,24 +55,22 @@ class DisplayFloat extends Display { onMessage(e) { const {action, params} = e.data; - const handlers = DisplayFloat.messageHandlers; - if (hasOwn(handlers, action)) { - const handler = handlers[action]; - handler(this, params); - } + const handler = DisplayFloat._messageHandlers.get(action); + if (typeof handler !== 'function') { return; } + + handler(this, params); } onKeyDown(e) { const key = Display.getKeyFromEvent(e); - const handlers = DisplayFloat.onKeyDownHandlers; - if (hasOwn(handlers, key)) { - const handler = handlers[key]; + const handler = DisplayFloat._onKeyDownHandlers.get(key); + if (typeof handler === 'function') { if (handler(this, e)) { e.preventDefault(); - return; + return true; } } - super.onKeyDown(e); + return super.onKeyDown(e); } getOptionsContext() { @@ -97,21 +102,21 @@ class DisplayFloat extends Display { } } -DisplayFloat.onKeyDownHandlers = { - 'C': (self, e) => { +DisplayFloat._onKeyDownHandlers = new Map([ + ['C', (self, e) => { if (e.ctrlKey && !window.getSelection().toString()) { self.onSelectionCopy(); return true; } return false; - } -}; + }] +]); -DisplayFloat.messageHandlers = { - setContent: (self, {type, details}) => self.setContent(type, details), - clearAutoPlayTimer: (self) => self.clearAutoPlayTimer(), - setCustomCss: (self, {css}) => self.setCustomCss(css), - initialize: (self, {options, popupInfo, url, childrenSupported}) => self.initialize(options, popupInfo, url, childrenSupported) -}; +DisplayFloat._messageHandlers = new Map([ + ['setContent', (self, {type, details}) => self.setContent(type, details)], + ['clearAutoPlayTimer', (self) => self.clearAutoPlayTimer()], + ['setCustomCss', (self, {css}) => self.setCustomCss(css)], + ['initialize', (self, {options, popupInfo, url, childrenSupported}) => self.initialize(options, popupInfo, url, childrenSupported)] +]); -window.yomichan_display = new DisplayFloat(); +DisplayFloat.instance = new DisplayFloat(); diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js index 7d38ddd5..642d96df 100644 --- a/ext/fg/js/frontend-api-receiver.js +++ b/ext/fg/js/frontend-api-receiver.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,14 +13,14 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ class FrontendApiReceiver { - constructor(source='', handlers={}) { - this.source = source; - this.handlers = handlers; + constructor(source='', handlers=new Map()) { + this._source = source; + this._handlers = handlers; chrome.runtime.onConnect.addListener(this.onConnect.bind(this)); } @@ -32,16 +32,13 @@ class FrontendApiReceiver { } onMessage(port, {id, action, params, target, senderId}) { - if ( - target !== this.source || - !hasOwn(this.handlers, action) - ) { - return; - } + if (target !== this._source) { return; } + + const handler = this._handlers.get(action); + if (typeof handler !== 'function') { return; } this.sendAck(port, id, senderId); - const handler = this.handlers[action]; handler(params).then( (result) => { this.sendResult(port, id, senderId, {result}); diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js index af998a8f..93c2e593 100644 --- a/ext/fg/js/frontend-api-sender.js +++ b/ext/fg/js/frontend-api-sender.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -22,7 +22,7 @@ class FrontendApiSender { this.senderId = FrontendApiSender.generateId(16); this.ackTimeout = 3000; // 3 seconds this.responseTimeout = 10000; // 10 seconds - this.callbacks = {}; + this.callbacks = new Map(); this.disconnected = false; this.nextId = 0; @@ -43,7 +43,7 @@ class FrontendApiSender { return new Promise((resolve, reject) => { const info = {id, resolve, reject, ack: false, timer: null}; - this.callbacks[id] = info; + this.callbacks.set(id, info); info.timer = setTimeout(() => this.onError(id, 'Timeout (ack)'), this.ackTimeout); this.port.postMessage({id, action, params, target, senderId: this.senderId}); @@ -71,19 +71,18 @@ class FrontendApiSender { onDisconnect() { this.disconnected = true; - const ids = Object.keys(this.callbacks); - for (const id of ids) { + for (const id of this.callbacks.keys()) { this.onError(id, 'Disconnected'); } } onAck(id) { - if (!hasOwn(this.callbacks, id)) { + const info = this.callbacks.get(id); + if (typeof info === 'undefined') { console.warn(`ID ${id} not found for ack`); return; } - const info = this.callbacks[id]; if (info.ack) { console.warn(`Request ${id} already ack'd`); return; @@ -95,18 +94,18 @@ class FrontendApiSender { } onResult(id, data) { - if (!hasOwn(this.callbacks, id)) { + const info = this.callbacks.get(id); + if (typeof info === 'undefined') { console.warn(`ID ${id} not found`); return; } - const info = this.callbacks[id]; if (!info.ack) { console.warn(`Request ${id} not ack'd`); return; } - delete this.callbacks[id]; + this.callbacks.delete(id); clearTimeout(info.timer); info.timer = null; @@ -118,9 +117,9 @@ class FrontendApiSender { } onError(id, reason) { - if (!hasOwn(this.callbacks, id)) { return; } - const info = this.callbacks[id]; - delete this.callbacks[id]; + 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-initialize.js b/ext/fg/js/frontend-initialize.js index 37a82faa..9c923fea 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,8 +13,23 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -window.yomichan_frontend = Frontend.create(); +async function main() { + const data = window.frontendInitializationData || {}; + const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data; + + let popupHost = null; + if (!proxy) { + popupHost = new PopupProxyHost(); + await popupHost.prepare(); + } + + const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : popupHost.createPopup(null, depth); + const frontend = new Frontend(popup, ignoreNodes); + await frontend.prepare(); +} + +main(); diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 9a1d507b..034d9075 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,221 +13,47 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -class Frontend { +class Frontend extends TextScanner { constructor(popup, ignoreNodes) { + super( + window, + ignoreNodes, + popup.isProxy() ? [] : [popup.getContainer()], + [(x, y) => this.popup.containsPoint(x, y)] + ); + this.popup = popup; - this.popupTimerPromise = null; - this.textSourceCurrent = null; - this.pendingLookup = false; this.options = null; - this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null); this.optionsContext = { depth: popup.depth, url: popup.url }; - this.primaryTouchIdentifier = null; - this.preventNextContextMenu = false; - this.preventNextMouseDown = false; - this.preventNextClick = false; - this.preventScroll = false; - - this.enabled = false; - this.eventListeners = []; - - this.isPreparedPromiseResolve = null; - this.isPreparedPromise = new Promise((resolve) => { this.isPreparedPromiseResolve = resolve; }); - - this.lastShowPromise = Promise.resolve(); - } - - static create() { - const data = window.frontendInitializationData || {}; - const {id, depth=0, parentFrameId, ignoreNodes, url, proxy=false} = data; - - const popup = proxy ? new PopupProxy(depth + 1, id, parentFrameId, url) : PopupProxyHost.instance.createPopup(null, depth); - const frontend = new Frontend(popup, ignoreNodes); - frontend.prepare(); - return frontend; + this._orphaned = true; + this._lastShowPromise = Promise.resolve(); } async prepare() { try { await this.updateOptions(); + yomichan.on('orphaned', () => this.onOrphaned()); + yomichan.on('optionsUpdate', () => this.updateOptions()); chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); - this.isPreparedPromiseResolve(); } catch (e) { this.onError(e); } } - isPrepared() { - return this.isPreparedPromise; - } - - onMouseOver(e) { - if (e.target === this.popup.container) { - this.popupTimerClear(); - } - } - - onMouseMove(e) { - this.popupTimerClear(); - - if (this.pendingLookup || DOM.isMouseButtonDown(e, 'primary')) { - return; - } - - const scanningOptions = this.options.scanning; - const scanningModifier = scanningOptions.modifier; - if (!( - Frontend.isScanningModifierPressed(scanningModifier, e) || - (scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary')) - )) { - return; - } - - const search = async () => { - if (scanningModifier === 'none') { - if (!await this.popupTimerWait()) { - // Aborted - return; - } - } - - await this.searchAt(e.clientX, e.clientY, 'mouse'); - }; - - search(); - } - - onMouseDown(e) { - if (this.preventNextMouseDown) { - this.preventNextMouseDown = false; - this.preventNextClick = true; - e.preventDefault(); - e.stopPropagation(); - return false; - } - - if (e.button === 0) { - this.popupTimerClear(); - this.searchClear(true); - } - } - - onMouseOut() { - this.popupTimerClear(); - } - - onClick(e) { - if (this.preventNextClick) { - this.preventNextClick = false; - e.preventDefault(); - e.stopPropagation(); - return false; - } - } - - onAuxClick() { - this.preventNextContextMenu = false; - } - - onContextMenu(e) { - if (this.preventNextContextMenu) { - this.preventNextContextMenu = false; - e.preventDefault(); - e.stopPropagation(); - return false; - } - } - - onTouchStart(e) { - if (this.primaryTouchIdentifier !== null || e.changedTouches.length === 0) { - return; - } - - this.preventScroll = false; - this.preventNextContextMenu = false; - this.preventNextMouseDown = false; - this.preventNextClick = false; - - const primaryTouch = e.changedTouches[0]; - if (DOM.isPointInSelection(primaryTouch.clientX, primaryTouch.clientY, window.getSelection())) { - return; - } - - this.primaryTouchIdentifier = primaryTouch.identifier; - - if (this.pendingLookup) { - return; - } - - const textSourceCurrentPrevious = this.textSourceCurrent !== null ? this.textSourceCurrent.clone() : null; - - this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchStart') - .then(() => { - if ( - this.textSourceCurrent === null || - this.textSourceCurrent.equals(textSourceCurrentPrevious) - ) { - return; - } - - this.preventScroll = true; - this.preventNextContextMenu = true; - this.preventNextMouseDown = true; - }); - } - - onTouchEnd(e) { - if ( - this.primaryTouchIdentifier === null || - this.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0 - ) { - return; - } - - this.primaryTouchIdentifier = null; - this.preventScroll = false; - this.preventNextClick = false; - // Don't revert context menu and mouse down prevention, - // since these events can occur after the touch has ended. - // this.preventNextContextMenu = false; - // this.preventNextMouseDown = false; - } - - onTouchCancel(e) { - this.onTouchEnd(e); - } - - onTouchMove(e) { - if (!this.preventScroll || !e.cancelable || this.primaryTouchIdentifier === null) { - return; - } - - const touches = e.changedTouches; - const index = this.getIndexOfTouch(touches, this.primaryTouchIdentifier); - if (index < 0) { - return; - } - - const primaryTouch = touches[index]; - this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove'); - - e.preventDefault(); // Disable scroll - } - async onResize() { - if (this.textSourceCurrent !== null && await this.popup.isVisibleAsync()) { - const textSource = this.textSourceCurrent; - this.lastShowPromise = this.popup.showContent( + const textSource = this.textSourceCurrent; + if (textSource !== null && await this.popup.isVisible()) { + this._lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode() ); @@ -236,128 +62,43 @@ class Frontend { onWindowMessage(e) { const action = e.data; - const handlers = Frontend.windowMessageHandlers; - if (hasOwn(handlers, action)) { - const handler = handlers[action]; - handler(this); - } - } - - onRuntimeMessage({action, params}, sender, callback) { - const handlers = Frontend.runtimeMessageHandlers; - if (hasOwn(handlers, action)) { - const handler = handlers[action]; - const result = handler(this, params); - callback(result); - } - } + const handler = Frontend._windowMessageHandlers.get(action); + if (typeof handler !== 'function') { return false; } - onError(error) { - logError(error, false); + handler(this); } - setEnabled(enabled) { - if (enabled) { - if (!this.enabled) { - this.hookEvents(); - this.enabled = true; - } - } else { - if (this.enabled) { - this.clearEventListeners(); - this.enabled = false; - } - this.searchClear(false); - } - } + onRuntimeMessage({action, params}, sender, callback) { + const handler = Frontend._runtimeMessageHandlers.get(action); + if (typeof handler !== 'function') { return false; } - hookEvents() { - this.addEventListener(window, 'message', this.onWindowMessage.bind(this)); - this.addEventListener(window, 'mousedown', this.onMouseDown.bind(this)); - this.addEventListener(window, 'mousemove', this.onMouseMove.bind(this)); - this.addEventListener(window, 'mouseover', this.onMouseOver.bind(this)); - this.addEventListener(window, 'mouseout', this.onMouseOut.bind(this)); - this.addEventListener(window, 'resize', this.onResize.bind(this)); - - if (this.options.scanning.touchInputEnabled) { - this.addEventListener(window, 'click', this.onClick.bind(this)); - this.addEventListener(window, 'auxclick', this.onAuxClick.bind(this)); - this.addEventListener(window, 'touchstart', this.onTouchStart.bind(this)); - this.addEventListener(window, 'touchend', this.onTouchEnd.bind(this)); - this.addEventListener(window, 'touchcancel', this.onTouchCancel.bind(this)); - this.addEventListener(window, 'touchmove', this.onTouchMove.bind(this), {passive: false}); - this.addEventListener(window, 'contextmenu', this.onContextMenu.bind(this)); - } + const result = handler(this, params, sender); + callback(result); + return false; } - addEventListener(node, type, listener, options) { - node.addEventListener(type, listener, options); - this.eventListeners.push([node, type, listener, options]); + onOrphaned() { + this._orphaned = true; } - clearEventListeners() { - for (const [node, type, listener, options] of this.eventListeners) { - node.removeEventListener(type, listener, options); - } - this.eventListeners = []; + getMouseEventListeners() { + return [ + ...super.getMouseEventListeners(), + [window, 'message', this.onWindowMessage.bind(this)], + [window, 'resize', this.onResize.bind(this)] + ]; } async updateOptions() { this.options = await apiOptionsGet(this.getOptionsContext()); - this.setEnabled(this.options.general.enable); await this.popup.setOptions(this.options); + this.setEnabled(this.options.general.enable); } - async popupTimerWait() { - const delay = this.options.scanning.delay; - const promise = promiseTimeout(delay, true); - this.popupTimerPromise = promise; - try { - return await promise; - } finally { - if (this.popupTimerPromise === promise) { - this.popupTimerPromise = null; - } - } - } - - popupTimerClear() { - if (this.popupTimerPromise !== null) { - this.popupTimerPromise.resolve(false); - this.popupTimerPromise = null; - } - } - - async searchAt(x, y, cause) { - try { - this.popupTimerClear(); - - if (this.pendingLookup || await this.popup.containsPoint(x, y)) { - return; - } - - const textSource = docRangeFromPoint(x, y, this.options); - if (this.textSourceCurrent !== null && this.textSourceCurrent.equals(textSource)) { - return; - } - - try { - await this.searchSource(textSource, cause); - } finally { - if (textSource !== null) { - textSource.cleanup(); - } - } - } catch (e) { - this.onError(e); - } - } - - async searchSource(textSource, cause) { + async onSearchSource(textSource, cause) { let results = null; try { - this.pendingLookup = true; if (textSource !== null) { results = ( await this.findTerms(textSource) || @@ -369,9 +110,9 @@ class Frontend { } } } catch (e) { - if (window.yomichan_orphaned) { + if (this._orphaned) { if (textSource !== null && this.options.scanning.modifier !== 'none') { - this.lastShowPromise = this.popup.showContent( + this._lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), 'orphaned' @@ -382,10 +123,8 @@ class Frontend { } } finally { if (results === null && this.options.scanning.autoHideResults) { - this.searchClear(true); + this.onSearchClear(true); } - - this.pendingLookup = false; } return results; @@ -394,17 +133,16 @@ class Frontend { showContent(textSource, focus, definitions, type) { const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); const url = window.location.href; - this.lastShowPromise = this.popup.showContent( + this._lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), type, {definitions, context: {sentence, url, focus, disableHistory: true}} ); + } - this.textSourceCurrent = textSource; - if (this.options.scanning.selectText) { - textSource.select(); - } + showContentCompleted() { + return this._lastShowPromise; } async findTerms(textSource) { @@ -433,82 +171,23 @@ class Frontend { return {definitions, type: 'kanji'}; } - searchClear(changeFocus) { + onSearchClear(changeFocus) { this.popup.hide(changeFocus); this.popup.clearAutoPlayTimer(); - - if (this.textSourceCurrent !== null) { - if (this.options.scanning.selectText) { - this.textSourceCurrent.deselect(); - } - - this.textSourceCurrent = null; - } - } - - getIndexOfTouch(touchList, identifier) { - for (const i in touchList) { - const t = touchList[i]; - if (t.identifier === identifier) { - return i; - } - } - return -1; - } - - setTextSourceScanLength(textSource, length) { - textSource.setEndOffset(length); - if (this.ignoreNodes === null || !textSource.range) { - return; - } - - length = textSource.text().length; - while (textSource.range && length > 0) { - const nodes = TextSourceRange.getNodesInRange(textSource.range); - if (!TextSourceRange.anyNodeMatchesSelector(nodes, this.ignoreNodes)) { - break; - } - --length; - textSource.setEndOffset(length); - } + super.onSearchClear(changeFocus); } getOptionsContext() { this.optionsContext.url = this.popup.url; return this.optionsContext; } - - static isScanningModifierPressed(scanningModifier, mouseEvent) { - switch (scanningModifier) { - case 'alt': return mouseEvent.altKey; - case 'ctrl': return mouseEvent.ctrlKey; - case 'shift': return mouseEvent.shiftKey; - case 'none': return true; - default: return false; - } - } } -Frontend.windowMessageHandlers = { - popupClose: (self) => { - self.searchClear(true); - }, - - selectionCopy: () => { - document.execCommand('copy'); - } -}; - -Frontend.runtimeMessageHandlers = { - optionsUpdate: (self) => { - self.updateOptions(); - }, +Frontend._windowMessageHandlers = new Map([ + ['popupClose', (self) => self.onSearchClear(true)], + ['selectionCopy', () => document.execCommand('copy')] +]); - popupSetVisibleOverride: (self, {visible}) => { - self.popup.setVisibleOverride(visible); - }, - - getUrl: () => { - return {url: window.location.href}; - } -}; +Frontend._runtimeMessageHandlers = new Map([ + ['popupSetVisibleOverride', (self, {visible}) => { self.popup.setVisibleOverride(visible); }] +]); diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index 31cb1cda..bacf3b93 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ @@ -41,6 +41,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true}; const scriptSrcs = [ + '/mixed/js/text-scanner.js', '/fg/js/frontend-api-sender.js', '/fg/js/popup.js', '/fg/js/popup-proxy.js', diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index b2f18b97..c4f0c6ff 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,126 +13,127 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ class PopupProxyHost { constructor() { - this.popups = {}; - this.nextId = 0; - this.apiReceiver = null; - this.frameIdPromise = null; + this._popups = new Map(); + this._nextId = 0; + this._apiReceiver = null; + this._frameIdPromise = null; } - static create() { - const popupProxyHost = new PopupProxyHost(); - popupProxyHost.prepare(); - return popupProxyHost; - } + // Public functions async prepare() { - this.frameIdPromise = apiFrameInformationGet(); - const {frameId} = await this.frameIdPromise; + this._frameIdPromise = apiFrameInformationGet(); + const {frameId} = await this._frameIdPromise; if (typeof frameId !== 'number') { return; } - this.apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, { - createNestedPopup: ({parentId}) => this.createNestedPopup(parentId), - setOptions: ({id, options}) => this.setOptions(id, options), - hide: ({id, changeFocus}) => this.hide(id, changeFocus), - isVisibleAsync: ({id}) => this.isVisibleAsync(id), - setVisibleOverride: ({id, visible}) => this.setVisibleOverride(id, visible), - containsPoint: ({id, x, y}) => this.containsPoint(id, x, y), - showContent: ({id, elementRect, writingMode, type, details}) => this.showContent(id, elementRect, writingMode, type, details), - setCustomCss: ({id, css}) => this.setCustomCss(id, css), - clearAutoPlayTimer: ({id}) => this.clearAutoPlayTimer(id) - }); + this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, new Map([ + ['createNestedPopup', ({parentId}) => this._onApiCreateNestedPopup(parentId)], + ['setOptions', ({id, options}) => this._onApiSetOptions(id, options)], + ['hide', ({id, changeFocus}) => this._onApiHide(id, changeFocus)], + ['isVisible', ({id}) => this._onApiIsVisibleAsync(id)], + ['setVisibleOverride', ({id, visible}) => this._onApiSetVisibleOverride(id, visible)], + ['containsPoint', ({id, x, y}) => this._onApiContainsPoint(id, x, y)], + ['showContent', ({id, elementRect, writingMode, type, details}) => this._onApiShowContent(id, elementRect, writingMode, type, details)], + ['setCustomCss', ({id, css}) => this._onApiSetCustomCss(id, css)], + ['clearAutoPlayTimer', ({id}) => this._onApiClearAutoPlayTimer(id)] + ])); } createPopup(parentId, depth) { - const parent = (typeof parentId === 'string' && hasOwn(this.popups, parentId) ? this.popups[parentId] : null); - const id = `${this.nextId}`; - if (parent !== null) { - depth = parent.depth + 1; - } - ++this.nextId; - const popup = new Popup(id, depth, this.frameIdPromise); - if (parent !== null) { - popup.parent = parent; - parent.child = popup; - } - this.popups[id] = popup; - return popup; - } - - async createNestedPopup(parentId) { - return this.createPopup(parentId, 0).id; + return this._createPopupInternal(parentId, depth).popup; } - getPopup(id) { - if (!hasOwn(this.popups, id)) { - throw new Error('Invalid popup ID'); - } - - return this.popups[id]; - } + // Message handlers - jsonRectToDOMRect(popup, jsonRect) { - let x = jsonRect.x; - let y = jsonRect.y; - if (popup.parent !== null) { - const popupRect = popup.parent.container.getBoundingClientRect(); - x += popupRect.x; - y += popupRect.y; - } - return new DOMRect(x, y, jsonRect.width, jsonRect.height); + async _onApiCreateNestedPopup(parentId) { + return this._createPopupInternal(parentId, 0).id; } - async setOptions(id, options) { - const popup = this.getPopup(id); + async _onApiSetOptions(id, options) { + const popup = this._getPopup(id); return await popup.setOptions(options); } - async hide(id, changeFocus) { - const popup = this.getPopup(id); + async _onApiHide(id, changeFocus) { + const popup = this._getPopup(id); return popup.hide(changeFocus); } - async isVisibleAsync(id) { - const popup = this.getPopup(id); - return await popup.isVisibleAsync(); + async _onApiIsVisibleAsync(id) { + const popup = this._getPopup(id); + return await popup.isVisible(); } - async setVisibleOverride(id, visible) { - const popup = this.getPopup(id); + async _onApiSetVisibleOverride(id, visible) { + const popup = this._getPopup(id); return await popup.setVisibleOverride(visible); } - async containsPoint(id, x, y) { - const popup = this.getPopup(id); + async _onApiContainsPoint(id, x, y) { + const popup = this._getPopup(id); return await popup.containsPoint(x, y); } - async showContent(id, elementRect, writingMode, type, details) { - const popup = this.getPopup(id); - elementRect = this.jsonRectToDOMRect(popup, elementRect); - if (!PopupProxyHost.popupCanShow(popup)) { return Promise.resolve(false); } + async _onApiShowContent(id, elementRect, writingMode, type, details) { + const popup = this._getPopup(id); + elementRect = PopupProxyHost._convertJsonRectToDOMRect(popup, elementRect); + if (!PopupProxyHost._popupCanShow(popup)) { return; } return await popup.showContent(elementRect, writingMode, type, details); } - async setCustomCss(id, css) { - const popup = this.getPopup(id); + async _onApiSetCustomCss(id, css) { + const popup = this._getPopup(id); return popup.setCustomCss(css); } - async clearAutoPlayTimer(id) { - const popup = this.getPopup(id); + async _onApiClearAutoPlayTimer(id) { + const popup = this._getPopup(id); return popup.clearAutoPlayTimer(); } - static popupCanShow(popup) { - return popup.parent === null || popup.parent.isVisible(); + // Private functions + + _createPopupInternal(parentId, depth) { + const parent = (typeof parentId === 'string' && this._popups.has(parentId) ? this._popups.get(parentId) : null); + const id = `${this._nextId}`; + if (parent !== null) { + depth = parent.depth + 1; + } + ++this._nextId; + const popup = new Popup(id, depth, this._frameIdPromise); + if (parent !== null) { + popup.setParent(parent); + } + this._popups.set(id, popup); + return {popup, id}; } -} -PopupProxyHost.instance = PopupProxyHost.create(); + _getPopup(id) { + const popup = this._popups.get(id); + if (typeof popup === 'undefined') { + throw new Error('Invalid popup ID'); + } + return popup; + } + + static _convertJsonRectToDOMRect(popup, jsonRect) { + let x = jsonRect.x; + let y = jsonRect.y; + if (popup.parent !== null) { + const popupRect = popup.parent.getContainerRect(); + x += popupRect.x; + y += popupRect.y; + } + return new DOMRect(x, y, jsonRect.width, jsonRect.height); + } + + static _popupCanShow(popup) { + return popup.parent === null || popup.parent.isVisibleSync(); + } +} diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index e62a4868..ae0cffad 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,94 +13,113 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ class PopupProxy { constructor(depth, parentId, parentFrameId, url) { - this.parentId = parentId; - this.parentFrameId = parentFrameId; - this.id = null; - this.idPromise = null; - this.parent = null; - this.child = null; - this.depth = depth; - this.url = url; + this._parentId = parentId; + this._parentFrameId = parentFrameId; + this._id = null; + this._idPromise = null; + this._depth = depth; + this._url = url; + this._apiSender = new FrontendApiSender(); + } - this.container = null; + // Public properties - this.apiSender = new FrontendApiSender(); + get parent() { + return null; } - getPopupId() { - if (this.idPromise === null) { - this.idPromise = this.getPopupIdAsync(); - } - return this.idPromise; + get depth() { + return this._depth; } - async getPopupIdAsync() { - const id = await this.invokeHostApi('createNestedPopup', {parentId: this.parentId}); - this.id = id; - return id; + get url() { + return this._url; + } + + // Public functions + + isProxy() { + return true; } async setOptions(options) { - const id = await this.getPopupId(); - return await this.invokeHostApi('setOptions', {id, options}); + const id = await this._getPopupId(); + return await this._invokeHostApi('setOptions', {id, options}); } - async hide(changeFocus) { - if (this.id === null) { + hide(changeFocus) { + if (this._id === null) { return; } - return await this.invokeHostApi('hide', {id: this.id, changeFocus}); + this._invokeHostApi('hide', {id: this._id, changeFocus}); } - async isVisibleAsync() { - const id = await this.getPopupId(); - return await this.invokeHostApi('isVisibleAsync', {id}); + async isVisible() { + const id = await this._getPopupId(); + return await this._invokeHostApi('isVisible', {id}); } - async setVisibleOverride(visible) { - const id = await this.getPopupId(); - return await this.invokeHostApi('setVisibleOverride', {id, visible}); + setVisibleOverride(visible) { + if (this._id === null) { + return; + } + this._invokeHostApi('setVisibleOverride', {id, visible}); } async containsPoint(x, y) { - if (this.id === null) { + if (this._id === null) { return false; } - return await this.invokeHostApi('containsPoint', {id: this.id, 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.DOMRectToJson(elementRect); - return await this.invokeHostApi('showContent', {id, elementRect, writingMode, type, details}); + const id = await this._getPopupId(); + elementRect = PopupProxy._convertDOMRectToJson(elementRect); + return await this._invokeHostApi('showContent', {id, elementRect, writingMode, type, details}); } async setCustomCss(css) { - const id = await this.getPopupId(); - return await this.invokeHostApi('setCustomCss', {id, css}); + const id = await this._getPopupId(); + return await this._invokeHostApi('setCustomCss', {id, css}); } - async clearAutoPlayTimer() { - if (this.id === null) { + clearAutoPlayTimer() { + if (this._id === null) { return; } - return await this.invokeHostApi('clearAutoPlayTimer', {id: this.id}); + this._invokeHostApi('clearAutoPlayTimer', {id: this._id}); + } + + // Private + + _getPopupId() { + if (this._idPromise === null) { + this._idPromise = this._getPopupIdAsync(); + } + return this._idPromise; + } + + async _getPopupIdAsync() { + const id = await this._invokeHostApi('createNestedPopup', {parentId: this._parentId}); + this._id = id; + return id; } - invokeHostApi(action, params={}) { - if (typeof this.parentFrameId !== 'number') { + _invokeHostApi(action, params={}) { + if (typeof this._parentFrameId !== 'number') { return Promise.reject(new Error('Invalid frame')); } - return this.apiSender.invoke(action, params, `popup-proxy-host#${this.parentFrameId}`); + return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`); } - static DOMRectToJson(domRect) { + static _convertDOMRectToJson(domRect) { return { x: domRect.x, y: domRect.y, diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 42475d96..7a0c6133 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,100 +13,241 @@ * 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 <http://www.gnu.org/licenses/>. + * along with this program. If not, see <https://www.gnu.org/licenses/>. */ class Popup { constructor(id, depth, frameIdPromise) { - this.id = id; - this.depth = depth; - this.frameIdPromise = frameIdPromise; - this.frameId = null; - this.parent = null; - this.child = null; - this.childrenSupported = true; - this.container = document.createElement('iframe'); - this.container.className = 'yomichan-float'; - this.container.addEventListener('mousedown', (e) => e.stopPropagation()); - this.container.addEventListener('scroll', (e) => e.stopPropagation()); - this.container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); - this.container.style.width = '0px'; - this.container.style.height = '0px'; - this.injectPromise = null; - this.isInjected = false; - this.visible = false; - this.visibleOverride = null; - this.options = null; - this.stylesheetInjectedViaApi = false; - this.updateVisibility(); - } - - inject() { - if (this.injectPromise === null) { - this.injectPromise = this.createInjectPromise(); + this._id = id; + this._depth = depth; + this._frameIdPromise = frameIdPromise; + this._frameId = null; + this._parent = null; + this._child = null; + this._childrenSupported = true; + this._injectPromise = null; + this._isInjected = false; + this._visible = false; + this._visibleOverride = null; + this._options = null; + this._stylesheetInjectedViaApi = false; + + this._container = document.createElement('iframe'); + this._container.className = 'yomichan-float'; + this._container.addEventListener('mousedown', (e) => e.stopPropagation()); + this._container.addEventListener('scroll', (e) => e.stopPropagation()); + this._container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); + this._container.style.width = '0px'; + this._container.style.height = '0px'; + + this._updateVisibility(); + } + + // Public properties + + get parent() { + return this._parent; + } + + get depth() { + return this._depth; + } + + get url() { + return window.location.href; + } + + // Public functions + + isProxy() { + return false; + } + + async setOptions(options) { + this._options = options; + this.updateTheme(); + } + + hide(changeFocus) { + if (!this.isVisibleSync()) { + return; + } + + this._setVisible(false); + if (this._child !== null) { + this._child.hide(false); + } + if (changeFocus) { + this._focusParent(); } - return this.injectPromise; } - async createInjectPromise() { + async isVisible() { + return this.isVisibleSync(); + } + + setVisibleOverride(visible) { + this._visibleOverride = visible; + this._updateVisibility(); + } + + async containsPoint(x, y) { + for (let popup = this; popup !== null && popup.isVisibleSync(); popup = popup._child) { + const rect = popup._container.getBoundingClientRect(); + if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { + return true; + } + } + return false; + } + + async showContent(elementRect, writingMode, type=null, details=null) { + if (!this._isInitialized()) { return; } + await this._show(elementRect, writingMode); + if (type === null) { return; } + this._invokeApi('setContent', {type, details}); + } + + async setCustomCss(css) { + this._invokeApi('setCustomCss', {css}); + } + + clearAutoPlayTimer() { + if (this._isInjected) { + this._invokeApi('clearAutoPlayTimer'); + } + } + + // Popup-only public functions + + setParent(parent) { + if (parent === null) { + throw new Error('Cannot set popup parent to null'); + } + if (this._parent !== null) { + throw new Error('Popup already has a parent'); + } + if (parent._child !== null) { + throw new Error('Cannot parent popup to another popup which already has a child'); + } + this._parent = parent; + parent._child = this; + } + + isVisibleSync() { + return this._isInjected && (this._visibleOverride !== null ? this._visibleOverride : this._visible); + } + + updateTheme() { + this._container.dataset.yomichanTheme = this._options.general.popupOuterTheme; + this._container.dataset.yomichanSiteColor = this._getSiteColor(); + } + + async setCustomOuterCss(css, injectDirectly) { + // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them. + if (this._stylesheetInjectedViaApi) { return; } + + if (injectDirectly || Popup._isOnExtensionPage()) { + Popup.injectOuterStylesheet(css); + } else { + if (!css) { return; } + try { + await apiInjectStylesheet(css); + this._stylesheetInjectedViaApi = true; + } catch (e) { + // NOP + } + } + } + + setChildrenSupported(value) { + this._childrenSupported = value; + } + + getContainer() { + return this._container; + } + + getContainerRect() { + return this._container.getBoundingClientRect(); + } + + static injectOuterStylesheet(css) { + if (Popup.outerStylesheet === null) { + if (!css) { return; } + Popup.outerStylesheet = document.createElement('style'); + Popup.outerStylesheet.id = 'yomichan-popup-outer-stylesheet'; + } + + const outerStylesheet = Popup.outerStylesheet; + if (css) { + outerStylesheet.textContent = css; + + const par = document.head; + if (par && outerStylesheet.parentNode !== par) { + par.appendChild(outerStylesheet); + } + } else { + outerStylesheet.textContent = ''; + } + } + + // Private functions + + _inject() { + if (this._injectPromise === null) { + this._injectPromise = this._createInjectPromise(); + } + return this._injectPromise; + } + + async _createInjectPromise() { try { - const {frameId} = await this.frameIdPromise; + const {frameId} = await this._frameIdPromise; if (typeof frameId === 'number') { - this.frameId = frameId; + this._frameId = frameId; } } catch (e) { // NOP } return new Promise((resolve) => { - const parentFrameId = (typeof this.frameId === 'number' ? this.frameId : null); - this.container.addEventListener('load', () => { - this.invokeApi('initialize', { - options: this.options, + const parentFrameId = (typeof this._frameId === 'number' ? this._frameId : null); + this._container.addEventListener('load', () => { + this._invokeApi('initialize', { + options: this._options, popupInfo: { - id: this.id, - depth: this.depth, + id: this._id, + depth: this._depth, parentFrameId }, url: this.url, - childrenSupported: this.childrenSupported + childrenSupported: this._childrenSupported }); resolve(); }); - this.observeFullscreen(); - this.onFullscreenChanged(); - this.setCustomOuterCss(this.options.general.customPopupOuterCss, false); - this.isInjected = true; + this._observeFullscreen(); + this._onFullscreenChanged(); + this.setCustomOuterCss(this._options.general.customPopupOuterCss, false); + this._isInjected = true; }); } - isInitialized() { - return this.options !== null; + _isInitialized() { + return this._options !== null; } - async setOptions(options) { - this.options = options; - this.updateTheme(); - } + async _show(elementRect, writingMode) { + await this._inject(); - async showContent(elementRect, writingMode, type=null, details=null) { - if (!this.isInitialized()) { return; } - await this.show(elementRect, writingMode); - if (type === null) { return; } - this.invokeApi('setContent', {type, details}); - } - - async show(elementRect, writingMode) { - await this.inject(); - - const optionsGeneral = this.options.general; - const container = this.container; + const optionsGeneral = this._options.general; + const container = this._container; const containerRect = container.getBoundingClientRect(); const getPosition = ( writingMode === 'horizontal-tb' || optionsGeneral.popupVerticalTextPosition === 'default' ? - Popup.getPositionForHorizontalText : - Popup.getPositionForVerticalText + Popup._getPositionForHorizontalText : + Popup._getPositionForVerticalText ); const [x, y, width, height, below] = getPosition( @@ -126,13 +267,78 @@ class Popup { container.style.width = `${width}px`; container.style.height = `${height}px`; - this.setVisible(true); - if (this.child !== null) { - this.child.hide(true); + this._setVisible(true); + if (this._child !== null) { + this._child.hide(true); + } + } + + _setVisible(visible) { + this._visible = visible; + this._updateVisibility(); + } + + _updateVisibility() { + this._container.style.setProperty('visibility', this.isVisibleSync() ? 'visible' : 'hidden', 'important'); + } + + _focusParent() { + if (this._parent !== null) { + // Chrome doesn't like focusing iframe without contentWindow. + const contentWindow = this._parent._container.contentWindow; + if (contentWindow !== null) { + contentWindow.focus(); + } + } else { + // Firefox doesn't like focusing window without first blurring the iframe. + // this.container.contentWindow.blur() doesn't work on Firefox for some reason. + this._container.blur(); + // This is needed for Chrome. + window.focus(); + } + } + + _getSiteColor() { + const color = [255, 255, 255]; + Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor)); + Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.body).backgroundColor)); + const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); + return dark ? 'dark' : 'light'; + } + + _invokeApi(action, params={}) { + this._container.contentWindow.postMessage({action, params}, '*'); + } + + _observeFullscreen() { + const fullscreenEvents = [ + 'fullscreenchange', + 'MSFullscreenChange', + 'mozfullscreenchange', + 'webkitfullscreenchange' + ]; + for (const eventName of fullscreenEvents) { + document.addEventListener(eventName, () => this._onFullscreenChanged(), false); } } - static getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) { + _getFullscreenElement() { + return ( + document.fullscreenElement || + document.msFullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement + ); + } + + _onFullscreenChanged() { + const parent = (this._getFullscreenElement() || document.body || null); + if (parent !== null && this._container.parentNode !== parent) { + parent.appendChild(this._container); + } + } + + static _getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) { let x = elementRect.left + optionsGeneral.popupHorizontalOffset; const overflowX = Math.max(x + width - maxWidth, 0); if (overflowX > 0) { @@ -147,7 +353,7 @@ class Popup { const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const verticalOffset = optionsGeneral.popupVerticalOffset; - const [y, h, below] = Popup.limitGeometry( + const [y, h, below] = Popup._limitGeometry( elementRect.top - verticalOffset, elementRect.bottom + verticalOffset, height, @@ -158,19 +364,19 @@ class Popup { return [x, y, width, h, below]; } - static getPositionForVerticalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral, writingMode) { - const preferRight = Popup.isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); + static _getPositionForVerticalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral, writingMode) { + const preferRight = Popup._isVerticalTextPopupOnRight(optionsGeneral.popupVerticalTextPosition, writingMode); const horizontalOffset = optionsGeneral.popupHorizontalOffset2; const verticalOffset = optionsGeneral.popupVerticalOffset2; - const [x, w] = Popup.limitGeometry( + const [x, w] = Popup._limitGeometry( elementRect.left - horizontalOffset, elementRect.right + horizontalOffset, width, maxWidth, preferRight ); - const [y, h, below] = Popup.limitGeometry( + const [y, h, below] = Popup._limitGeometry( elementRect.bottom - verticalOffset, elementRect.top + verticalOffset, height, @@ -180,12 +386,12 @@ class Popup { return [x, y, w, h, below]; } - static isVerticalTextPopupOnRight(positionPreference, writingMode) { + static _isVerticalTextPopupOnRight(positionPreference, writingMode) { switch (positionPreference) { case 'before': - return !Popup.isWritingModeLeftToRight(writingMode); + return !Popup._isWritingModeLeftToRight(writingMode); case 'after': - return Popup.isWritingModeLeftToRight(writingMode); + return Popup._isWritingModeLeftToRight(writingMode); case 'left': return false; case 'right': @@ -193,7 +399,7 @@ class Popup { } } - static isWritingModeLeftToRight(writingMode) { + static _isWritingModeLeftToRight(writingMode) { switch (writingMode) { case 'vertical-lr': case 'sideways-lr': @@ -203,7 +409,7 @@ class Popup { } } - static limitGeometry(positionBefore, positionAfter, size, limit, preferAfter) { + static _limitGeometry(positionBefore, positionAfter, size, limit, preferAfter) { let after = preferAfter; let position = 0; const overflowBefore = Math.max(0, size - positionBefore); @@ -225,72 +431,7 @@ class Popup { return [position, size, after]; } - hide(changeFocus) { - if (!this.isVisible()) { - return; - } - - this.setVisible(false); - if (this.child !== null) { - this.child.hide(false); - } - if (changeFocus) { - this.focusParent(); - } - } - - async isVisibleAsync() { - return this.isVisible(); - } - - isVisible() { - return this.isInjected && (this.visibleOverride !== null ? this.visibleOverride : this.visible); - } - - setVisible(visible) { - this.visible = visible; - this.updateVisibility(); - } - - setVisibleOverride(visible) { - this.visibleOverride = visible; - this.updateVisibility(); - } - - updateVisibility() { - this.container.style.setProperty('visibility', this.isVisible() ? 'visible' : 'hidden', 'important'); - } - - focusParent() { - if (this.parent !== null) { - // Chrome doesn't like focusing iframe without contentWindow. - const contentWindow = this.parent.container.contentWindow; - if (contentWindow !== null) { - contentWindow.focus(); - } - } else { - // Firefox doesn't like focusing window without first blurring the iframe. - // this.container.contentWindow.blur() doesn't work on Firefox for some reason. - this.container.blur(); - // This is needed for Chrome. - window.focus(); - } - } - - updateTheme() { - this.container.dataset.yomichanTheme = this.options.general.popupOuterTheme; - this.container.dataset.yomichanSiteColor = this.getSiteColor(); - } - - getSiteColor() { - const color = [255, 255, 255]; - Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor)); - Popup.addColor(color, Popup.getColorInfo(window.getComputedStyle(document.body).backgroundColor)); - const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); - return dark ? 'dark' : 'light'; - } - - static addColor(target, color) { + static _addColor(target, color) { if (color === null) { return; } const a = color[3]; @@ -302,7 +443,7 @@ class Popup { } } - static getColorInfo(cssColor) { + static _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; } @@ -315,80 +456,7 @@ class Popup { ]; } - async containsPoint(x, y) { - for (let popup = this; popup !== null && popup.isVisible(); popup = popup.child) { - const rect = popup.container.getBoundingClientRect(); - if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { - return true; - } - } - return false; - } - - async setCustomCss(css) { - this.invokeApi('setCustomCss', {css}); - } - - async setCustomOuterCss(css, injectDirectly) { - // Cannot repeatedly inject stylesheets using web extension APIs since there is no way to remove them. - if (this.stylesheetInjectedViaApi) { return; } - - if (injectDirectly || Popup.isOnExtensionPage()) { - Popup.injectOuterStylesheet(css); - } else { - if (!css) { return; } - try { - await apiInjectStylesheet(css); - this.stylesheetInjectedViaApi = true; - } catch (e) { - // NOP - } - } - } - - clearAutoPlayTimer() { - if (this.isInjected) { - this.invokeApi('clearAutoPlayTimer'); - } - } - - invokeApi(action, params={}) { - this.container.contentWindow.postMessage({action, params}, '*'); - } - - observeFullscreen() { - const fullscreenEvents = [ - 'fullscreenchange', - 'MSFullscreenChange', - 'mozfullscreenchange', - 'webkitfullscreenchange' - ]; - for (const eventName of fullscreenEvents) { - document.addEventListener(eventName, () => this.onFullscreenChanged(), false); - } - } - - getFullscreenElement() { - return ( - document.fullscreenElement || - document.msFullscreenElement || - document.mozFullScreenElement || - document.webkitFullscreenElement - ); - } - - onFullscreenChanged() { - const parent = (this.getFullscreenElement() || document.body || null); - if (parent !== null && this.container.parentNode !== parent) { - parent.appendChild(this.container); - } - } - - get url() { - return window.location.href; - } - - static isOnExtensionPage() { + static _isOnExtensionPage() { try { const url = chrome.runtime.getURL('/'); return window.location.href.substring(0, url.length) === url; @@ -396,26 +464,6 @@ class Popup { // NOP } } - - static injectOuterStylesheet(css) { - if (Popup.outerStylesheet === null) { - if (!css) { return; } - Popup.outerStylesheet = document.createElement('style'); - Popup.outerStylesheet.id = 'yomichan-popup-outer-stylesheet'; - } - - const outerStylesheet = Popup.outerStylesheet; - if (css) { - outerStylesheet.textContent = css; - - const par = document.head; - if (par && outerStylesheet.parentNode !== par) { - par.appendChild(outerStylesheet); - } - } else { - outerStylesheet.textContent = ''; - } - } } Popup.outerStylesheet = null; diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index a84feed4..5cdf47b5 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net> * Author: Alex Yatskov <alex@foosoft.net> * * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@ * 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 <http://www.gnu.org/licenses/>. + * 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 |