diff options
| author | siikamiika <siikamiika@users.noreply.github.com> | 2019-12-05 03:58:35 +0200 | 
|---|---|---|
| committer | siikamiika <siikamiika@users.noreply.github.com> | 2019-12-05 03:58:35 +0200 | 
| commit | 02a34bb4bc81471934e54b5440a20585527d76f5 (patch) | |
| tree | ce1f6d6eb9b56fcb16663a97df1fbdc06c383bba /ext | |
| parent | 248b0f15eb7d0fd10d3678a2e5d2f6ddb631309f (diff) | |
initial text scanner extract
Diffstat (limited to 'ext')
| -rw-r--r-- | ext/bg/js/search-frontend.js | 1 | ||||
| -rw-r--r-- | ext/bg/search.html | 1 | ||||
| -rw-r--r-- | ext/fg/js/frontend.js | 287 | ||||
| -rw-r--r-- | ext/fg/js/popup-nested.js | 1 | ||||
| -rw-r--r-- | ext/manifest.json | 1 | ||||
| -rw-r--r-- | ext/mixed/js/text-scanner.js | 341 | 
6 files changed, 357 insertions, 275 deletions
| diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index 6ba8467e..e21dc371 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -33,6 +33,7 @@ async function searchFrontendSetup() {      window.frontendInitializationData = {depth: 1, ignoreNodes, proxy: false};      const scriptSrcs = [ +        '/mixed/js/text-scanner.js',          '/fg/js/frontend-api-receiver.js',          '/fg/js/popup.js',          '/fg/js/popup-proxy-host.js', diff --git a/ext/bg/search.html b/ext/bg/search.html index 58bb9ba8..7b4616da 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -74,6 +74,7 @@          <script src="/mixed/js/display.js"></script>          <script src="/mixed/js/japanese.js"></script>          <script src="/mixed/js/scroll.js"></script> +        <script src="/mixed/js/text-scanner.js"></script>          <script src="/bg/js/search-query-parser.js"></script>          <script src="/bg/js/search.js"></script> diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 9a1d507b..43a4830f 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -20,23 +20,14 @@  class Frontend {      constructor(popup, ignoreNodes) {          this.popup = popup; -        this.popupTimerPromise = null; -        this.textSourceCurrent = null; -        this.pendingLookup = false; +        this.textScanner = new TextScanner(window, ignoreNodes, this.popup, this.searchSource.bind(this));          this.options = null; -        this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null);          this.optionsContext = {              depth: popup.depth,              url: popup.url          }; -        this.primaryTouchIdentifier = null; -        this.preventNextContextMenu = false; -        this.preventNextMouseDown = false; -        this.preventNextClick = false; -        this.preventScroll = false; -          this.enabled = false;          this.eventListeners = []; @@ -71,162 +62,9 @@ class Frontend {          return this.isPreparedPromise;      } -    onMouseOver(e) { -        if (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 (!( -            Frontend.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 (e.button === 0) { -            this.popupTimerClear(); -            this.searchClear(true); -        } -    } - -    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, window.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 || -            this.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 = this.getIndexOfTouch(touches, this.primaryTouchIdentifier); -        if (index < 0) { -            return; -        } - -        const primaryTouch = touches[index]; -        this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove'); - -        e.preventDefault(); // Disable scroll -    } -      async onResize() { -        if (this.textSourceCurrent !== null && await this.popup.isVisibleAsync()) { -            const textSource = this.textSourceCurrent; +        const textSource = this.textScanner.getCurrentTextSource(); +        if (textSource !== null && await this.popup.isVisibleAsync()) {              this.lastShowPromise = this.popup.showContent(                  textSource.getRect(),                  textSource.getWritingMode() @@ -257,6 +95,7 @@ class Frontend {      }      setEnabled(enabled) { +        this.textScanner.setEnabled(enabled);          if (enabled) {              if (!this.enabled) {                  this.hookEvents(); @@ -273,21 +112,7 @@ class Frontend {      hookEvents() {          this.addEventListener(window, 'message', this.onWindowMessage.bind(this)); -        this.addEventListener(window, 'mousedown', this.onMouseDown.bind(this)); -        this.addEventListener(window, 'mousemove', this.onMouseMove.bind(this)); -        this.addEventListener(window, 'mouseover', this.onMouseOver.bind(this)); -        this.addEventListener(window, 'mouseout', this.onMouseOut.bind(this));          this.addEventListener(window, 'resize', this.onResize.bind(this)); - -        if (this.options.scanning.touchInputEnabled) { -            this.addEventListener(window, 'click', this.onClick.bind(this)); -            this.addEventListener(window, 'auxclick', this.onAuxClick.bind(this)); -            this.addEventListener(window, 'touchstart', this.onTouchStart.bind(this)); -            this.addEventListener(window, 'touchend', this.onTouchEnd.bind(this)); -            this.addEventListener(window, 'touchcancel', this.onTouchCancel.bind(this)); -            this.addEventListener(window, 'touchmove', this.onTouchMove.bind(this), {passive: false}); -            this.addEventListener(window, 'contextmenu', this.onContextMenu.bind(this)); -        }      }      addEventListener(node, type, listener, options) { @@ -304,60 +129,16 @@ class Frontend {      async updateOptions() {          this.options = await apiOptionsGet(this.getOptionsContext()); -        this.setEnabled(this.options.general.enable); +        this.textScanner.setOptions(this.options);          await this.popup.setOptions(this.options); -    } - -    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; -        } -    } - -    async searchAt(x, y, cause) { -        try { -            this.popupTimerClear(); - -            if (this.pendingLookup || 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.searchSource(textSource, cause); -            } finally { -                if (textSource !== null) { -                    textSource.cleanup(); -                } -            } -        } catch (e) { -            this.onError(e); -        } +        this.setEnabled(this.options.general.enable);      }      async searchSource(textSource, cause) {          let results = null;          try { -            this.pendingLookup = true; +            this.textScanner.pendingLookup = true;              if (textSource !== null) {                  results = (                      await this.findTerms(textSource) || @@ -385,7 +166,7 @@ class Frontend {                  this.searchClear(true);              } -            this.pendingLookup = false; +            this.textScanner.pendingLookup = false;          }          return results; @@ -401,14 +182,14 @@ class Frontend {              {definitions, context: {sentence, url, focus, disableHistory: true}}          ); -        this.textSourceCurrent = textSource; +        this.textScanner.setCurrentTextSource(textSource);          if (this.options.scanning.selectText) {              textSource.select();          }      }      async findTerms(textSource) { -        this.setTextSourceScanLength(textSource, this.options.scanning.length); +        this.textScanner.setTextSourceScanLength(textSource, this.options.scanning.length);          const searchText = textSource.text();          if (searchText.length === 0) { return null; } @@ -422,7 +203,7 @@ class Frontend {      }      async findKanji(textSource) { -        this.setTextSourceScanLength(textSource, 1); +        this.textScanner.setTextSourceScanLength(textSource, 1);          const searchText = textSource.text();          if (searchText.length === 0) { return null; } @@ -436,57 +217,13 @@ class Frontend {      searchClear(changeFocus) {          this.popup.hide(changeFocus);          this.popup.clearAutoPlayTimer(); - -        if (this.textSourceCurrent !== null) { -            if (this.options.scanning.selectText) { -                this.textSourceCurrent.deselect(); -            } - -            this.textSourceCurrent = null; -        } -    } - -    getIndexOfTouch(touchList, identifier) { -        for (const i in touchList) { -            const t = touchList[i]; -            if (t.identifier === identifier) { -                return i; -            } -        } -        return -1; -    } - -    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); -        } +        this.textScanner.searchClear();      }      getOptionsContext() {          this.optionsContext.url = this.popup.url;          return this.optionsContext;      } - -    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; -        } -    }  }  Frontend.windowMessageHandlers = { diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index 31cb1cda..3df469fe 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -41,6 +41,7 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {      window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes, url, proxy: true};      const scriptSrcs = [ +        '/mixed/js/text-scanner.js',          '/fg/js/frontend-api-sender.js',          '/fg/js/popup.js',          '/fg/js/popup-proxy.js', diff --git a/ext/manifest.json b/ext/manifest.json index 9a9d6639..f91a6b33 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -20,6 +20,7 @@          "js": [              "mixed/js/core.js",              "mixed/js/dom.js", +            "mixed/js/text-scanner.js",              "fg/js/api.js",              "fg/js/document.js",              "fg/js/frontend-api-receiver.js", 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; +    } +} |