diff options
| -rw-r--r-- | ext/bg/js/search.js | 1 | ||||
| -rw-r--r-- | ext/fg/float.html | 1 | ||||
| -rw-r--r-- | ext/fg/js/float-main.js | 5 | ||||
| -rw-r--r-- | ext/fg/js/float.js | 184 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 272 | 
5 files changed, 211 insertions, 252 deletions
| diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index e68dccfd..498f4ade 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -56,6 +56,7 @@ class DisplaySearch extends Display {              ['AltGraph', new Set()],              ['Shift', new Set()]          ]); +        this.autoPlayAudioDelay = 0;      }      async prepare() { diff --git a/ext/fg/float.html b/ext/fg/float.html index 4b1f55e1..36aedb7a 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -96,7 +96,6 @@  <script src="/bg/js/template-renderer-proxy.js"></script>  <script src="/bg/js/query-parser.js"></script> -<script src="/fg/js/float.js"></script>  <script src="/fg/js/float-main.js"></script> diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 85f605a2..6b4daebb 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -16,7 +16,7 @@   */  /* global - * DisplayFloat + * Display   * api   */ @@ -25,8 +25,9 @@          api.forwardLogsToBackend();          await yomichan.backendReady(); -        const display = new DisplayFloat(); +        const display = new Display('popup');          await display.prepare(); +        display.initializeState();          yomichan.ready();      } catch (e) { diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js deleted file mode 100644 index 195861a3..00000000 --- a/ext/fg/js/float.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (C) 2016-2020  Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program.  If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * Display - * FrameEndpoint - * api - */ - -class DisplayFloat extends Display { -    constructor() { -        super('popup'); -        this._frameEndpoint = new FrameEndpoint(); -        this._windowMessageHandlers = new Map([ -            ['extensionUnloaded', {async: false, handler: this._onMessageExtensionUnloaded.bind(this)}] -        ]); -        this._browser = null; -        this._copyTextarea = null; - -        this.registerActions([ -            ['copyHostSelection', () => this._copySelection()] -        ]); -        this.registerHotkeys([ -            {key: 'C', modifiers: ['ctrl'], action: 'copyHostSelection'} -        ]); - -        this.autoPlayAudioDelay = 400; -    } - -    async prepare() { -        await super.prepare(); - -        const {browser} = await api.getEnvironmentInfo(); -        this._browser = browser; - -        window.addEventListener('message', this._onWindowMessage.bind(this), false); -        document.documentElement.addEventListener('mouseup', this._onMouseUp.bind(this), false); -        document.documentElement.addEventListener('click', this._onClick.bind(this), false); -        document.documentElement.addEventListener('auxclick', this._onClick.bind(this), false); - -        this.initializeState(); - -        this._frameEndpoint.signal(); -    } - -    onEscape() { -        this.close(); -    } - -    async getDocumentTitle() { -        try { -            const targetFrameId = 0; -            const {title} = await api.crossFrame.invoke(targetFrameId, 'getDocumentInformation'); -            return title; -        } catch (e) { -            return ''; -        } -    } - -    authenticateMessageData(data) { -        if (!this._frameEndpoint.authenticate(data)) { -            throw new Error('Invalid authentication'); -        } -        return data.data; -    } - -    close() { -        this._invokeOwner('closePopup'); -    } - -    // Message handling - -    _onWindowMessage(e) { -        const data = e.data; -        if (!this._frameEndpoint.authenticate(data)) { return; } - -        const {action, params} = data.data; -        const messageHandler = this._windowMessageHandlers.get(action); -        if (typeof messageHandler === 'undefined') { return; } - -        const callback = () => {}; // NOP -        yomichan.invokeMessageHandler(messageHandler, params, callback); -    } - -    _onMessageExtensionUnloaded() { -        if (yomichan.isExtensionUnloaded) { return; } -        yomichan.triggerExtensionUnloaded(); -    } - -    // Private - -    _onMouseUp(e) { -        switch (e.button) { -            case 3: // Back -                if (this._history.hasPrevious()) { -                    e.preventDefault(); -                } -                break; -            case 4: // Forward -                if (this._history.hasNext()) { -                    e.preventDefault(); -                } -                break; -        } -    } - -    _onClick(e) { -        switch (e.button) { -            case 3: // Back -                if (this._history.hasPrevious()) { -                    e.preventDefault(); -                    this._history.back(); -                } -                break; -            case 4: // Forward -                if (this._history.hasNext()) { -                    e.preventDefault(); -                    this._history.forward(); -                } -                break; -        } -    } - -    _copySelection() { -        if (window.getSelection().toString()) { return false; } -        this._copyHostSelection(); -        return true; -    } - -    async _copyHostSelection() { -        switch (this._browser) { -            case 'firefox': -            case 'firefox-mobile': -                { -                    let text; -                    try { -                        text = await this._invokeOwner('getSelectionText'); -                    } catch (e) { -                        break; -                    } -                    this._copyText(text); -                } -                break; -            default: -                this._invokeOwner('copySelection'); -                break; -        } -    } - -    _copyText(text) { -        const parent = document.body; -        if (parent === null) { return; } - -        let textarea = this._copyTextarea; -        if (textarea === null) { -            textarea = document.createElement('textarea'); -            this._copyTextarea = textarea; -        } - -        textarea.value = text; -        parent.appendChild(textarea); -        textarea.select(); -        document.execCommand('copy'); -        parent.removeChild(textarea); -    } - -    _invokeOwner(action, params={}) { -        return api.crossFrame.invoke(this.ownerFrameId, action, params); -    } -} diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index ba1f3758..8b755b83 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -21,6 +21,7 @@   * DisplayGenerator   * DisplayHistory   * DocumentUtil + * FrameEndpoint   * Frontend   * MediaLoader   * PopupFactory @@ -47,19 +48,18 @@ class Display extends EventDispatcher {          });          this._styleNode = null;          this._eventListeners = new EventListenerCollection(); -        this._persistentEventListeners = new EventListenerCollection(); -        this._interactive = false;          this._eventListenersActive = false;          this._clickScanPrevent = false;          this._setContentToken = null;          this._autoPlayAudioTimer = null; -        this._autoPlayAudioDelay = 0; +        this._autoPlayAudioDelay = 400;          this._mediaLoader = new MediaLoader();          this._displayGenerator = new DisplayGenerator({mediaLoader: this._mediaLoader});          this._hotkeys = new Map();          this._actions = new Map();          this._messageHandlers = new Map();          this._directMessageHandlers = new Map(); +        this._windowMessageHandlers = new Map();          this._history = new DisplayHistory({clearable: true, useBrowserHistory: false});          this._historyChangeIgnore = false;          this._historyHasChanged = false; @@ -102,38 +102,43 @@ class Display extends EventDispatcher {          this._parentFrameId = null;          this._ownerFrameId = null;          this._childrenSupported = true; +        this._frameEndpoint = (pageType === 'popup' ? new FrameEndpoint() : null); +        this._browser = null; +        this._copyTextarea = null;          this.registerActions([ -            ['close',            () => { this.onEscape(); }], -            ['nextEntry',        () => { this._focusEntry(this._index + 1, true); }], -            ['nextEntry3',       () => { this._focusEntry(this._index + 3, true); }], -            ['previousEntry',    () => { this._focusEntry(this._index - 1, true); }], -            ['previousEntry3',   () => { this._focusEntry(this._index - 3, true); }], -            ['lastEntry',        () => { this._focusEntry(this._definitions.length - 1, true); }], -            ['firstEntry',       () => { this._focusEntry(0, true); }], -            ['historyBackward',  () => { this._sourceTermView(); }], -            ['historyForward',   () => { this._nextTermView(); }], -            ['addNoteKanji',     () => { this._noteTryAdd('kanji'); }], -            ['addNoteTermKanji', () => { this._noteTryAdd('term-kanji'); }], -            ['addNoteTermKana',  () => { this._noteTryAdd('term-kana'); }], -            ['viewNote',         () => { this._noteTryView(); }], -            ['playAudio',        () => { this._playAudioCurrent(); }] +            ['close',             () => { this.onEscape(); }], +            ['nextEntry',         () => { this._focusEntry(this._index + 1, true); }], +            ['nextEntry3',        () => { this._focusEntry(this._index + 3, true); }], +            ['previousEntry',     () => { this._focusEntry(this._index - 1, true); }], +            ['previousEntry3',    () => { this._focusEntry(this._index - 3, true); }], +            ['lastEntry',         () => { this._focusEntry(this._definitions.length - 1, true); }], +            ['firstEntry',        () => { this._focusEntry(0, true); }], +            ['historyBackward',   () => { this._sourceTermView(); }], +            ['historyForward',    () => { this._nextTermView(); }], +            ['addNoteKanji',      () => { this._noteTryAdd('kanji'); }], +            ['addNoteTermKanji',  () => { this._noteTryAdd('term-kanji'); }], +            ['addNoteTermKana',   () => { this._noteTryAdd('term-kana'); }], +            ['viewNote',          () => { this._noteTryView(); }], +            ['playAudio',         () => { this._playAudioCurrent(); }], +            ['copyHostSelection', () => this._copyHostSelection()]          ]);          this.registerHotkeys([ -            {key: 'Escape',    modifiers: [],      action: 'close'}, -            {key: 'PageUp',    modifiers: ['alt'], action: 'previousEntry3'}, -            {key: 'PageDown',  modifiers: ['alt'], action: 'nextEntry3'}, -            {key: 'End',       modifiers: ['alt'], action: 'lastEntry'}, -            {key: 'Home',      modifiers: ['alt'], action: 'firstEntry'}, -            {key: 'ArrowUp',   modifiers: ['alt'], action: 'previousEntry'}, -            {key: 'ArrowDown', modifiers: ['alt'], action: 'nextEntry'}, -            {key: 'B',         modifiers: ['alt'], action: 'historyBackward'}, -            {key: 'F',         modifiers: ['alt'], action: 'historyForward'}, -            {key: 'K',         modifiers: ['alt'], action: 'addNoteKanji'}, -            {key: 'E',         modifiers: ['alt'], action: 'addNoteTermKanji'}, -            {key: 'R',         modifiers: ['alt'], action: 'addNoteTermKana'}, -            {key: 'P',         modifiers: ['alt'], action: 'playAudio'}, -            {key: 'V',         modifiers: ['alt'], action: 'viewNote'} +            {key: 'Escape',    modifiers: [],       action: 'close'}, +            {key: 'PageUp',    modifiers: ['alt'],  action: 'previousEntry3'}, +            {key: 'PageDown',  modifiers: ['alt'],  action: 'nextEntry3'}, +            {key: 'End',       modifiers: ['alt'],  action: 'lastEntry'}, +            {key: 'Home',      modifiers: ['alt'],  action: 'firstEntry'}, +            {key: 'ArrowUp',   modifiers: ['alt'],  action: 'previousEntry'}, +            {key: 'ArrowDown', modifiers: ['alt'],  action: 'nextEntry'}, +            {key: 'B',         modifiers: ['alt'],  action: 'historyBackward'}, +            {key: 'F',         modifiers: ['alt'],  action: 'historyForward'}, +            {key: 'K',         modifiers: ['alt'],  action: 'addNoteKanji'}, +            {key: 'E',         modifiers: ['alt'],  action: 'addNoteTermKanji'}, +            {key: 'R',         modifiers: ['alt'],  action: 'addNoteTermKana'}, +            {key: 'P',         modifiers: ['alt'],  action: 'playAudio'}, +            {key: 'V',         modifiers: ['alt'],  action: 'viewNote'}, +            {key: 'C',         modifiers: ['ctrl'], action: 'copyHostSelection'}          ]);          this.registerMessageHandlers([              ['setMode', {async: false, handler: this._onMessageSetMode.bind(this)}] @@ -146,6 +151,9 @@ class Display extends EventDispatcher {              ['setContentScale',    {async: false, handler: this._onMessageSetContentScale.bind(this)}],              ['configure',          {async: true,  handler: this._onMessageConfigure.bind(this)}]          ]); +        this.registerWindowMessageHandlers([ +            ['extensionUnloaded', {async: false, handler: this._onMessageExtensionUnloaded.bind(this)}] +        ]);      }      get autoPlayAudioDelay() { @@ -169,31 +177,58 @@ class Display extends EventDispatcher {          return this._mode;      } -    get ownerFrameId() { -        return this._ownerFrameId; -    } -      async prepare() { -        this._audioSystem.prepare(); +        // State setup +        const {documentElement} = document;          this._updateMode(); -        this._setInteractive(true); +        const {browser} = await api.getEnvironmentInfo(); +        this._browser = browser; + +        // Prepare          await this._displayGenerator.prepare(); +        this._audioSystem.prepare();          this._queryParser.prepare();          this._history.prepare(); + +        // Event setup          this._history.on('stateChanged', this._onStateChanged.bind(this));          this._queryParser.on('searched', this._onQueryParserSearch.bind(this)); +        this._progressIndicatorVisible.on('change', this._onProgressIndicatorVisibleChanged.bind(this));          yomichan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this));          chrome.runtime.onMessage.addListener(this._onMessage.bind(this));          api.crossFrame.registerHandlers([              ['popupMessage', {async: 'dynamic', handler: this._onDirectMessage.bind(this)}]          ]); +        window.addEventListener('message', this._onWindowMessage.bind(this), false);          window.addEventListener('focus', this._onWindowFocus.bind(this), false); + +        if (this._pageType === 'popup' && documentElement !== null) { +            documentElement.addEventListener('mouseup', this._onDocumentElementMouseUp.bind(this), false); +            documentElement.addEventListener('click', this._onDocumentElementClick.bind(this), false); +            documentElement.addEventListener('auxclick', this._onDocumentElementClick.bind(this), false); +        } + +        document.addEventListener('keydown', this.onKeyDown.bind(this), false); +        document.addEventListener('wheel', this._onWheel.bind(this), {passive: false}); +        if (this._closeButton !== null) { +            this._closeButton.addEventListener('click', this._onCloseButtonClick.bind(this), false); +        } +        if (this._navigationPreviousButton !== null) { +            this._navigationPreviousButton.addEventListener('click', this._onSourceTermView.bind(this), false); +        } +        if (this._navigationNextButton !== null) { +            this._navigationNextButton.addEventListener('click', this._onNextTermView.bind(this), false); +        } + +        // Final preparation          this._updateFocusedElement(); -        this._progressIndicatorVisible.on('change', this._onProgressIndicatorVisibleChanged.bind(this));      }      initializeState() {          this._onStateChanged(); +        if (this._frameEndpoint !== null) { +            this._frameEndpoint.signal(); +        }      }      setHistorySettings({clearable, useBrowserHistory}) { @@ -211,7 +246,9 @@ class Display extends EventDispatcher {      }      onEscape() { -        throw new Error('Override me'); +        if (this._pageType === 'popup') { +            this.close(); +        }      }      onKeyDown(e) { @@ -340,6 +377,9 @@ class Display extends EventDispatcher {      }      async getDocumentTitle() { +        if (this._pageType === 'float') { +            return await this._getRootFrameDocumentTitle(); +        }          return document.title;      } @@ -372,8 +412,20 @@ class Display extends EventDispatcher {          }      } +    registerWindowMessageHandlers(handlers) { +        for (const [name, handlerInfo] of handlers) { +            this._windowMessageHandlers.set(name, handlerInfo); +        } +    } +      authenticateMessageData(data) { -        return data; +        if (this._frameEndpoint === null) { +            return data; +        } +        if (!this._frameEndpoint.authenticate(data)) { +            throw new Error('Invalid authentication'); +        } +        return data.data;      }      postProcessQuery(query) { @@ -381,7 +433,9 @@ class Display extends EventDispatcher {      }      close() { -        // NOP +        if (this._pageType === 'popup') { +            this._invokeOwner('closePopup'); +        }      }      blurElement(element) { @@ -410,6 +464,21 @@ class Display extends EventDispatcher {          return {async, result};      } +    _onWindowMessage({data}) { +        try { +            data = this.authenticateMessageData(data); +        } catch (e) { +            return; +        } + +        const {action, params} = data; +        const messageHandler = this._windowMessageHandlers.get(action); +        if (typeof messageHandler === 'undefined') { return; } + +        const callback = () => {}; // NOP +        yomichan.invokeMessageHandler(messageHandler, params, callback); +    } +      _onMessageSetMode({mode}) {          this._setMode(mode, true);      } @@ -444,6 +513,11 @@ class Display extends EventDispatcher {          await this.setOptionsContext(optionsContext);      } +    _onMessageExtensionUnloaded() { +        if (yomichan.isExtensionUnloaded) { return; } +        yomichan.triggerExtensionUnloaded(); +    } +      // Private      async _onStateChanged() { @@ -751,6 +825,38 @@ class Display extends EventDispatcher {          console.log(definition);      } +    _onDocumentElementMouseUp(e) { +        switch (e.button) { +            case 3: // Back +                if (this._history.hasPrevious()) { +                    e.preventDefault(); +                } +                break; +            case 4: // Forward +                if (this._history.hasNext()) { +                    e.preventDefault(); +                } +                break; +        } +    } + +    _onDocumentElementClick(e) { +        switch (e.button) { +            case 3: // Back +                if (this._history.hasPrevious()) { +                    e.preventDefault(); +                    this._history.back(); +                } +                break; +            case 4: // Forward +                if (this._history.hasNext()) { +                    e.preventDefault(); +                    this._history.forward(); +                } +                break; +        } +    } +      _updateDocumentOptions(options) {          const data = document.documentElement.dataset;          data.ankiEnabled = `${options.anki.enable}`; @@ -768,31 +874,8 @@ class Display extends EventDispatcher {          document.documentElement.dataset.yomichanTheme = themeName;      } -    _setInteractive(interactive) { -        interactive = !!interactive; -        if (this._interactive === interactive) { return; } -        this._interactive = interactive; - -        if (interactive) { -            this._persistentEventListeners.addEventListener(document, 'keydown', this.onKeyDown.bind(this), false); -            this._persistentEventListeners.addEventListener(document, 'wheel', this._onWheel.bind(this), {passive: false}); -            if (this._closeButton !== null) { -                this._persistentEventListeners.addEventListener(this._closeButton, 'click', this._onCloseButtonClick.bind(this)); -            } -            if (this._navigationPreviousButton !== null) { -                this._persistentEventListeners.addEventListener(this._navigationPreviousButton, 'click', this._onSourceTermView.bind(this)); -            } -            if (this._navigationNextButton !== null) { -                this._persistentEventListeners.addEventListener(this._navigationNextButton, 'click', this._onNextTermView.bind(this)); -            } -        } else { -            this._persistentEventListeners.removeAllEventListeners(); -        } -        this._setEventListenersActive(this._eventListenersActive); -    } -      _setEventListenersActive(active) { -        active = !!active && this._interactive; +        active = !!active;          if (this._eventListenersActive === active) { return; }          this._eventListenersActive = active; @@ -1628,4 +1711,63 @@ class Display extends EventDispatcher {          this._frontend = frontend;          await frontend.prepare();      } + +    async _invokeOwner(action, params={}) { +        if (this._ownerFrameId === null) { +            throw new Error('No owner frame'); +        } +        return await api.crossFrame.invoke(this._ownerFrameId, action, params); +    } + +    _copyHostSelection() { +        if (window.getSelection().toString()) { return false; } +        this._copyHostSelectionInner(); +        return true; +    } + +    async _copyHostSelectionInner() { +        switch (this._browser) { +            case 'firefox': +            case 'firefox-mobile': +                { +                    let text; +                    try { +                        text = await this._invokeOwner('getSelectionText'); +                    } catch (e) { +                        break; +                    } +                    this._copyText(text); +                } +                break; +            default: +                await this._invokeOwner('copySelection'); +                break; +        } +    } + +    _copyText(text) { +        const parent = document.body; +        if (parent === null) { return; } + +        let textarea = this._copyTextarea; +        if (textarea === null) { +            textarea = document.createElement('textarea'); +            this._copyTextarea = textarea; +        } + +        textarea.value = text; +        parent.appendChild(textarea); +        textarea.select(); +        document.execCommand('copy'); +        parent.removeChild(textarea); +    } + +    async _getRootFrameDocumentTitle() { +        try { +            const {title} = await api.crossFrame.invoke(0, 'getDocumentInformation'); +            return title; +        } catch (e) { +            return ''; +        } +    }  } |