diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json index e2dd0573..52d96db9 100644 --- a/ext/bg/data/options-schema.json +++ b/ext/bg/data/options-schema.json @@ -324,6 +324,7 @@ "alphanumeric", "autoHideResults", "delay", + "hideDelay", "length", "modifier", "deepDomScan", @@ -360,6 +361,11 @@ "minimum": 0, "default": 20 }, + "hideDelay": { + "type": "number", + "minimum": 0, + "default": 0 + }, "length": { "type": "integer", "minimum": 1, diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 9dc0c166..398fb95c 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -473,6 +473,7 @@ class OptionsUtil { // Options conditions converted to string representations. // Added usePopupWindow. // Updated handlebars templates to include "clipboard-image" definition. + // Added hideDelay. for (const {conditionGroups} of options.profiles) { for (const {conditions} of conditionGroups) { for (const condition of conditions) { @@ -487,6 +488,7 @@ class OptionsUtil { } for (const {options: profileOptions} of options.profiles) { profileOptions.general.usePopupWindow = false; + profileOptions.scanning.hideDelay = 0; } await this._addFieldTemplatesToOptions(options, '/bg/data/anki-field-templates-upgrade-v4.handlebars'); return options; diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 0ad5b79b..3fa14f49 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -415,8 +415,16 @@
- - +
+
+ + +
+
+ + +
+
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 9177f985..e92feaf9 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -61,7 +61,10 @@ class Frontend { this._popupFactory = popupFactory; this._allowRootFramePopupProxy = allowRootFramePopupProxy; this._popupCache = new Map(); + this._popupEventListeners = new EventListenerCollection(); this._updatePopupToken = null; + this._clearSelectionTimer = null; + this._isPointerOverPopup = false; this._runtimeMessageHandlers = new Map([ ['requestFrontendReadyBroadcast', {async: false, handler: this._onMessageRequestFrontendReadyBroadcast.bind(this)}] @@ -175,7 +178,7 @@ class Frontend { } _onApiClosePopup() { - this._textScanner.clearSelection(false); + this._clearSelection(false); } _onApiCopySelection() { @@ -232,9 +235,11 @@ class Frontend { } _onClearSelection({passive}) { + this._stopClearSelectionDelayed(); if (this._popup !== null) { this._popup.hide(!passive); this._popup.clearAutoPlayTimer(); + this._isPointerOverPopup = false; } this._updatePendingOptions(); } @@ -249,24 +254,61 @@ class Frontend { await this.updateOptions(); } - _onSearched({textScanner, type, definitions, sentence, input: {cause}, textSource, optionsContext, error}) { + _onSearched({type, definitions, sentence, input: {cause}, textSource, optionsContext, error}) { + const scanningOptions = this._options.scanning; + if (error !== null) { if (yomichan.isExtensionUnloaded) { - if (textSource !== null && this._options.scanning.modifier !== 'none') { + if (textSource !== null && scanningOptions.modifier !== 'none') { this._showExtensionUnloaded(textSource); } } else { yomichan.logError(error); } + } if (type !== null) { + this._stopClearSelectionDelayed(); + const focus = (cause === 'mouse'); + this._showContent(textSource, focus, definitions, type, sentence, optionsContext); } else { - if (type !== null) { - const focus = (cause === 'mouse'); - this._showContent(textSource, focus, definitions, type, sentence, optionsContext); + if (scanningOptions.autoHideResults) { + this._clearSelectionDelayed(scanningOptions.hideDelay, false); } } + } - if (type === null && this._options.scanning.autoHideResults) { - textScanner.clearSelection(false); + _onPopupFramePointerOver() { + this._isPointerOverPopup = true; + this._stopClearSelectionDelayed(); + } + + _onPopupFramePointerOut() { + this._isPointerOverPopup = false; + } + + _clearSelection(passive) { + this._stopClearSelectionDelayed(); + this._textScanner.clearSelection(passive); + } + + _clearSelectionDelayed(delay, restart, passive) { + if (!this._textScanner.hasSelection()) { return; } + if (delay > 0) { + if (this._clearSelectionTimer !== null && !restart) { return; } // Already running + this._stopClearSelectionDelayed(); + this._clearSelectionTimer = setTimeout(() => { + this._clearSelectionTimer = null; + if (this._isPointerOverPopup) { return; } + this._clearSelection(passive); + }, delay); + } else { + this._clearSelection(passive); + } + } + + _stopClearSelectionDelayed() { + if (this._clearSelectionTimer !== null) { + clearTimeout(this._clearSelectionTimer); + this._clearSelectionTimer = null; } } @@ -354,8 +396,12 @@ class Frontend { this.setDisabledOverride(!this._options.scanning.enableOnSearchPage); } - this._textScanner.clearSelection(true); + this._clearSelection(true); + this._popupEventListeners.removeAllEventListeners(); this._popup = popup; + this._popupEventListeners.on(popup, 'framePointerOver', this._onPopupFramePointerOver.bind(this)); + this._popupEventListeners.on(popup, 'framePointerOut', this._onPopupFramePointerOut.bind(this)); + this._isPointerOverPopup = false; } async _getDefaultPopup() { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index f644ee98..ee3bf646 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -95,6 +95,8 @@ class Popup extends EventDispatcher { // Public functions prepare() { + this._frame.addEventListener('mouseover', this._onFrameMouseOver.bind(this)); + this._frame.addEventListener('mouseout', this._onFrameMouseOut.bind(this)); this._frame.addEventListener('mousedown', (e) => e.stopPropagation()); this._frame.addEventListener('scroll', (e) => e.stopPropagation()); this._frame.addEventListener('load', this._onFrameLoad.bind(this)); @@ -207,6 +209,14 @@ class Popup extends EventDispatcher { // Private functions + _onFrameMouseOver() { + this.trigger('framePointerOver', {}); + } + + _onFrameMouseOut() { + this.trigger('framePointerOut', {}); + } + _inject() { let injectPromise = this._injectPromise; if (injectPromise === null) { diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 2410f2b7..f3e99577 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -144,6 +144,10 @@ class TextScanner extends EventDispatcher { return clonedTextSource.text(); } + hasSelection() { + return (this._textSourceCurrent !== null); + } + clearSelection(passive) { if (!this._canClearSelection) { return; } if (this._textSourceCurrent !== null) {