Text scanner improvements (#1056)

* Only ignore nodes on non-web pages

* Fix issue where options might not be assigned on nested frontends

* Refactor default TextScanner options

* Add option to enable search only on click

* Simplify restore state assignment

* Update options context passing

* Fix empty title

* Use TextScanner to scan content inside of Display

* Rename ignoreNodes to excludeSelector(s)

* Fix options update incorrectly triggering a re-search

* Fix copy throwing an error on the search page

* Replace _onSearchQueryUpdated with _search

* Use include selector instead of exclude selector
This commit is contained in:
toasted-nutbread 2020-11-23 20:31:48 -05:00 committed by GitHub
parent 12e5cec99c
commit 068b1eef71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 214 additions and 168 deletions

View File

@ -34,8 +34,6 @@ class QueryParser extends EventDispatcher {
this._queryParserModeSelect = document.querySelector('#query-parser-mode-select'); this._queryParserModeSelect = document.querySelector('#query-parser-mode-select');
this._textScanner = new TextScanner({ this._textScanner = new TextScanner({
node: this._queryParser, node: this._queryParser,
ignoreElements: () => [],
ignorePoint: null,
getOptionsContext, getOptionsContext,
documentUtil, documentUtil,
searchTerms: true, searchTerms: true,

View File

@ -62,7 +62,7 @@ class DisplaySearch extends Display {
async prepare() { async prepare() {
await super.prepare(); await super.prepare();
await this.updateOptions(); await this.updateOptions();
yomichan.on('optionsUpdated', () => this.updateOptions()); yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this));
this.on('contentUpdating', this._onContentUpdating.bind(this)); this.on('contentUpdating', this._onContentUpdating.bind(this));
this.on('modeChange', this._onModeChange.bind(this)); this.on('modeChange', this._onModeChange.bind(this));
@ -126,15 +126,6 @@ class DisplaySearch extends Display {
} }
} }
async updateOptions() {
await super.updateOptions();
if (!this._isPrepared) { return; }
const query = this._queryInput.value;
if (query) {
this._onSearchQueryUpdated(query, false);
}
}
postProcessQuery(query) { postProcessQuery(query) {
if (this._wanakanaEnabled) { if (this._wanakanaEnabled) {
try { try {
@ -148,6 +139,14 @@ class DisplaySearch extends Display {
// Private // Private
async _onOptionsUpdated() {
await this.updateOptions();
const query = this._queryInput.value;
if (query) {
this._search(false);
}
}
_onContentUpdating({type, content, source}) { _onContentUpdating({type, content, source}) {
let animate = false; let animate = false;
let valid = false; let valid = false;
@ -183,12 +182,12 @@ class DisplaySearch extends Display {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
this.blurElement(e.currentTarget); this.blurElement(e.currentTarget);
this._search(); this._search(true);
} }
_onSearch(e) { _onSearch(e) {
e.preventDefault(); e.preventDefault();
this._search(); this._search(true);
} }
_onCopy() { _onCopy() {
@ -197,27 +196,8 @@ class DisplaySearch extends Display {
} }
_onExternalSearchUpdate({text, animate=true}) { _onExternalSearchUpdate({text, animate=true}) {
this._onSearchQueryUpdated(text, animate); this._queryInput.value = text;
} this._search(animate);
_onSearchQueryUpdated(query, animate) {
const details = {
focus: false,
history: false,
params: {
query
},
state: {
focusEntry: 0,
sentence: {text: query, offset: 0},
url: window.location.href
},
content: {
definitions: null,
animate
}
};
this.setContent(details);
} }
_onWanakanaEnableChange(e) { _onWanakanaEnableChange(e) {
@ -362,9 +342,25 @@ class DisplaySearch extends Display {
}); });
} }
_search() { _search(animate) {
const query = this._queryInput.value; const query = this._queryInput.value;
this._onSearchQueryUpdated(query, true); const details = {
focus: false,
history: false,
params: {
query
},
state: {
focusEntry: 0,
sentence: {text: query, offset: 0},
url: window.location.href
},
content: {
definitions: null,
animate
}
};
this.setContent(details);
} }
_updateSearchHeight() { _updateSearchHeight() {

View File

@ -322,11 +322,13 @@ class Frontend {
}); });
this._updateTextScannerEnabled(); this._updateTextScannerEnabled();
const ignoreNodes = ['.scan-disable', '.scan-disable *']; if (this._pageType !== 'web') {
if (!this._options.scanning.enableOnPopupExpressions) { const excludeSelectors = ['.scan-disable', '.scan-disable *'];
ignoreNodes.push('.source-text', '.source-text *'); if (!scanningOptions.enableOnPopupExpressions) {
excludeSelectors.push('.source-text', '.source-text *');
}
this._textScanner.excludeSelector = excludeSelectors.join(',');
} }
this._textScanner.ignoreNodes = ignoreNodes.join(',');
this._updateContentScale(); this._updateContentScale();
@ -527,7 +529,7 @@ class Frontend {
} }
_updateTextScannerEnabled() { _updateTextScannerEnabled() {
const enabled = (this._options.general.enable && !this._disabledOverride); const enabled = (this._options !== null && this._options.general.enable && !this._disabledOverride);
this._textScanner.setEnabled(enabled); this._textScanner.setEnabled(enabled);
} }

View File

@ -27,6 +27,7 @@
* PopupFactory * PopupFactory
* QueryParser * QueryParser
* TemplateRendererProxy * TemplateRendererProxy
* TextScanner
* WindowScroll * WindowScroll
* api * api
* dynamicLoader * dynamicLoader
@ -48,7 +49,6 @@ class Display extends EventDispatcher {
}); });
this._styleNode = null; this._styleNode = null;
this._eventListeners = new EventListenerCollection(); this._eventListeners = new EventListenerCollection();
this._clickScanPrevent = false;
this._setContentToken = null; this._setContentToken = null;
this._autoPlayAudioTimer = null; this._autoPlayAudioTimer = null;
this._autoPlayAudioDelay = 400; this._autoPlayAudioDelay = 400;
@ -104,6 +104,7 @@ class Display extends EventDispatcher {
this._frameEndpoint = (pageType === 'popup' ? new FrameEndpoint() : null); this._frameEndpoint = (pageType === 'popup' ? new FrameEndpoint() : null);
this._browser = null; this._browser = null;
this._copyTextarea = null; this._copyTextarea = null;
this._definitionTextScanner = null;
this.registerActions([ this.registerActions([
['close', () => { this.onEscape(); }], ['close', () => { this.onEscape(); }],
@ -311,6 +312,7 @@ class Display extends EventDispatcher {
}); });
this._updateNestedFrontend(options); this._updateNestedFrontend(options);
this._updateDefinitionTextScanner(options);
} }
autoPlayAudio() { autoPlayAudio() {
@ -348,6 +350,7 @@ class Display extends EventDispatcher {
const url = `${location.protocol}//${location.host}${location.pathname}?${urlSearchParams.toString()}`; const url = `${location.protocol}//${location.host}${location.pathname}?${urlSearchParams.toString()}`;
if (history && this._historyHasChanged) { if (history && this._historyHasChanged) {
this._updateHistoryState();
this._history.pushState(state, content, url); this._history.pushState(state, content, url);
} else { } else {
this._history.clear(); this._history.clear();
@ -648,24 +651,18 @@ class Display extends EventDispatcher {
e.preventDefault(); e.preventDefault();
if (!this._historyHasState()) { return; } if (!this._historyHasState()) { return; }
const link = e.target; const {state: {sentence}} = this._history;
const {state} = this._history; const optionsContext = this.getOptionsContext();
const query = e.currentTarget.textContent;
state.focusEntry = this._getClosestDefinitionIndex(link); const definitions = await api.kanjiFind(query, optionsContext);
state.scrollX = this._windowScroll.x;
state.scrollY = this._windowScroll.y;
this._historyStateUpdate(state);
const query = link.textContent;
const definitions = await api.kanjiFind(query, this.getOptionsContext());
const details = { const details = {
focus: false, focus: false,
history: true, history: true,
params: this._createSearchParams('kanji', query, false), params: this._createSearchParams('kanji', query, false),
state: { state: {
focusEntry: 0, focusEntry: 0,
sentence: state.sentence, sentence,
optionsContext: state.optionsContext optionsContext
}, },
content: { content: {
definitions definitions
@ -677,88 +674,6 @@ class Display extends EventDispatcher {
} }
} }
_onGlossaryMouseDown(e) {
if (DocumentUtil.isMouseButtonPressed(e, 'primary')) {
this._clickScanPrevent = false;
}
}
_onGlossaryMouseMove() {
this._clickScanPrevent = true;
}
_onGlossaryMouseUp(e) {
if (!this._clickScanPrevent && DocumentUtil.isMouseButtonPressed(e, 'primary')) {
try {
this._onTermLookup(e);
} catch (error) {
this.onError(error);
}
}
}
async _onTermLookup(e) {
if (!this._historyHasState()) { return; }
const termLookupResults = await this._termLookup(e);
if (!termLookupResults || !this._historyHasState()) { return; }
const {state} = this._history;
const {textSource, definitions} = termLookupResults;
const scannedElement = e.target;
const sentenceExtent = this._options.anki.sentenceExt;
const layoutAwareScan = this._options.scanning.layoutAwareScan;
const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan);
state.focusEntry = this._getClosestDefinitionIndex(scannedElement);
state.scrollX = this._windowScroll.x;
state.scrollY = this._windowScroll.y;
this._historyStateUpdate(state);
const query = textSource.text();
const details = {
focus: false,
history: true,
params: this._createSearchParams('terms', query, false),
state: {
focusEntry: 0,
sentence,
optionsContext: state.optionsContext
},
content: {
definitions
}
};
this.setContent(details);
}
async _termLookup(e) {
e.preventDefault();
const {length: scanLength, deepDomScan: deepScan, layoutAwareScan} = this._options.scanning;
const textSource = this._documentUtil.getRangeFromPoint(e.clientX, e.clientY, deepScan);
if (textSource === null) {
return false;
}
let definitions, length;
try {
textSource.setEndOffset(scanLength, layoutAwareScan);
({definitions, length} = await api.termsFind(textSource.text(), {}, this.getOptionsContext()));
if (definitions.length === 0) {
return false;
}
textSource.setEndOffset(length, layoutAwareScan);
} finally {
textSource.cleanup();
}
return {textSource, definitions};
}
_onAudioPlay(e) { _onAudioPlay(e) {
e.preventDefault(); e.preventDefault();
const link = e.currentTarget; const link = e.currentTarget;
@ -942,7 +857,7 @@ class Display extends EventDispatcher {
if (this._setContentToken !== token) { return true; } if (this._setContentToken !== token) { return true; }
if (changeHistory) { if (changeHistory) {
this._historyStateUpdate(state, content); this._replaceHistoryStateNoNavigate(state, content);
} }
eventArgs.source = source; eventArgs.source = source;
@ -1054,17 +969,17 @@ class Display extends EventDispatcher {
} }
_setTitleText(text) { _setTitleText(text) {
let title = ''; let title = this._defaultTitle;
if (text.length > 0) { if (text.length > 0) {
// Chrome limits title to 1024 characters // Chrome limits title to 1024 characters
const ellipsis = '...'; const ellipsis = '...';
const separator = ' - '; const separator = ' - ';
const maxLength = this._titleMaxLength - this._defaultTitle.length - separator.length; const maxLength = this._titleMaxLength - title.length - separator.length;
if (text.length > maxLength) { if (text.length > maxLength) {
text = `${text.substring(0, Math.max(0, maxLength - ellipsis.length))}${ellipsis}`; text = `${text.substring(0, Math.max(0, maxLength - ellipsis.length))}${ellipsis}`;
} }
title = `${text}${separator}${this._defaultTitle}`; title = `${text}${separator}${title}`;
} }
document.title = title; document.title = title;
} }
@ -1384,12 +1299,20 @@ class Display extends EventDispatcher {
return isObject(this._history.state); return isObject(this._history.state);
} }
_historyStateUpdate(state, content) { _updateHistoryState() {
const {state, content} = this._history;
if (!isObject(state)) { return; }
state.focusEntry = this._index;
state.scrollX = this._windowScroll.x;
state.scrollY = this._windowScroll.y;
this._replaceHistoryStateNoNavigate(state, content);
}
_replaceHistoryStateNoNavigate(state, content) {
const historyChangeIgnorePre = this._historyChangeIgnore; const historyChangeIgnorePre = this._historyChangeIgnore;
try { try {
this._historyChangeIgnore = true; this._historyChangeIgnore = true;
if (typeof state === 'undefined') { state = this._history.state; }
if (typeof content === 'undefined') { content = this._history.content; }
this._history.replaceState(state, content); this._history.replaceState(state, content);
} finally { } finally {
this._historyChangeIgnore = historyChangeIgnorePre; this._historyChangeIgnore = historyChangeIgnorePre;
@ -1702,7 +1625,7 @@ class Display extends EventDispatcher {
} }
_copyHostSelection() { _copyHostSelection() {
if (window.getSelection().toString()) { return false; } if (this._ownerFrameId === null || window.getSelection().toString()) { return false; }
this._copyHostSelectionInner(); this._copyHostSelectionInner();
return true; return true;
} }
@ -1766,10 +1689,89 @@ class Display extends EventDispatcher {
this._addMultipleEventListeners(entry, '.action-play-audio', 'click', this._onAudioPlay.bind(this)); this._addMultipleEventListeners(entry, '.action-play-audio', 'click', this._onAudioPlay.bind(this));
this._addMultipleEventListeners(entry, '.kanji-link', 'click', this._onKanjiLookup.bind(this)); this._addMultipleEventListeners(entry, '.kanji-link', 'click', this._onKanjiLookup.bind(this));
this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this)); this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this));
if (this._options !== null && this._options.scanning.enablePopupSearch) { }
this._addMultipleEventListeners(entry, '.term-glossary-item,.tag', 'mouseup', this._onGlossaryMouseUp.bind(this));
this._addMultipleEventListeners(entry, '.term-glossary-item,.tag', 'mousedown', this._onGlossaryMouseDown.bind(this)); _updateDefinitionTextScanner(options) {
this._addMultipleEventListeners(entry, '.term-glossary-item,.tag', 'mousemove', this._onGlossaryMouseMove.bind(this)); if (!options.scanning.enablePopupSearch) {
if (this._definitionTextScanner !== null) {
this._definitionTextScanner.setEnabled(false);
}
return;
} }
if (this._definitionTextScanner === null) {
this._definitionTextScanner = new TextScanner({
node: window,
getOptionsContext: this.getOptionsContext.bind(this),
documentUtil: this._documentUtil,
searchTerms: true,
searchKanji: false,
searchOnClick: true,
searchOnClickOnly: true
});
this._definitionTextScanner.prepare();
this._definitionTextScanner.on('searched', this._onDefinitionTextScannerSearched.bind(this));
}
const scanningOptions = options.scanning;
this._definitionTextScanner.setOptions({
inputs: [{
include: 'mouse0',
exclude: '',
types: {mouse: true, pen: false, touch: false},
options: {
searchTerms: true,
searchKanji: true,
scanOnTouchMove: false,
scanOnPenHover: false,
scanOnPenPress: false,
scanOnPenRelease: false,
preventTouchScrolling: false
}
}],
deepContentScan: scanningOptions.deepDomScan,
selectText: false,
delay: scanningOptions.delay,
touchInputEnabled: false,
pointerEventsEnabled: false,
scanLength: scanningOptions.length,
sentenceExtent: options.anki.sentenceExt,
layoutAwareScan: scanningOptions.layoutAwareScan,
preventMiddleMouse: false
});
const includeSelector = '.term-glossary-item,.term-glossary-item *,.tag,.tag *';
this._definitionTextScanner.includeSelector = includeSelector;
this._definitionTextScanner.setEnabled(true);
}
_onDefinitionTextScannerSearched({type, definitions, sentence, textSource, optionsContext, error}) {
if (error !== null && !yomichan.isExtensionUnloaded) {
yomichan.logError(error);
}
if (type === null) { return; }
const query = textSource.text();
const details = {
focus: false,
history: true,
params: {
type,
query,
wildcards: 'off'
},
state: {
focusEntry: 0,
sentence,
optionsContext
},
content: {
definitions
}
};
this._definitionTextScanner.clearSelection(true);
this.setContent(details);
} }
} }

View File

@ -261,6 +261,18 @@ class DocumentUtil {
return false; return false;
} }
static everyNodeMatchesSelector(nodes, selector) {
const ELEMENT_NODE = Node.ELEMENT_NODE;
for (let node of nodes) {
while (true) {
if (node === null) { return false; }
if (node.nodeType === ELEMENT_NODE && node.matches(selector)) { break; }
node = node.parentNode;
}
}
return true;
}
static getModifierKeys(os) { static getModifierKeys(os) {
switch (os) { switch (os) {
case 'win': case 'win':

View File

@ -21,19 +21,31 @@
*/ */
class TextScanner extends EventDispatcher { class TextScanner extends EventDispatcher {
constructor({node, ignoreElements, ignorePoint, documentUtil, getOptionsContext, searchTerms=false, searchKanji=false, searchOnClick=false}) { constructor({
node,
documentUtil,
getOptionsContext,
ignoreElements=null,
ignorePoint=null,
searchTerms=false,
searchKanji=false,
searchOnClick=false,
searchOnClickOnly=false
}) {
super(); super();
this._node = node; this._node = node;
this._ignoreElements = ignoreElements;
this._ignorePoint = ignorePoint;
this._documentUtil = documentUtil; this._documentUtil = documentUtil;
this._getOptionsContext = getOptionsContext; this._getOptionsContext = getOptionsContext;
this._ignoreElements = ignoreElements;
this._ignorePoint = ignorePoint;
this._searchTerms = searchTerms; this._searchTerms = searchTerms;
this._searchKanji = searchKanji; this._searchKanji = searchKanji;
this._searchOnClick = searchOnClick; this._searchOnClick = searchOnClick;
this._searchOnClickOnly = searchOnClickOnly;
this._isPrepared = false; this._isPrepared = false;
this._ignoreNodes = null; this._includeSelector = null;
this._excludeSelector = null;
this._inputInfoCurrent = null; this._inputInfoCurrent = null;
this._scanTimerPromise = null; this._scanTimerPromise = null;
@ -76,12 +88,20 @@ class TextScanner extends EventDispatcher {
this._canClearSelection = value; this._canClearSelection = value;
} }
get ignoreNodes() { get includeSelector() {
return this._ignoreNodes; return this._includeSelector;
} }
set ignoreNodes(value) { set includeSelector(value) {
this._ignoreNodes = value; this._includeSelector = value;
}
get excludeSelector() {
return this._excludeSelector;
}
set excludeSelector(value) {
this._excludeSelector = value;
} }
prepare() { prepare() {
@ -178,15 +198,8 @@ class TextScanner extends EventDispatcher {
clonedTextSource.setEndOffset(length, layoutAwareScan); clonedTextSource.setEndOffset(length, layoutAwareScan);
if (this._ignoreNodes !== null) { if (this._excludeSelector !== null) {
length = clonedTextSource.text().length; this._constrainTextSource(clonedTextSource, this._includeSelector, this._excludeSelector, layoutAwareScan);
while (
length > 0 &&
DocumentUtil.anyNodeMatchesSelector(clonedTextSource.getNodesInRange(), this._ignoreNodes)
) {
--length;
clonedTextSource.setEndOffset(length, layoutAwareScan);
}
} }
return clonedTextSource.text(); return clonedTextSource.text();
@ -287,7 +300,7 @@ class TextScanner extends EventDispatcher {
} }
_onMouseOver(e) { _onMouseOver(e) {
if (this._ignoreElements().includes(e.target)) { if (this._ignoreElements !== null && this._ignoreElements().includes(e.target)) {
this._scanTimerClear(); this._scanTimerClear();
} }
} }
@ -613,7 +626,9 @@ class TextScanner extends EventDispatcher {
_hookEvents() { _hookEvents() {
let eventListenerInfos; let eventListenerInfos;
if (this._arePointerEventsSupported()) { if (this._searchOnClickOnly) {
eventListenerInfos = this._getMouseClickOnlyEventListeners();
} else if (this._arePointerEventsSupported()) {
eventListenerInfos = this._getPointerEventListeners(); eventListenerInfos = this._getPointerEventListeners();
} else { } else {
eventListenerInfos = this._getMouseEventListeners(); eventListenerInfos = this._getMouseEventListeners();
@ -652,6 +667,11 @@ class TextScanner extends EventDispatcher {
]; ];
} }
_getMouseClickOnlyEventListeners() {
return [
[this._node, 'click', this._onClick.bind(this)]
];
}
_getTouchEventListeners() { _getTouchEventListeners() {
return [ return [
[this._node, 'auxclick', this._onAuxClick.bind(this)], [this._node, 'auxclick', this._onAuxClick.bind(this)],
@ -873,4 +893,20 @@ class TextScanner extends EventDispatcher {
const cachedPointerType = this._pointerIdTypeMap.get(e.pointerId); const cachedPointerType = this._pointerIdTypeMap.get(e.pointerId);
return (typeof cachedPointerType !== 'undefined' ? cachedPointerType : e.pointerType); 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;
}
}
}
} }