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 |