From 02a34bb4bc81471934e54b5440a20585527d76f5 Mon Sep 17 00:00:00 2001
From: siikamiika <siikamiika@users.noreply.github.com>
Date: Thu, 5 Dec 2019 03:58:35 +0200
Subject: initial text scanner extract

---
 ext/mixed/js/text-scanner.js | 341 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 341 insertions(+)
 create mode 100644 ext/mixed/js/text-scanner.js

(limited to 'ext/mixed')

diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
new file mode 100644
index 00000000..cf6e5397
--- /dev/null
+++ b/ext/mixed/js/text-scanner.js
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+
+class TextScanner {
+    constructor(node, ignoreNodes, popup, onTextSearch) {
+        this.node = node;
+        this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null);
+        this.popup = popup;
+        this.onTextSearch = onTextSearch;
+
+        this.popupTimerPromise = null;
+        this.textSourceCurrent = null;
+        this.pendingLookup = false;
+        this.options = null;
+
+        this.enabled = false;
+        this.eventListeners = [];
+
+        this.primaryTouchIdentifier = null;
+        this.preventNextContextMenu = false;
+        this.preventNextMouseDown = false;
+        this.preventNextClick = false;
+        this.preventScroll = false;
+    }
+
+    onMouseOver(e) {
+        if (this.popup && e.target === this.popup.container) {
+            this.popupTimerClear();
+        }
+    }
+
+    onMouseMove(e) {
+        this.popupTimerClear();
+
+        if (this.pendingLookup || DOM.isMouseButtonDown(e, 'primary')) {
+            return;
+        }
+
+        const scanningOptions = this.options.scanning;
+        const scanningModifier = scanningOptions.modifier;
+        if (!(
+            TextScanner.isScanningModifierPressed(scanningModifier, e) ||
+            (scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary'))
+        )) {
+            return;
+        }
+
+        const search = async () => {
+            if (scanningModifier === 'none') {
+                if (!await this.popupTimerWait()) {
+                    // Aborted
+                    return;
+                }
+            }
+
+            await this.searchAt(e.clientX, e.clientY, 'mouse');
+        };
+
+        search();
+    }
+
+    onMouseDown(e) {
+        if (this.preventNextMouseDown) {
+            this.preventNextMouseDown = false;
+            this.preventNextClick = true;
+            e.preventDefault();
+            e.stopPropagation();
+            return false;
+        }
+
+        if (DOM.isMouseButtonPressed(e, 'primary')) {
+            this.popupTimerClear();
+            this.searchClear();
+        }
+    }
+
+    onMouseOut() {
+        this.popupTimerClear();
+    }
+
+    onClick(e) {
+        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;
+        }
+
+        this.preventScroll = false;
+        this.preventNextContextMenu = false;
+        this.preventNextMouseDown = false;
+        this.preventNextClick = false;
+
+        const primaryTouch = e.changedTouches[0];
+        if (DOM.isPointInSelection(primaryTouch.clientX, primaryTouch.clientY, this.node.getSelection())) {
+            return;
+        }
+
+        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;
+        });
+    }
+
+    onTouchEnd(e) {
+        if (
+            this.primaryTouchIdentifier === null ||
+            TextScanner.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0
+        ) {
+            return;
+        }
+
+        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.
+        // this.preventNextContextMenu = false;
+        // this.preventNextMouseDown = false;
+    }
+
+    onTouchCancel(e) {
+        this.onTouchEnd(e);
+    }
+
+    onTouchMove(e) {
+        if (!this.preventScroll || !e.cancelable || this.primaryTouchIdentifier === null) {
+            return;
+        }
+
+        const touches = e.changedTouches;
+        const index = TextScanner.getIndexOfTouch(touches, this.primaryTouchIdentifier);
+        if (index < 0) {
+            return;
+        }
+
+        const primaryTouch = touches[index];
+        this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove');
+
+        e.preventDefault(); // Disable scroll
+    }
+
+    async popupTimerWait() {
+        const delay = this.options.scanning.delay;
+        const promise = promiseTimeout(delay, true);
+        this.popupTimerPromise = promise;
+        try {
+            return await promise;
+        } finally {
+            if (this.popupTimerPromise === promise) {
+                this.popupTimerPromise = null;
+            }
+        }
+    }
+
+    popupTimerClear() {
+        if (this.popupTimerPromise !== null) {
+            this.popupTimerPromise.resolve(false);
+            this.popupTimerPromise = null;
+        }
+    }
+
+    setEnabled(enabled) {
+        if (enabled) {
+            if (!this.enabled) {
+                this.hookEvents();
+                this.enabled = true;
+            }
+        } else {
+            if (this.enabled) {
+                this.clearEventListeners();
+                this.enabled = false;
+            }
+            this.searchClear();
+        }
+    }
+
+    hookEvents() {
+        this.addEventListener('mousedown', this.onMouseDown.bind(this));
+        this.addEventListener('mousemove', this.onMouseMove.bind(this));
+        this.addEventListener('mouseover', this.onMouseOver.bind(this));
+        this.addEventListener('mouseout', this.onMouseOut.bind(this));
+
+        if (this.options.scanning.touchInputEnabled) {
+            this.addEventListener('click', this.onClick.bind(this));
+            this.addEventListener('auxclick', this.onAuxClick.bind(this));
+            this.addEventListener('touchstart', this.onTouchStart.bind(this));
+            this.addEventListener('touchend', this.onTouchEnd.bind(this));
+            this.addEventListener('touchcancel', this.onTouchCancel.bind(this));
+            this.addEventListener('touchmove', this.onTouchMove.bind(this), {passive: false});
+            this.addEventListener('contextmenu', this.onContextMenu.bind(this));
+        }
+    }
+
+    addEventListener(type, listener, options) {
+        this.node.addEventListener(type, listener, options);
+        this.eventListeners.push([type, listener, options]);
+    }
+
+    clearEventListeners() {
+        for (const [type, listener, options] of this.eventListeners) {
+            this.node.removeEventListener(type, listener, options);
+        }
+        this.eventListeners = [];
+    }
+
+    setOptions(options) {
+        this.options = options;
+    }
+
+    async searchAt(x, y, cause) {
+        try {
+            this.popupTimerClear();
+
+            if (this.pendingLookup || (this.popup && await this.popup.containsPoint(x, y))) {
+                return;
+            }
+
+            const textSource = docRangeFromPoint(x, y, this.options);
+            if (this.textSourceCurrent !== null && this.textSourceCurrent.equals(textSource)) {
+                return;
+            }
+
+            try {
+                await this.onTextSearch(textSource, cause);
+            } finally {
+                if (textSource !== null) {
+                    textSource.cleanup();
+                }
+            }
+        } catch (e) {
+            this.onError(e);
+        }
+    }
+
+    setTextSourceScanLength(textSource, length) {
+        textSource.setEndOffset(length);
+        if (this.ignoreNodes === null || !textSource.range) {
+            return;
+        }
+
+        length = textSource.text().length;
+        while (textSource.range && length > 0) {
+            const nodes = TextSourceRange.getNodesInRange(textSource.range);
+            if (!TextSourceRange.anyNodeMatchesSelector(nodes, this.ignoreNodes)) {
+                break;
+            }
+            --length;
+            textSource.setEndOffset(length);
+        }
+    }
+
+    searchClear() {
+        if (this.textSourceCurrent !== null) {
+            if (this.options.scanning.selectText) {
+                this.textSourceCurrent.deselect();
+            }
+            this.textSourceCurrent = null;
+        }
+    }
+
+    getCurrentTextSource() {
+        return this.textSourceCurrent;
+    }
+
+    setCurrentTextSource(textSource) {
+        return this.textSourceCurrent = textSource;
+    }
+
+    static isScanningModifierPressed(scanningModifier, mouseEvent) {
+        switch (scanningModifier) {
+            case 'alt': return mouseEvent.altKey;
+            case 'ctrl': return mouseEvent.ctrlKey;
+            case 'shift': return mouseEvent.shiftKey;
+            case 'none': return true;
+            default: return false;
+        }
+    }
+
+    static getIndexOfTouch(touchList, identifier) {
+        for (const i in touchList) {
+            const t = touchList[i];
+            if (t.identifier === identifier) {
+                return i;
+            }
+        }
+        return -1;
+    }
+}
-- 
cgit v1.2.3


