From 2f8408ffcc0be1321bcd105e7675a1210b8f7df8 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 5 Sep 2020 21:43:19 -0400 Subject: [PATCH] Text scanner refactor (#771) * Create searchAt wrappers * Add optional support for searching on the click event * Update QueryParser to use TextScanner's searchOnClick functionality * Move/rename searchAt * Move pendingLookup checks * Add 'searched' event to TextScanner * Use common searched event for Frontend and QueryParser * Move functions, make private --- ext/bg/js/query-parser.js | 29 ++--- ext/fg/js/frontend.js | 81 +++++------- ext/mixed/js/text-scanner.js | 246 +++++++++++++++++++++-------------- 3 files changed, 196 insertions(+), 160 deletions(-) diff --git a/ext/bg/js/query-parser.js b/ext/bg/js/query-parser.js index b0ef2d78..a2a04606 100644 --- a/ext/bg/js/query-parser.js +++ b/ext/bg/js/query-parser.js @@ -36,15 +36,18 @@ class QueryParser extends EventDispatcher { node: this._queryParser, ignoreElements: () => [], ignorePoint: null, - search: this._search.bind(this), - documentUtil + getOptionsContext, + documentUtil, + searchTerms: true, + searchKanji: false, + searchOnClick: true }); } async prepare() { await this._queryParserGenerator.prepare(); this._textScanner.prepare(); - this._queryParser.addEventListener('click', this._onClick.bind(this)); + this._textScanner.on('searched', this._onSearched.bind(this)); } setOptions({selectedParser, termSpacing, scanning}) { @@ -76,18 +79,12 @@ class QueryParser extends EventDispatcher { // Private - _onClick(e) { - this._textScanner.searchAt(e.clientX, e.clientY, 'click'); - } - - async _search(textSource, cause) { - if (textSource === null) { return null; } - - const optionsContext = this._getOptionsContext(); - const results = await this._textScanner.findTerms(textSource, optionsContext); - if (results === null) { return null; } - - const {definitions, sentence, type} = results; + _onSearched({type, definitions, sentence, cause, textSource, optionsContext, error}) { + if (error !== null) { + yomichan.logError(error); + return; + } + if (type === null) { return; } this.trigger('searched', { type, @@ -97,8 +94,6 @@ class QueryParser extends EventDispatcher { textSource, optionsContext }); - - return {definitions, type}; } _onParserChange(e) { diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 44690028..8c4cfc82 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -47,8 +47,10 @@ class Frontend { node: window, ignoreElements: this._ignoreElements.bind(this), ignorePoint: this._ignorePoint.bind(this), - search: this._search.bind(this), - documentUtil: this._documentUtil + getOptionsContext: this._getUpToDateOptionsContext.bind(this), + documentUtil: this._documentUtil, + searchTerms: true, + searchKanji: true }); this._parentPopupId = parentPopupId; this._parentFrameId = parentFrameId; @@ -105,6 +107,7 @@ class Frontend { this._textScanner.on('clearSelection', this._onClearSelection.bind(this)); this._textScanner.on('activeModifiersChanged', this._onActiveModifiersChanged.bind(this)); + this._textScanner.on('searched', this._onSearched.bind(this)); api.crossFrame.registerHandlers([ ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}], @@ -126,8 +129,7 @@ class Frontend { } async setTextSource(textSource) { - await this._search(textSource, 'script'); - this._textScanner.setCurrentTextSource(textSource); + await this._textScanner.search(textSource, 'script'); } async getOptionsContext() { @@ -247,6 +249,27 @@ class Frontend { await this.updateOptions(); } + _onSearched({textScanner, type, definitions, sentence, cause, textSource, optionsContext, error}) { + if (error !== null) { + if (yomichan.isExtensionUnloaded) { + if (textSource !== null && this._options.scanning.modifier !== 'none') { + this._showExtensionUnloaded(textSource); + } + } else { + yomichan.logError(error); + } + } else { + if (type !== null) { + const focus = (cause === 'mouse'); + this._showContent(textSource, focus, definitions, type, sentence, optionsContext); + } + } + + if (type === null && this._options.scanning.autoHideResults) { + textScanner.clearSelection(false); + } + } + async _updateOptionsInternal() { const optionsContext = await this.getOptionsContext(); const options = await api.optionsGet(optionsContext); @@ -279,7 +302,7 @@ class Frontend { const textSourceCurrent = this._textScanner.getCurrentTextSource(); const causeCurrent = this._textScanner.causeCurrent; if (textSourceCurrent !== null && causeCurrent !== null) { - await this._search(textSourceCurrent, causeCurrent); + await this._textScanner.search(textSourceCurrent, causeCurrent); } } @@ -402,44 +425,6 @@ class Frontend { } } - async _search(textSource, cause) { - if (this._popup === null) { - return null; - } - - await this._updatePendingOptions(); - - let results = null; - - try { - if (textSource !== null) { - const optionsContext = await this.getOptionsContext(); - results = ( - await this._textScanner.findTerms(textSource, optionsContext) || - await this._textScanner.findKanji(textSource, optionsContext) - ); - if (results !== null) { - const focus = (cause === 'mouse'); - this._showContent(textSource, focus, results.definitions, results.type, optionsContext); - } - } - } catch (e) { - if (yomichan.isExtensionUnloaded) { - if (textSource !== null && this._options.scanning.modifier !== 'none') { - this._showExtensionUnloaded(textSource); - } - } else { - yomichan.logError(e); - } - } finally { - if (results === null && this._options.scanning.autoHideResults) { - this._textScanner.clearSelection(false); - } - } - - return results; - } - async _showExtensionUnloaded(textSource) { if (textSource === null) { textSource = this._textScanner.getCurrentTextSource(); @@ -448,11 +433,8 @@ class Frontend { this._showPopupContent(textSource, await this.getOptionsContext()); } - _showContent(textSource, focus, definitions, type, optionsContext) { + _showContent(textSource, focus, definitions, type, sentence, optionsContext) { const {url} = optionsContext; - const sentenceExtent = this._options.anki.sentenceExt; - const layoutAwareScan = this._options.scanning.layoutAwareScan; - const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); const query = textSource.text(); const details = { focus, @@ -571,4 +553,9 @@ class Frontend { api.broadcastTab('requestFrontendReadyBroadcast', {frameId: this._frameId}); await promise; } + + async _getUpToDateOptionsContext() { + await this._updatePendingOptions(); + return await this.getOptionsContext(); + } } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 5a64c14a..2aeac565 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -21,13 +21,16 @@ */ class TextScanner extends EventDispatcher { - constructor({node, ignoreElements, ignorePoint, search, documentUtil}) { + constructor({node, ignoreElements, ignorePoint, documentUtil, getOptionsContext, searchTerms=false, searchKanji=false, searchOnClick=false}) { super(); this._node = node; this._ignoreElements = ignoreElements; this._ignorePoint = ignorePoint; - this._search = search; this._documentUtil = documentUtil; + this._getOptionsContext = getOptionsContext; + this._searchTerms = searchTerms; + this._searchKanji = searchKanji; + this._searchOnClick = searchOnClick; this._isPrepared = false; this._ignoreNodes = null; @@ -125,41 +128,6 @@ class TextScanner extends EventDispatcher { } } - async searchAt(x, y, cause) { - try { - this._scanTimerClear(); - - if (this._pendingLookup) { - return; - } - - if (typeof this._ignorePoint === 'function' && await this._ignorePoint(x, y)) { - return; - } - - const textSource = this._documentUtil.getRangeFromPoint(x, y, this._deepContentScan); - try { - if (this._textSourceCurrent !== null && this._textSourceCurrent.equals(textSource)) { - return; - } - - this._pendingLookup = true; - const result = await this._search(textSource, cause); - if (result !== null) { - this._causeCurrent = cause; - this.setCurrentTextSource(textSource); - } - this._pendingLookup = false; - } finally { - if (textSource !== null) { - textSource.cleanup(); - } - } - } catch (e) { - yomichan.logError(e); - } - } - getTextSourceContent(textSource, length, layoutAwareScan) { const clonedTextSource = textSource.clone(); @@ -206,35 +174,44 @@ class TextScanner extends EventDispatcher { } } - async findTerms(textSource, optionsContext) { - const scanLength = this._scanLength; - const sentenceExtent = this._sentenceExtent; - const layoutAwareScan = this._layoutAwareScan; - const searchText = this.getTextSourceContent(textSource, scanLength, layoutAwareScan); - if (searchText.length === 0) { return null; } + async search(textSource, cause) { + let definitions = null; + let sentence = null; + let type = null; + let error = null; + let searched = false; + let optionsContext = null; - const {definitions, length} = await api.termsFind(searchText, {}, optionsContext); - if (definitions.length === 0) { return null; } + try { + if (this._textSourceCurrent !== null && this._textSourceCurrent.equals(textSource)) { + return; + } - textSource.setEndOffset(length, layoutAwareScan); - const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); + optionsContext = await this._getOptionsContext(); + searched = true; - return {definitions, sentence, type: 'terms'}; - } + const result = await this._findDefinitions(textSource, cause); + if (result !== null) { + ({definitions, sentence, type} = result); + this._causeCurrent = cause; + this.setCurrentTextSource(textSource); + } + } catch (e) { + error = e; + } - async findKanji(textSource, optionsContext) { - const sentenceExtent = this._sentenceExtent; - const layoutAwareScan = this._layoutAwareScan; - const searchText = this.getTextSourceContent(textSource, 1, layoutAwareScan); - if (searchText.length === 0) { return null; } + if (!searched) { return; } - const definitions = await api.kanjiFind(searchText, optionsContext); - if (definitions.length === 0) { return null; } - - textSource.setEndOffset(1, layoutAwareScan); - const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); - - return {definitions, sentence, type: 'kanji'}; + this.trigger('searched', { + textScanner: this, + type, + definitions, + sentence, + cause, + textSource, + optionsContext, + error + }); } // Private @@ -248,7 +225,7 @@ class TextScanner extends EventDispatcher { _onMouseMove(e) { this._scanTimerClear(); - if (this._pendingLookup || DocumentUtil.isMouseButtonDown(e, 'primary')) { + if (DocumentUtil.isMouseButtonDown(e, 'primary')) { return; } @@ -262,18 +239,7 @@ class TextScanner extends EventDispatcher { return; } - const search = async () => { - if (this._modifier === 'none') { - if (!await this._scanTimerWait()) { - // Aborted - return; - } - } - - await this.searchAt(e.clientX, e.clientY, 'mouse'); - }; - - search(); + this._searchAtFromMouse(e.clientX, e.clientY); } _onMouseDown(e) { @@ -296,6 +262,10 @@ class TextScanner extends EventDispatcher { } _onClick(e) { + if (this._searchOnClick) { + this._searchAt(e.clientX, e.clientY, 'click'); + } + if (this._preventNextClick) { this._preventNextClick = false; e.preventDefault(); @@ -334,25 +304,7 @@ class TextScanner extends EventDispatcher { 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; - }); + this._searchAtFromTouchStart(primaryTouch.clientX, primaryTouch.clientY); } _onTouchEnd(e) { @@ -384,7 +336,7 @@ class TextScanner extends EventDispatcher { return; } - this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove'); + this._searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove'); e.preventDefault(); // Disable scroll } @@ -425,13 +377,13 @@ class TextScanner extends EventDispatcher { [this._node, 'mousedown', this._onMouseDown.bind(this)], [this._node, 'mousemove', this._onMouseMove.bind(this)], [this._node, 'mouseover', this._onMouseOver.bind(this)], - [this._node, 'mouseout', this._onMouseOut.bind(this)] + [this._node, 'mouseout', this._onMouseOut.bind(this)], + [this._node, 'click', this._onClick.bind(this)] ]; } _getTouchEventListeners() { return [ - [this._node, 'click', this._onClick.bind(this)], [this._node, 'auxclick', this._onAuxClick.bind(this)], [this._node, 'touchstart', this._onTouchStart.bind(this)], [this._node, 'touchend', this._onTouchEnd.bind(this)], @@ -460,4 +412,106 @@ class TextScanner extends EventDispatcher { } return null; } + + async _findDefinitions(textSource, optionsContext) { + if (textSource === null) { + return null; + } + if (this._searchTerms) { + const results = await this._findTerms(textSource, optionsContext); + if (results !== null) { return results; } + } + if (this._searchKanji) { + const results = await this._findKanji(textSource, optionsContext); + if (results !== null) { return results; } + } + return null; + } + + async _findTerms(textSource, optionsContext) { + const scanLength = this._scanLength; + const sentenceExtent = this._sentenceExtent; + const layoutAwareScan = this._layoutAwareScan; + const searchText = this.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); + const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); + + return {definitions, sentence, type: 'terms'}; + } + + async _findKanji(textSource, optionsContext) { + const sentenceExtent = this._sentenceExtent; + const layoutAwareScan = this._layoutAwareScan; + const searchText = this.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); + const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); + + return {definitions, sentence, type: 'kanji'}; + } + + async _searchAt(x, y, cause) { + if (this._pendingLookup) { return; } + + try { + this._pendingLookup = true; + this._scanTimerClear(); + + if (typeof this._ignorePoint === 'function' && await this._ignorePoint(x, y)) { + return; + } + + const textSource = this._documentUtil.getRangeFromPoint(x, y, this._deepContentScan); + try { + await this.search(textSource, cause); + } finally { + if (textSource !== null) { + textSource.cleanup(); + } + } + } catch (e) { + yomichan.logError(e); + } finally { + this._pendingLookup = false; + } + } + + async _searchAtFromMouse(x, y) { + if (this._pendingLookup) { return; } + + if (this._modifier === 'none') { + if (!await this._scanTimerWait()) { + // Aborted + return; + } + } + + await this._searchAt(x, y, 'mouse'); + } + + async _searchAtFromTouchStart(x, y) { + if (this._pendingLookup) { return; } + + const textSourceCurrentPrevious = this._textSourceCurrent !== null ? this._textSourceCurrent.clone() : null; + + await this._searchAt(x, y, 'touchStart'); + + if ( + this._textSourceCurrent !== null && + !this._textSourceCurrent.equals(textSourceCurrentPrevious) + ) { + this._preventScroll = true; + this._preventNextContextMenu = true; + this._preventNextMouseDown = true; + } + } }