diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2021-02-13 22:52:28 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-13 22:52:28 -0500 |
commit | 6a271e067fa917614f4c81f473533e24c6d04404 (patch) | |
tree | 0d81658b1c03aecfbba133425aefc0ea7612338c /ext/js/language/text-scanner.js | |
parent | deed5027cd18bcdb9cb9d13cb7831be0ec5384e8 (diff) |
Move mixed/js (#1383)
* Move mixed/js/core.js to js/core.js
* Move mixed/js/yomichan.js to js/yomichan.js
* Move mixed/js/timer.js to js/debug/timer.js
* Move mixed/js/hotkey-handler.js to js/input/hotkey-handler.js
* Move mixed/js/hotkey-help-controller.js to js/input/hotkey-help-controller.js
* Move mixed/js/hotkey-util.js to js/input/hotkey-util.js
* Move mixed/js/audio-system.js to js/input/audio-system.js
* Move mixed/js/media-loader.js to js/input/media-loader.js
* Move mixed/js/text-to-speech-audio.js to js/input/text-to-speech-audio.js
* Move mixed/js/comm.js to js/comm/cross-frame-api.js
* Move mixed/js/api.js to js/comm/api.js
* Move mixed/js/frame-client.js to js/comm/frame-client.js
* Move mixed/js/frame-endpoint.js to js/comm/frame-endpoint.js
* Move mixed/js/display.js to js/display/display.js
* Move mixed/js/display-audio.js to js/display/display-audio.js
* Move mixed/js/display-generator.js to js/display/display-generator.js
* Move mixed/js/display-history.js to js/display/display-history.js
* Move mixed/js/display-notification.js to js/display/display-notification.js
* Move mixed/js/display-profile-selection.js to js/display/display-profile-selection.js
* Move mixed/js/japanese.js to js/language/japanese-util.js
* Move mixed/js/dictionary-data-util.js to js/language/dictionary-data-util.js
* Move mixed/js/document-focus-controller.js to js/dom/document-focus-controller.js
* Move mixed/js/document-util.js to js/dom/document-util.js
* Move mixed/js/dom-data-binder.js to js/dom/dom-data-binder.js
* Move mixed/js/html-template-collection.js to js/dom/html-template-collection.js
* Move mixed/js/panel-element.js to js/dom/panel-element.js
* Move mixed/js/popup-menu.js to js/dom/popup-menu.js
* Move mixed/js/selector-observer.js to js/dom/selector-observer.js
* Move mixed/js/scroll.js to js/dom/window-scroll.js
* Move mixed/js/text-scanner.js to js/language/text-scanner.js
* Move mixed/js/cache-map.js to js/general/cache-map.js
* Move mixed/js/object-property-accessor.js to js/general/object-property-accessor.js
* Move mixed/js/task-accumulator.js to js/general/task-accumulator.js
* Move mixed/js/environment.js to js/background/environment.js
* Move mixed/js/dynamic-loader.js to js/scripting/dynamic-loader.js
* Move mixed/js/dynamic-loader-sentinel.js to js/scripting/dynamic-loader-sentinel.js
Diffstat (limited to 'ext/js/language/text-scanner.js')
-rw-r--r-- | ext/js/language/text-scanner.js | 982 |
1 files changed, 982 insertions, 0 deletions
diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js new file mode 100644 index 00000000..7672b69d --- /dev/null +++ b/ext/js/language/text-scanner.js @@ -0,0 +1,982 @@ +/* + * Copyright (C) 2019-2021 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 + * DocumentUtil + * api + */ + +class TextScanner extends EventDispatcher { + constructor({ + node, + documentUtil, + getSearchContext, + ignoreElements=null, + ignorePoint=null, + searchTerms=false, + searchKanji=false, + searchOnClick=false, + searchOnClickOnly=false + }) { + super(); + this._node = node; + this._documentUtil = documentUtil; + this._getSearchContext = getSearchContext; + this._ignoreElements = ignoreElements; + this._ignorePoint = ignorePoint; + this._searchTerms = searchTerms; + this._searchKanji = searchKanji; + this._searchOnClick = searchOnClick; + this._searchOnClickOnly = searchOnClickOnly; + + this._isPrepared = false; + this._includeSelector = null; + this._excludeSelector = null; + + this._inputInfoCurrent = null; + this._scanTimerPromise = null; + this._textSourceCurrent = null; + this._textSourceCurrentSelected = false; + this._pendingLookup = false; + + this._deepContentScan = false; + this._selectText = false; + this._delay = 0; + this._touchInputEnabled = false; + this._pointerEventsEnabled = false; + this._scanLength = 1; + this._layoutAwareScan = false; + this._preventMiddleMouse = false; + this._sentenceScanExtent = 0; + this._sentenceTerminatorMap = new Map(); + this._sentenceForwardQuoteMap = new Map(); + this._sentenceBackwardQuoteMap = new Map(); + this._inputs = []; + + this._enabled = false; + this._enabledValue = false; + this._eventListeners = new EventListenerCollection(); + + this._primaryTouchIdentifier = null; + this._preventNextContextMenu = false; + this._preventNextMouseDown = false; + this._preventNextClick = false; + this._preventScroll = false; + this._penPointerPressed = false; + this._penPointerReleased = false; + this._pointerIdTypeMap = new Map(); + + this._canClearSelection = true; + } + + get canClearSelection() { + return this._canClearSelection; + } + + set canClearSelection(value) { + this._canClearSelection = value; + } + + get includeSelector() { + return this._includeSelector; + } + + set includeSelector(value) { + this._includeSelector = value; + } + + get excludeSelector() { + return this._excludeSelector; + } + + set excludeSelector(value) { + this._excludeSelector = value; + } + + prepare() { + this._isPrepared = true; + this.setEnabled(this._enabled); + } + + setEnabled(enabled) { + this._enabled = enabled; + + const value = enabled && this._isPrepared; + if (this._enabledValue === value) { return; } + + this._eventListeners.removeAllEventListeners(); + this._primaryTouchIdentifier = null; + this._preventNextContextMenu = false; + this._preventNextMouseDown = false; + this._preventNextClick = false; + this._preventScroll = false; + this._penPointerPressed = false; + this._penPointerReleased = false; + this._pointerIdTypeMap.clear(); + + this._enabledValue = value; + + if (value) { + this._hookEvents(); + } else { + this.clearSelection(true); + } + } + + setOptions({ + inputs, + deepContentScan, + selectText, + delay, + touchInputEnabled, + pointerEventsEnabled, + scanLength, + layoutAwareScan, + preventMiddleMouse, + sentenceParsingOptions + }) { + if (Array.isArray(inputs)) { + this._inputs = inputs.map(({ + include, + exclude, + types, + options: { + searchTerms, + searchKanji, + scanOnTouchMove, + scanOnPenHover, + scanOnPenPress, + scanOnPenRelease, + preventTouchScrolling + } + }) => ({ + include: this._getInputArray(include), + exclude: this._getInputArray(exclude), + types: this._getInputTypeSet(types), + options: { + searchTerms, + searchKanji, + scanOnTouchMove, + scanOnPenHover, + scanOnPenPress, + scanOnPenRelease, + preventTouchScrolling + } + })); + } + if (typeof deepContentScan === 'boolean') { + this._deepContentScan = deepContentScan; + } + if (typeof selectText === 'boolean') { + this._selectText = selectText; + } + if (typeof delay === 'number') { + this._delay = delay; + } + if (typeof touchInputEnabled === 'boolean') { + this._touchInputEnabled = touchInputEnabled; + } + if (typeof pointerEventsEnabled === 'boolean') { + this._pointerEventsEnabled = pointerEventsEnabled; + } + if (typeof scanLength === 'number') { + this._scanLength = scanLength; + } + if (typeof layoutAwareScan === 'boolean') { + this._layoutAwareScan = layoutAwareScan; + } + if (typeof preventMiddleMouse === 'boolean') { + this._preventMiddleMouse = preventMiddleMouse; + } + if (typeof sentenceParsingOptions === 'object' && sentenceParsingOptions !== null) { + const {scanExtent, enableTerminationCharacters, terminationCharacters} = sentenceParsingOptions; + const hasTerminationCharacters = (typeof terminationCharacters === 'object' && Array.isArray(terminationCharacters)); + if (typeof scanExtent === 'number') { + this._sentenceScanExtent = sentenceParsingOptions.scanExtent; + } + if (typeof enableTerminationCharacters === 'boolean' || hasTerminationCharacters) { + const sentenceTerminatorMap = this._sentenceTerminatorMap; + const sentenceForwardQuoteMap = this._sentenceForwardQuoteMap; + const sentenceBackwardQuoteMap = this._sentenceBackwardQuoteMap; + sentenceTerminatorMap.clear(); + sentenceForwardQuoteMap.clear(); + sentenceBackwardQuoteMap.clear(); + if (enableTerminationCharacters !== false && hasTerminationCharacters) { + for (const {enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd} of terminationCharacters) { + if (!enabled) { continue; } + if (character2 === null) { + sentenceTerminatorMap.set(character1, [includeCharacterAtStart, includeCharacterAtEnd]); + } else { + sentenceForwardQuoteMap.set(character1, [character2, includeCharacterAtStart]); + sentenceBackwardQuoteMap.set(character2, [character1, includeCharacterAtEnd]); + } + } + } + } + } + } + + getTextSourceContent(textSource, length, layoutAwareScan) { + const clonedTextSource = textSource.clone(); + + clonedTextSource.setEndOffset(length, layoutAwareScan); + + const includeSelector = this._includeSelector; + const excludeSelector = this._excludeSelector; + if (includeSelector !== null || excludeSelector !== null) { + this._constrainTextSource(clonedTextSource, includeSelector, excludeSelector, layoutAwareScan); + } + + return clonedTextSource.text(); + } + + hasSelection() { + return (this._textSourceCurrent !== null); + } + + clearSelection(passive) { + if (!this._canClearSelection) { return; } + if (this._textSourceCurrent !== null) { + if (this._textSourceCurrentSelected) { + this._textSourceCurrent.deselect(); + } + this._textSourceCurrent = null; + this._textSourceCurrentSelected = false; + this._inputInfoCurrent = null; + } + this.trigger('clearSelection', {passive}); + } + + getCurrentTextSource() { + return this._textSourceCurrent; + } + + setCurrentTextSource(textSource) { + this._textSourceCurrent = textSource; + if (this._selectText) { + this._textSourceCurrent.select(); + this._textSourceCurrentSelected = true; + } else { + this._textSourceCurrentSelected = false; + } + } + + async searchLast() { + if (this._textSourceCurrent !== null && this._inputInfoCurrent !== null) { + await this._search(this._textSourceCurrent, this._searchTerms, this._searchKanji, this._inputInfoCurrent); + return true; + } + return false; + } + + async search(textSource, inputDetail) { + const inputInfo = this._createInputInfo(null, 'script', 'script', true, [], [], inputDetail); + return await this._search(textSource, this._searchTerms, this._searchKanji, inputInfo); + } + + // Private + + _createOptionsContextForInput(baseOptionsContext, inputInfo) { + const optionsContext = clone(baseOptionsContext); + const {modifiers, modifierKeys} = inputInfo; + optionsContext.modifiers = [...modifiers]; + optionsContext.modifierKeys = [...modifierKeys]; + return optionsContext; + } + + async _search(textSource, searchTerms, searchKanji, inputInfo) { + let definitions = null; + let sentence = null; + let type = null; + let error = null; + let searched = false; + let optionsContext = null; + let detail = null; + + try { + if (this._textSourceCurrent !== null && this._textSourceCurrent.hasSameStart(textSource)) { + return null; + } + + ({optionsContext, detail} = await this._getSearchContext()); + optionsContext = this._createOptionsContextForInput(optionsContext, inputInfo); + + searched = true; + + const result = await this._findDefinitions(textSource, searchTerms, searchKanji, optionsContext); + if (result !== null) { + ({definitions, sentence, type} = result); + this._inputInfoCurrent = inputInfo; + this.setCurrentTextSource(textSource); + } + } catch (e) { + error = e; + } + + if (!searched) { return null; } + + const results = { + textScanner: this, + type, + definitions, + sentence, + inputInfo, + textSource, + optionsContext, + detail, + error + }; + this.trigger('searched', results); + return results; + } + + _onMouseOver(e) { + if (this._ignoreElements !== null && this._ignoreElements().includes(e.target)) { + this._scanTimerClear(); + } + } + + _onMouseMove(e) { + this._scanTimerClear(); + + const inputInfo = this._getMatchingInputGroupFromEvent('mouse', 'mouseMove', e); + if (inputInfo === null) { return; } + + this._searchAtFromMouseMove(e.clientX, e.clientY, inputInfo); + } + + _onMouseDown(e) { + if (this._preventNextMouseDown) { + this._preventNextMouseDown = false; + this._preventNextClick = true; + e.preventDefault(); + e.stopPropagation(); + return false; + } + + switch (e.button) { + case 0: // Primary + this._scanTimerClear(); + this.clearSelection(false); + break; + case 1: // Middle + if (this._preventMiddleMouse) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + break; + } + } + + _onMouseOut() { + this._scanTimerClear(); + } + + _onClick(e) { + if (this._searchOnClick) { + const modifiers = DocumentUtil.getActiveModifiersAndButtons(e); + const modifierKeys = DocumentUtil.getActiveModifiers(e); + const inputInfo = this._createInputInfo(null, 'mouse', 'click', false, modifiers, modifierKeys); + this._searchAt(e.clientX, e.clientY, inputInfo); + } + + 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; + } + + const {clientX, clientY, identifier} = e.changedTouches[0]; + this._onPrimaryTouchStart(e, clientX, clientY, identifier); + } + + _onPrimaryTouchStart(e, x, y, identifier) { + this._preventScroll = false; + this._preventNextContextMenu = false; + this._preventNextMouseDown = false; + this._preventNextClick = false; + + if (DocumentUtil.isPointInSelection(x, y, window.getSelection())) { + return; + } + + this._primaryTouchIdentifier = identifier; + + this._searchAtFromTouchStart(e, x, y); + } + + _onTouchEnd(e) { + if ( + this._primaryTouchIdentifier === null || + this._getTouch(e.changedTouches, this._primaryTouchIdentifier) === null + ) { + return; + } + + this._onPrimaryTouchEnd(); + } + + _onPrimaryTouchEnd() { + 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. + // I.e. this._preventNextContextMenu and this._preventNextMouseDown should not be assigned to false. + } + + _onTouchCancel(e) { + this._onTouchEnd(e); + } + + _onTouchMove(e) { + if (!this._preventScroll || !e.cancelable || this._primaryTouchIdentifier === null) { + return; + } + + const primaryTouch = this._getTouch(e.changedTouches, this._primaryTouchIdentifier); + if (primaryTouch === null) { + return; + } + + const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchMove', e); + if (inputInfo === null) { return; } + + if (inputInfo.input.options.scanOnTouchMove) { + this._searchAt(primaryTouch.clientX, primaryTouch.clientY, inputInfo); + } + + e.preventDefault(); // Disable scroll + } + + _onPointerOver(e) { + const {pointerType, pointerId, isPrimary} = e; + if (pointerType === 'pen') { + this._pointerIdTypeMap.set(pointerId, pointerType); + } + + if (!isPrimary) { return; } + switch (pointerType) { + case 'mouse': return this._onMousePointerOver(e); + case 'touch': return this._onTouchPointerOver(e); + case 'pen': return this._onPenPointerOver(e); + } + } + + _onPointerDown(e) { + if (!e.isPrimary) { return; } + switch (this._getPointerEventType(e)) { + case 'mouse': return this._onMousePointerDown(e); + case 'touch': return this._onTouchPointerDown(e); + case 'pen': return this._onPenPointerDown(e); + } + } + + _onPointerMove(e) { + if (!e.isPrimary) { return; } + switch (this._getPointerEventType(e)) { + case 'mouse': return this._onMousePointerMove(e); + case 'touch': return this._onTouchPointerMove(e); + case 'pen': return this._onPenPointerMove(e); + } + } + + _onPointerUp(e) { + if (!e.isPrimary) { return; } + switch (this._getPointerEventType(e)) { + case 'mouse': return this._onMousePointerUp(e); + case 'touch': return this._onTouchPointerUp(e); + case 'pen': return this._onPenPointerUp(e); + } + } + + _onPointerCancel(e) { + this._pointerIdTypeMap.delete(e.pointerId); + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerCancel(e); + case 'touch': return this._onTouchPointerCancel(e); + case 'pen': return this._onPenPointerCancel(e); + } + } + + _onPointerOut(e) { + this._pointerIdTypeMap.delete(e.pointerId); + if (!e.isPrimary) { return; } + switch (e.pointerType) { + case 'mouse': return this._onMousePointerOut(e); + case 'touch': return this._onTouchPointerOut(e); + case 'pen': return this._onPenPointerOut(e); + } + } + + _onMousePointerOver(e) { + return this._onMouseOver(e); + } + + _onMousePointerDown(e) { + return this._onMouseDown(e); + } + + _onMousePointerMove(e) { + return this._onMouseMove(e); + } + + _onMousePointerUp() { + // NOP + } + + _onMousePointerCancel(e) { + return this._onMouseOut(e); + } + + _onMousePointerOut(e) { + return this._onMouseOut(e); + } + + _onTouchPointerOver() { + // NOP + } + + _onTouchPointerDown(e) { + const {clientX, clientY, pointerId} = e; + return this._onPrimaryTouchStart(e, clientX, clientY, pointerId); + } + + _onTouchPointerMove(e) { + if (!this._preventScroll || !e.cancelable) { + return; + } + + const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchMove', e); + if (inputInfo === null || !inputInfo.input.options.scanOnTouchMove) { return; } + + this._searchAt(e.clientX, e.clientY, inputInfo); + } + + _onTouchPointerUp() { + return this._onPrimaryTouchEnd(); + } + + _onTouchPointerCancel() { + return this._onPrimaryTouchEnd(); + } + + _onTouchPointerOut() { + // NOP + } + + _onTouchMovePreventScroll(e) { + if (!this._preventScroll) { return; } + + if (e.cancelable) { + e.preventDefault(); + } else { + this._preventScroll = false; + } + } + + _onPenPointerOver(e) { + this._penPointerPressed = false; + this._penPointerReleased = false; + this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerOver', false); + } + + _onPenPointerDown(e) { + this._penPointerPressed = true; + this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerDown', true); + } + + _onPenPointerMove(e) { + if (this._penPointerPressed && (!this._preventScroll || !e.cancelable)) { return; } + this._searchAtFromPen(e, e.clientX, e.clientY, 'pointerMove', true); + } + + _onPenPointerUp() { + this._penPointerPressed = false; + this._penPointerReleased = true; + this._preventScroll = false; + } + + _onPenPointerCancel(e) { + this._onPenPointerOut(e); + } + + _onPenPointerOut() { + this._penPointerPressed = false; + this._penPointerReleased = false; + this._preventScroll = false; + this._preventNextContextMenu = false; + this._preventNextMouseDown = false; + this._preventNextClick = false; + } + + async _scanTimerWait() { + const delay = this._delay; + const promise = promiseTimeout(delay, true); + this._scanTimerPromise = promise; + try { + return await promise; + } finally { + if (this._scanTimerPromise === promise) { + this._scanTimerPromise = null; + } + } + } + + _scanTimerClear() { + if (this._scanTimerPromise !== null) { + this._scanTimerPromise.resolve(false); + this._scanTimerPromise = null; + } + } + + _arePointerEventsSupported() { + return (this._pointerEventsEnabled && typeof PointerEvent !== 'undefined'); + } + + _hookEvents() { + let eventListenerInfos; + if (this._searchOnClickOnly) { + eventListenerInfos = this._getMouseClickOnlyEventListeners(); + } else if (this._arePointerEventsSupported()) { + eventListenerInfos = this._getPointerEventListeners(); + } else { + eventListenerInfos = this._getMouseEventListeners(); + if (this._touchInputEnabled) { + eventListenerInfos.push(...this._getTouchEventListeners()); + } + } + + for (const args of eventListenerInfos) { + this._eventListeners.addEventListener(...args); + } + } + + _getPointerEventListeners() { + return [ + [this._node, 'pointerover', this._onPointerOver.bind(this)], + [this._node, 'pointerdown', this._onPointerDown.bind(this)], + [this._node, 'pointermove', this._onPointerMove.bind(this)], + [this._node, 'pointerup', this._onPointerUp.bind(this)], + [this._node, 'pointercancel', this._onPointerCancel.bind(this)], + [this._node, 'pointerout', this._onPointerOut.bind(this)], + [this._node, 'touchmove', this._onTouchMovePreventScroll.bind(this), {passive: false}], + [this._node, 'mousedown', this._onMouseDown.bind(this)], + [this._node, 'click', this._onClick.bind(this)], + [this._node, 'auxclick', this._onAuxClick.bind(this)] + ]; + } + + _getMouseEventListeners() { + return [ + [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, 'click', this._onClick.bind(this)] + ]; + } + + _getMouseClickOnlyEventListeners() { + return [ + [this._node, 'click', this._onClick.bind(this)] + ]; + } + _getTouchEventListeners() { + return [ + [this._node, 'auxclick', this._onAuxClick.bind(this)], + [this._node, 'touchstart', this._onTouchStart.bind(this)], + [this._node, 'touchend', this._onTouchEnd.bind(this)], + [this._node, 'touchcancel', this._onTouchCancel.bind(this)], + [this._node, 'touchmove', this._onTouchMove.bind(this), {passive: false}], + [this._node, 'contextmenu', this._onContextMenu.bind(this)] + ]; + } + + _getTouch(touchList, identifier) { + for (const touch of touchList) { + if (touch.identifier === identifier) { + return touch; + } + } + return null; + } + + async _findDefinitions(textSource, searchTerms, searchKanji, optionsContext) { + if (textSource === null) { + return null; + } + if (searchTerms) { + const results = await this._findTerms(textSource, optionsContext); + if (results !== null) { return results; } + } + if (searchKanji) { + const results = await this._findKanji(textSource, optionsContext); + if (results !== null) { return results; } + } + return null; + } + + async _findTerms(textSource, optionsContext) { + const scanLength = this._scanLength; + const sentenceScanExtent = this._sentenceScanExtent; + const sentenceTerminatorMap = this._sentenceTerminatorMap; + const sentenceForwardQuoteMap = this._sentenceForwardQuoteMap; + const sentenceBackwardQuoteMap = this._sentenceBackwardQuoteMap; + 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, + layoutAwareScan, + sentenceScanExtent, + sentenceTerminatorMap, + sentenceForwardQuoteMap, + sentenceBackwardQuoteMap + ); + + return {definitions, sentence, type: 'terms'}; + } + + async _findKanji(textSource, optionsContext) { + const sentenceScanExtent = this._sentenceScanExtent; + const sentenceTerminatorMap = this._sentenceTerminatorMap; + const sentenceForwardQuoteMap = this._sentenceForwardQuoteMap; + const sentenceBackwardQuoteMap = this._sentenceBackwardQuoteMap; + 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, + layoutAwareScan, + sentenceScanExtent, + sentenceTerminatorMap, + sentenceForwardQuoteMap, + sentenceBackwardQuoteMap + ); + + return {definitions, sentence, type: 'kanji'}; + } + + async _searchAt(x, y, inputInfo) { + if (this._pendingLookup) { return; } + + try { + const sourceInput = inputInfo.input; + let searchTerms = this._searchTerms; + let searchKanji = this._searchKanji; + if (sourceInput !== null) { + if (searchTerms && !sourceInput.options.searchTerms) { searchTerms = false; } + if (searchKanji && !sourceInput.options.searchKanji) { searchKanji = false; } + } + + 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, searchTerms, searchKanji, inputInfo); + } finally { + if (textSource !== null) { + textSource.cleanup(); + } + } + } catch (e) { + yomichan.logError(e); + } finally { + this._pendingLookup = false; + } + } + + async _searchAtFromMouseMove(x, y, inputInfo) { + if (this._pendingLookup) { return; } + + if (inputInfo.passive) { + if (!await this._scanTimerWait()) { + // Aborted + return; + } + } + + await this._searchAt(x, y, inputInfo); + } + + async _searchAtFromTouchStart(e, x, y) { + if (this._pendingLookup) { return; } + + const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchStart', e); + if (inputInfo === null) { return; } + + const textSourceCurrentPrevious = this._textSourceCurrent !== null ? this._textSourceCurrent.clone() : null; + const preventScroll = inputInfo.input.options.preventTouchScrolling; + + await this._searchAt(x, y, inputInfo); + + if ( + this._textSourceCurrent !== null && + !this._textSourceCurrent.hasSameStart(textSourceCurrentPrevious) + ) { + this._preventScroll = preventScroll; + this._preventNextContextMenu = true; + this._preventNextMouseDown = true; + } + } + + async _searchAtFromPen(e, x, y, eventType, prevent) { + if (this._pendingLookup) { return; } + + const inputInfo = this._getMatchingInputGroupFromEvent('pen', eventType, e); + if (inputInfo === null) { return; } + + const {input: {options}} = inputInfo; + if ( + (!options.scanOnPenRelease && this._penPointerReleased) || + !(this._penPointerPressed ? options.scanOnPenPress : options.scanOnPenHover) + ) { + return; + } + + const preventScroll = inputInfo.input.options.preventTouchScrolling; + + await this._searchAt(x, y, inputInfo); + + if ( + prevent && + this._textSourceCurrent !== null + ) { + this._preventScroll = preventScroll; + this._preventNextContextMenu = true; + this._preventNextMouseDown = true; + this._preventNextClick = true; + } + } + + _getMatchingInputGroupFromEvent(pointerType, eventType, event) { + const modifiers = DocumentUtil.getActiveModifiersAndButtons(event); + const modifierKeys = DocumentUtil.getActiveModifiers(event); + return this._getMatchingInputGroup(pointerType, eventType, modifiers, modifierKeys); + } + + _getMatchingInputGroup(pointerType, eventType, modifiers, modifierKeys) { + let fallbackIndex = -1; + const modifiersSet = new Set(modifiers); + for (let i = 0, ii = this._inputs.length; i < ii; ++i) { + const input = this._inputs[i]; + const {include, exclude, types} = input; + if (!types.has(pointerType)) { continue; } + if (this._setHasAll(modifiersSet, include) && (exclude.length === 0 || !this._setHasAll(modifiersSet, exclude))) { + if (include.length > 0) { + return this._createInputInfo(input, pointerType, eventType, false, modifiers, modifierKeys); + } else if (fallbackIndex < 0) { + fallbackIndex = i; + } + } + } + + return ( + fallbackIndex >= 0 ? + this._createInputInfo(this._inputs[fallbackIndex], pointerType, eventType, true, modifiers, modifierKeys) : + null + ); + } + + _createInputInfo(input, pointerType, eventType, passive, modifiers, modifierKeys, detail) { + return {input, pointerType, eventType, passive, modifiers, modifierKeys, detail}; + } + + _setHasAll(set, values) { + for (const value of values) { + if (!set.has(value)) { + return false; + } + } + return true; + } + + _getInputArray(value) { + return ( + typeof value === 'string' ? + value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0) : + [] + ); + } + + _getInputTypeSet({mouse, touch, pen}) { + const set = new Set(); + if (mouse) { set.add('mouse'); } + if (touch) { set.add('touch'); } + if (pen) { set.add('pen'); } + return set; + } + + _getPointerEventType(e) { + // Workaround for Firefox bug not detecting certain 'touch' events as 'pen' events. + const cachedPointerType = this._pointerIdTypeMap.get(e.pointerId); + return (typeof cachedPointerType !== 'undefined' ? cachedPointerType : e.pointerType); + } + + _constrainTextSource(textSource, includeSelector, excludeSelector, layoutAwareScan) { + let length = textSource.text().length; + while (length > 0) { + const nodes = textSource.getNodesInRange(); + if ( + (includeSelector !== null && !DocumentUtil.everyNodeMatchesSelector(nodes, includeSelector)) || + (excludeSelector !== null && DocumentUtil.anyNodeMatchesSelector(nodes, excludeSelector)) + ) { + --length; + textSource.setEndOffset(length, layoutAwareScan); + } else { + break; + } + } + } +} |