/* * 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 * TextScanner * api * docSentenceExtract */ class Frontend { constructor(popup, getUrl=null) { this._id = yomichan.generateId(16); this._popup = popup; this._getUrl = getUrl; this._disabledOverride = false; this._options = null; this._pageZoomFactor = 1.0; this._contentScale = 1.0; this._orphaned = false; this._lastShowPromise = Promise.resolve(); this._enabledEventListeners = new EventListenerCollection(); this._activeModifiers = new Set(); this._optionsUpdatePending = false; this._textScanner = new TextScanner({ node: window, ignoreElements: () => this._popup.isProxy() ? [] : [this._popup.getFrame()], ignorePoint: (x, y) => this._popup.containsPoint(x, y), search: this._search.bind(this) }); this._windowMessageHandlers = new Map([ ['popupClose', this._onMessagePopupClose.bind(this)], ['selectionCopy', this._onMessageSelectionCopy.bind()] ]); this._runtimeMessageHandlers = new Map([ ['popupSetVisibleOverride', this._onMessagePopupSetVisibleOverride.bind(this)], ['rootPopupRequestInformationBroadcast', this._onMessageRootPopupRequestInformationBroadcast.bind(this)], ['requestDocumentInformationBroadcast', this._onMessageRequestDocumentInformationBroadcast.bind(this)] ]); } get canClearSelection() { return this._textScanner.canClearSelection; } set canClearSelection(value) { this._textScanner.canClearSelection = value; } async prepare() { try { await this.updateOptions(); try { const {zoomFactor} = await api.getZoom(); this._pageZoomFactor = zoomFactor; } catch (e) { // Ignore exceptions which may occur due to being on an unsupported page (e.g. about:blank) } window.addEventListener('resize', this._onResize.bind(this), false); const visualViewport = window.visualViewport; if (visualViewport !== null && typeof visualViewport === 'object') { window.visualViewport.addEventListener('scroll', this._onVisualViewportScroll.bind(this)); window.visualViewport.addEventListener('resize', this._onVisualViewportResize.bind(this)); } yomichan.on('orphaned', this._onOrphaned.bind(this)); yomichan.on('optionsUpdated', this.updateOptions.bind(this)); yomichan.on('zoomChanged', this._onZoomChanged.bind(this)); chrome.runtime.onMessage.addListener(this._onRuntimeMessage.bind(this)); this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this)); this._updateContentScale(); this._broadcastRootPopupInformation(); } catch (e) { yomichan.logError(e); } } async setPopup(popup) { this._textScanner.clearSelection(true); this._popup = popup; await popup.setOptionsContext(await this.getOptionsContext(), this._id); } setDisabledOverride(disabled) { this._disabledOverride = disabled; this._updateTextScannerEnabled(); } async setTextSource(textSource) { await this._search(textSource, 'script'); this._textScanner.setCurrentTextSource(textSource); } async getOptionsContext() { const url = this._getUrl !== null ? await this._getUrl() : window.location.href; const depth = this._popup.depth; const modifierKeys = [...this._activeModifiers]; return {depth, url, modifierKeys}; } async updateOptions() { const optionsContext = await this.getOptionsContext(); this._options = await api.optionsGet(optionsContext); this._textScanner.setOptions(this._options); this._updateTextScannerEnabled(); const ignoreNodes = ['.scan-disable', '.scan-disable *']; if (!this._options.scanning.enableOnPopupExpressions) { ignoreNodes.push('.source-text', '.source-text *'); } this._textScanner.ignoreNodes = ignoreNodes.join(','); await this._popup.setOptionsContext(optionsContext, this._id); this._updateContentScale(); const textSourceCurrent = this._textScanner.getCurrentTextSource(); const causeCurrent = this._textScanner.causeCurrent; if (textSourceCurrent !== null && causeCurrent !== null) { await this._search(textSourceCurrent, causeCurrent); } } showContentCompleted() { return this._lastShowPromise; } // Message handlers _onMessagePopupClose() { this._textScanner.clearSelection(false); } _onMessageSelectionCopy() { document.execCommand('copy'); } _onMessagePopupSetVisibleOverride({visible}) { this._popup.setVisibleOverride(visible); } _onMessageRootPopupRequestInformationBroadcast() { this._broadcastRootPopupInformation(); } _onMessageRequestDocumentInformationBroadcast({uniqueId}) { this._broadcastDocumentInformation(uniqueId); } // Private _onResize() { this._updatePopupPosition(); } _onWindowMessage(e) { const action = e.data; const handler = this._windowMessageHandlers.get(action); if (typeof handler !== 'function') { return false; } handler(); } _onRuntimeMessage({action, params}, sender, callback) { const handler = this._runtimeMessageHandlers.get(action); if (typeof handler !== 'function') { return false; } const result = handler(params, sender); callback(result); return false; } _onOrphaned() { this._orphaned = true; } _onZoomChanged({newZoomFactor}) { this._pageZoomFactor = newZoomFactor; this._updateContentScale(); } _onVisualViewportScroll() { this._updatePopupPosition(); } _onVisualViewportResize() { this._updateContentScale(); } _onClearSelection({passive}) { this._popup.hide(!passive); this._popup.clearAutoPlayTimer(); this._updatePendingOptions(); } async _onActiveModifiersChanged({modifiers}) { if (areSetsEqual(modifiers, this._activeModifiers)) { return; } this._activeModifiers = modifiers; if (await this._popup.isVisible()) { this._optionsUpdatePending = true; return; } await this.updateOptions(); } async _search(textSource, cause) { await this._updatePendingOptions(); let results = null; try { if (textSource !== null) { const optionsContext = await this.getOptionsContext(); results = ( await this._findTerms(textSource, optionsContext) || await this._findKanji(textSource, optionsContext) ); if (results !== null) { const focus = (cause === 'mouse'); this._showContent(textSource, focus, results.definitions, results.type, optionsContext); } } } catch (e) { if (this._orphaned) { if (textSource !== null && this._options.scanning.modifier !== 'none') { this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned'); } } else { yomichan.logError(e); } } finally { if (results === null && this._options.scanning.autoHideResults) { this._textScanner.clearSelection(false); } } return results; } async _findTerms(textSource, optionsContext) { const {length: scanLength, layoutAwareScan} = this._options.scanning; const searchText = this._textScanner.getTextSourceContent(textSource, scanLength, layoutAwareScan); if (searchText.length === 0) { return null; } const {definitions, length} = await api.termsFind(searchText, {}, optionsContext); if (definitions.length === 0) { return null; } textSource.setEndOffset(length, layoutAwareScan); return {definitions, type: 'terms'}; } async _findKanji(textSource, optionsContext) { const layoutAwareScan = this._options.scanning.layoutAwareScan; const searchText = this._textScanner.getTextSourceContent(textSource, 1, layoutAwareScan); if (searchText.length === 0) { return null; } const definitions = await api.kanjiFind(searchText, optionsContext); if (definitions.length === 0) { return null; } textSource.setEndOffset(1, layoutAwareScan); return {definitions, type: 'kanji'}; } _showContent(textSource, focus, definitions, type, optionsContext) { const {url} = optionsContext; const sentenceExtent = this._options.anki.sentenceExt; const layoutAwareScan = this._options.scanning.layoutAwareScan; const sentence = docSentenceExtract(textSource, sentenceExtent, layoutAwareScan); this._showPopupContent( textSource, optionsContext, type, {definitions, context: {sentence, url, focus, disableHistory: true}} ); } _showPopupContent(textSource, optionsContext, type=null, details=null) { const context = {optionsContext, source: this._id}; this._lastShowPromise = this._popup.showContent( textSource.getRect(), textSource.getWritingMode(), type, details, context ); return this._lastShowPromise; } async _updatePendingOptions() { if (this._optionsUpdatePending) { this._optionsUpdatePending = false; await this.updateOptions(); } } _updateTextScannerEnabled() { const enabled = ( this._options.general.enable && this._popup.depth <= this._options.scanning.popupNestingMaxDepth && !this._disabledOverride ); this._enabledEventListeners.removeAllEventListeners(); this._textScanner.setEnabled(enabled); if (enabled) { this._enabledEventListeners.addEventListener(window, 'message', this._onWindowMessage.bind(this)); } } _updateContentScale() { const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this._options.general; let contentScale = popupScalingFactor; if (popupScaleRelativeToPageZoom) { contentScale /= this._pageZoomFactor; } if (popupScaleRelativeToVisualViewport) { const visualViewport = window.visualViewport; const visualViewportScale = (visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0); contentScale /= visualViewportScale; } if (contentScale === this._contentScale) { return; } this._contentScale = contentScale; this._popup.setContentScale(this._contentScale); this._updatePopupPosition(); } async _updatePopupPosition() { const textSource = this._textScanner.getCurrentTextSource(); if (textSource !== null && await this._popup.isVisible()) { this._showPopupContent(textSource, await this.getOptionsContext()); } } _broadcastRootPopupInformation() { if (!this._popup.isProxy() && this._popup.depth === 0 && this._popup.frameId === 0) { api.broadcastTab('rootPopupInformation', {popupId: this._popup.id, frameId: this._popup.frameId}); } } _broadcastDocumentInformation(uniqueId) { api.broadcastTab('documentInformationBroadcast', { uniqueId, frameId: this._popup.frameId, title: document.title }); } }