From e5be42d3de14be48c6ef4ef47d06ba130d16fcaf Mon Sep 17 00:00:00 2001
From: siikamiika <siikamiika@users.noreply.github.com>
Date: Thu, 5 Dec 2019 22:12:43 +0200
Subject: scan decoupling

---
 ext/fg/js/frontend.js        | 13 ++++---
 ext/mixed/js/text-scanner.js | 88 ++++++++++++++++++++++++++++++++------------
 2 files changed, 73 insertions(+), 28 deletions(-)

(limited to 'ext/mixed')

diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 43a4830f..2adbde36 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -20,7 +20,14 @@
 class Frontend {
     constructor(popup, ignoreNodes) {
         this.popup = popup;
-        this.textScanner = new TextScanner(window, ignoreNodes, this.popup, this.searchSource.bind(this));
+        this.textScanner = new TextScanner(
+            window,
+            ignoreNodes,
+            [this.popup.container],
+            [(x, y) => this.popup.containsPoint(x, y)]
+        );
+        this.textScanner.subscribe('textSearch', ({textSource, cause}) => this.searchSource(textSource, cause));
+        this.textScanner.subscribe('searchClear', () => this.searchClear(true));
         this.options = null;
 
         this.optionsContext = {
@@ -138,7 +145,6 @@ class Frontend {
         let results = null;
 
         try {
-            this.textScanner.pendingLookup = true;
             if (textSource !== null) {
                 results = (
                     await this.findTerms(textSource) ||
@@ -165,8 +171,6 @@ class Frontend {
             if (results === null && this.options.scanning.autoHideResults) {
                 this.searchClear(true);
             }
-
-            this.textScanner.pendingLookup = false;
         }
 
         return results;
@@ -182,7 +186,6 @@ class Frontend {
             {definitions, context: {sentence, url, focus, disableHistory: true}}
         );
 
-        this.textScanner.setCurrentTextSource(textSource);
         if (this.options.scanning.selectText) {
             textSource.select();
         }
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index cf6e5397..fc57d6c3 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -18,19 +18,23 @@
 
 
 class TextScanner {
-    constructor(node, ignoreNodes, popup, onTextSearch) {
+    constructor(node, ignoreNodes, ignoreElements, ignorePoints) {
         this.node = node;
         this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null);
-        this.popup = popup;
-        this.onTextSearch = onTextSearch;
+        this.ignoreElements = ignoreElements;
+        this.ignorePoints = ignorePoints;
 
-        this.popupTimerPromise = null;
+        this.scanTimerPromise = null;
         this.textSourceCurrent = null;
         this.pendingLookup = false;
         this.options = null;
 
         this.enabled = false;
         this.eventListeners = [];
+        this.subscribers = {
+            searchClear: [],
+            textSearch: []
+        };
 
         this.primaryTouchIdentifier = null;
         this.preventNextContextMenu = false;
@@ -40,13 +44,13 @@ class TextScanner {
     }
 
     onMouseOver(e) {
-        if (this.popup && e.target === this.popup.container) {
-            this.popupTimerClear();
+        if (this.ignoreElements.includes(e.target)) {
+            this.scanTimerClear();
         }
     }
 
     onMouseMove(e) {
-        this.popupTimerClear();
+        this.scanTimerClear();
 
         if (this.pendingLookup || DOM.isMouseButtonDown(e, 'primary')) {
             return;
@@ -63,7 +67,7 @@ class TextScanner {
 
         const search = async () => {
             if (scanningModifier === 'none') {
-                if (!await this.popupTimerWait()) {
+                if (!await this.scanTimerWait()) {
                     // Aborted
                     return;
                 }
@@ -84,14 +88,14 @@ class TextScanner {
             return false;
         }
 
-        if (DOM.isMouseButtonPressed(e, 'primary')) {
-            this.popupTimerClear();
-            this.searchClear();
+        if (DOM.isMouseButtonDown(e, 'primary')) {
+            this.scanTimerClear();
+            this.onSearchClear();
         }
     }
 
     onMouseOut() {
-        this.popupTimerClear();
+        this.scanTimerClear();
     }
 
     onClick(e) {
@@ -192,23 +196,55 @@ class TextScanner {
         e.preventDefault(); // Disable scroll
     }
 
-    async popupTimerWait() {
+    async onSearchClear() {
+        this.searchClear();
+        await this.publish('searchClear', {});
+    }
+
+    async onTextSearch(textSource, cause) {
+        this.pendingLookup = true;
+        const results = await this.publish('textSearch', {textSource, cause});
+        if (results.some((r) => r)) {
+            this.textSourceCurrent = textSource;
+        }
+        this.pendingLookup = false;
+    }
+
+    onError(error) {
+        logError(error, false);
+    }
+
+    subscribe(eventName, subscriber) {
+        if (this.subscribers[eventName].includes(subscriber)) { return; }
+        this.subscribers[eventName].push(subscriber);
+    }
+
+    async publish(eventName, data) {
+        const results = [];
+        for (const subscriber of this.subscribers[eventName]) {
+            const result = await subscriber(data);
+            results.push(result);
+        }
+        return results;
+    }
+
+    async scanTimerWait() {
         const delay = this.options.scanning.delay;
         const promise = promiseTimeout(delay, true);
-        this.popupTimerPromise = promise;
+        this.scanTimerPromise = promise;
         try {
             return await promise;
         } finally {
-            if (this.popupTimerPromise === promise) {
-                this.popupTimerPromise = null;
+            if (this.scanTimerPromise === promise) {
+                this.scanTimerPromise = null;
             }
         }
     }
 
-    popupTimerClear() {
-        if (this.popupTimerPromise !== null) {
-            this.popupTimerPromise.resolve(false);
-            this.popupTimerPromise = null;
+    scanTimerClear() {
+        if (this.scanTimerPromise !== null) {
+            this.scanTimerPromise.resolve(false);
+            this.scanTimerPromise = null;
         }
     }
 
@@ -223,7 +259,7 @@ class TextScanner {
                 this.clearEventListeners();
                 this.enabled = false;
             }
-            this.searchClear();
+            this.onSearchClear();
         }
     }
 
@@ -262,12 +298,18 @@ class TextScanner {
 
     async searchAt(x, y, cause) {
         try {
-            this.popupTimerClear();
+            this.scanTimerClear();
 
-            if (this.pendingLookup || (this.popup && await this.popup.containsPoint(x, y))) {
+            if (this.pendingLookup) {
                 return;
             }
 
+            for (const ignorePointFn of this.ignorePoints) {
+                if (await ignorePointFn(x, y)) {
+                    return;
+                }
+            }
+
             const textSource = docRangeFromPoint(x, y, this.options);
             if (this.textSourceCurrent !== null && this.textSourceCurrent.equals(textSource)) {
                 return;
-- 
cgit v1.2.3


From 595636c40bdc344ee34fee55c951e62ba0a24505 Mon Sep 17 00:00:00 2001
From: siikamiika <siikamiika@users.noreply.github.com>
Date: Thu, 5 Dec 2019 22:48:05 +0200
Subject: move text selection to TextScanner

---
 ext/fg/js/frontend.js        | 4 ----
 ext/mixed/js/text-scanner.js | 3 +++
 2 files changed, 3 insertions(+), 4 deletions(-)

(limited to 'ext/mixed')

diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 2adbde36..9ec66fb1 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -185,10 +185,6 @@ class Frontend {
             type,
             {definitions, context: {sentence, url, focus, disableHistory: true}}
         );
-
-        if (this.options.scanning.selectText) {
-            textSource.select();
-        }
     }
 
     async findTerms(textSource) {
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index fc57d6c3..0adcc0bd 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -206,6 +206,9 @@ class TextScanner {
         const results = await this.publish('textSearch', {textSource, cause});
         if (results.some((r) => r)) {
             this.textSourceCurrent = textSource;
+            if (this.options.scanning.selectText) {
+                textSource.select();
+            }
         }
         this.pendingLookup = false;
     }
-- 
cgit v1.2.3


From f6d0503604e66ef89578332f6adb477606dc81f9 Mon Sep 17 00:00:00 2001
From: siikamiika <siikamiika@users.noreply.github.com>
Date: Fri, 6 Dec 2019 21:39:29 +0200
Subject: simplify with inheritance

---
 ext/fg/js/frontend.js        |  72 ++++++++-----------------------
 ext/mixed/js/text-scanner.js | 100 ++++++++++++++++++++-----------------------
 2 files changed, 65 insertions(+), 107 deletions(-)

(limited to 'ext/mixed')

diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js
index 9ec66fb1..01f5c13d 100644
--- a/ext/fg/js/frontend.js
+++ b/ext/fg/js/frontend.js
@@ -17,17 +17,16 @@
  */
 
 
-class Frontend {
+class Frontend extends TextScanner {
     constructor(popup, ignoreNodes) {
-        this.popup = popup;
-        this.textScanner = new TextScanner(
+        super(
             window,
             ignoreNodes,
-            [this.popup.container],
+            [popup.container],
             [(x, y) => this.popup.containsPoint(x, y)]
         );
-        this.textScanner.subscribe('textSearch', ({textSource, cause}) => this.searchSource(textSource, cause));
-        this.textScanner.subscribe('searchClear', () => this.searchClear(true));
+
+        this.popup = popup;
         this.options = null;
 
         this.optionsContext = {
@@ -35,9 +34,6 @@ class Frontend {
             url: popup.url
         };
 
-        this.enabled = false;
-        this.eventListeners = [];
-
         this.isPreparedPromiseResolve = null;
         this.isPreparedPromise = new Promise((resolve) => { this.isPreparedPromiseResolve = resolve; });
 
@@ -70,7 +66,7 @@ class Frontend {
     }
 
     async onResize() {
-        const textSource = this.textScanner.getCurrentTextSource();
+        const textSource = this.textSourceCurrent;
         if (textSource !== null && await this.popup.isVisibleAsync()) {
             this.lastShowPromise = this.popup.showContent(
                 textSource.getRect(),
@@ -97,51 +93,21 @@ class Frontend {
         }
     }
 
-    onError(error) {
-        logError(error, false);
-    }
-
-    setEnabled(enabled) {
-        this.textScanner.setEnabled(enabled);
-        if (enabled) {
-            if (!this.enabled) {
-                this.hookEvents();
-                this.enabled = true;
-            }
-        } else {
-            if (this.enabled) {
-                this.clearEventListeners();
-                this.enabled = false;
-            }
-            this.searchClear(false);
-        }
-    }
-
-    hookEvents() {
-        this.addEventListener(window, 'message', this.onWindowMessage.bind(this));
-        this.addEventListener(window, 'resize', this.onResize.bind(this));
-    }
-
-    addEventListener(node, type, listener, options) {
-        node.addEventListener(type, listener, options);
-        this.eventListeners.push([node, type, listener, options]);
-    }
-
-    clearEventListeners() {
-        for (const [node, type, listener, options] of this.eventListeners) {
-            node.removeEventListener(type, listener, options);
-        }
-        this.eventListeners = [];
+    getMouseEventListeners() {
+        return [
+            ...super.getMouseEventListeners(),
+            [window, 'message', this.onWindowMessage.bind(this)],
+            [window, 'resize', this.onResize.bind(this)]
+        ];
     }
 
     async updateOptions() {
         this.options = await apiOptionsGet(this.getOptionsContext());
-        this.textScanner.setOptions(this.options);
         await this.popup.setOptions(this.options);
         this.setEnabled(this.options.general.enable);
     }
 
-    async searchSource(textSource, cause) {
+    async onSearchSource(textSource, cause) {
         let results = null;
 
         try {
@@ -169,7 +135,7 @@ class Frontend {
             }
         } finally {
             if (results === null && this.options.scanning.autoHideResults) {
-                this.searchClear(true);
+                this.onSearchClear(true);
             }
         }
 
@@ -188,7 +154,7 @@ class Frontend {
     }
 
     async findTerms(textSource) {
-        this.textScanner.setTextSourceScanLength(textSource, this.options.scanning.length);
+        this.setTextSourceScanLength(textSource, this.options.scanning.length);
 
         const searchText = textSource.text();
         if (searchText.length === 0) { return null; }
@@ -202,7 +168,7 @@ class Frontend {
     }
 
     async findKanji(textSource) {
-        this.textScanner.setTextSourceScanLength(textSource, 1);
+        this.setTextSourceScanLength(textSource, 1);
 
         const searchText = textSource.text();
         if (searchText.length === 0) { return null; }
@@ -213,10 +179,10 @@ class Frontend {
         return {definitions, type: 'kanji'};
     }
 
-    searchClear(changeFocus) {
+    onSearchClear(changeFocus) {
         this.popup.hide(changeFocus);
         this.popup.clearAutoPlayTimer();
-        this.textScanner.searchClear();
+        super.onSearchClear(changeFocus);
     }
 
     getOptionsContext() {
@@ -227,7 +193,7 @@ class Frontend {
 
 Frontend.windowMessageHandlers = {
     popupClose: (self) => {
-        self.searchClear(true);
+        self.onSearchClear(true);
     },
 
     selectionCopy: () => {
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index 0adcc0bd..ac5d68d1 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -31,10 +31,6 @@ class TextScanner {
 
         this.enabled = false;
         this.eventListeners = [];
-        this.subscribers = {
-            searchClear: [],
-            textSearch: []
-        };
 
         this.primaryTouchIdentifier = null;
         this.preventNextContextMenu = false;
@@ -90,7 +86,7 @@ class TextScanner {
 
         if (DOM.isMouseButtonDown(e, 'primary')) {
             this.scanTimerClear();
-            this.onSearchClear();
+            this.onSearchClear(true);
         }
     }
 
@@ -196,41 +192,14 @@ class TextScanner {
         e.preventDefault(); // Disable scroll
     }
 
-    async onSearchClear() {
-        this.searchClear();
-        await this.publish('searchClear', {});
-    }
-
-    async onTextSearch(textSource, cause) {
-        this.pendingLookup = true;
-        const results = await this.publish('textSearch', {textSource, cause});
-        if (results.some((r) => r)) {
-            this.textSourceCurrent = textSource;
-            if (this.options.scanning.selectText) {
-                textSource.select();
-            }
-        }
-        this.pendingLookup = false;
+    async onSearchSource(_textSource, _cause) {
+        throw new Error('Override me');
     }
 
     onError(error) {
         logError(error, false);
     }
 
-    subscribe(eventName, subscriber) {
-        if (this.subscribers[eventName].includes(subscriber)) { return; }
-        this.subscribers[eventName].push(subscriber);
-    }
-
-    async publish(eventName, data) {
-        const results = [];
-        for (const subscriber of this.subscribers[eventName]) {
-            const result = await subscriber(data);
-            results.push(result);
-        }
-        return results;
-    }
-
     async scanTimerWait() {
         const delay = this.options.scanning.delay;
         const promise = promiseTimeout(delay, true);
@@ -262,35 +231,50 @@ class TextScanner {
                 this.clearEventListeners();
                 this.enabled = false;
             }
-            this.onSearchClear();
+            this.onSearchClear(false);
         }
     }
 
     hookEvents() {
-        this.addEventListener('mousedown', this.onMouseDown.bind(this));
-        this.addEventListener('mousemove', this.onMouseMove.bind(this));
-        this.addEventListener('mouseover', this.onMouseOver.bind(this));
-        this.addEventListener('mouseout', this.onMouseOut.bind(this));
-
+        let eventListeners = this.getMouseEventListeners();
         if (this.options.scanning.touchInputEnabled) {
-            this.addEventListener('click', this.onClick.bind(this));
-            this.addEventListener('auxclick', this.onAuxClick.bind(this));
-            this.addEventListener('touchstart', this.onTouchStart.bind(this));
-            this.addEventListener('touchend', this.onTouchEnd.bind(this));
-            this.addEventListener('touchcancel', this.onTouchCancel.bind(this));
-            this.addEventListener('touchmove', this.onTouchMove.bind(this), {passive: false});
-            this.addEventListener('contextmenu', this.onContextMenu.bind(this));
+            eventListeners = eventListeners.concat(this.getTouchEventListeners());
+        }
+
+        for (const [node, type, listener, options] of eventListeners) {
+            this.addEventListener(node, type, listener, options);
         }
     }
 
-    addEventListener(type, listener, options) {
-        this.node.addEventListener(type, listener, options);
-        this.eventListeners.push([type, listener, options]);
+    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)]
+        ];
+    }
+
+    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)],
+            [this.node, 'touchcancel', this.onTouchCancel.bind(this)],
+            [this.node, 'touchmove', this.onTouchMove.bind(this), {passive: false}],
+            [this.node, 'contextmenu', this.onContextMenu.bind(this)]
+        ];
+    }
+
+    addEventListener(node, type, listener, options) {
+        node.addEventListener(type, listener, options);
+        this.eventListeners.push([node, type, listener, options]);
     }
 
     clearEventListeners() {
-        for (const [type, listener, options] of this.eventListeners) {
-            this.node.removeEventListener(type, listener, options);
+        for (const [node, type, listener, options] of this.eventListeners) {
+            node.removeEventListener(type, listener, options);
         }
         this.eventListeners = [];
     }
@@ -319,7 +303,15 @@ class TextScanner {
             }
 
             try {
-                await this.onTextSearch(textSource, cause);
+                this.pendingLookup = true;
+                const result = await this.onSearchSource(textSource, cause);
+                if (result !== null) {
+                    this.textSourceCurrent = textSource;
+                    if (this.options.scanning.selectText) {
+                        textSource.select();
+                    }
+                }
+                this.pendingLookup = false;
             } finally {
                 if (textSource !== null) {
                     textSource.cleanup();
@@ -347,7 +339,7 @@ class TextScanner {
         }
     }
 
-    searchClear() {
+    onSearchClear(_) {
         if (this.textSourceCurrent !== null) {
             if (this.options.scanning.selectText) {
                 this.textSourceCurrent.deselect();
-- 
cgit v1.2.3