diff options
Diffstat (limited to 'ext/mixed')
-rw-r--r-- | ext/mixed/js/display.js | 272 |
1 files changed, 207 insertions, 65 deletions
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 ''; + } + } } |