diff options
| author | siikamiika <siikamiika@users.noreply.github.com> | 2019-12-07 15:20:28 +0200 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-12-07 15:20:28 +0200 | 
| commit | df06251cb024a285c6ef27bf6b5dfc907afaef84 (patch) | |
| tree | 1238fed12525ad4b8ac15357322c1601f426fea4 /ext/mixed/js/text-scanner.js | |
| parent | d1950bc5f1a3cc737bbb43418738f5da7955ee7f (diff) | |
| parent | f6d0503604e66ef89578332f6adb477606dc81f9 (diff) | |
Merge pull request #299 from siikamiika/frontend-extract-text-scanner
Extract text scanner from Frontend 
Diffstat (limited to 'ext/mixed/js/text-scanner.js')
| -rw-r--r-- | ext/mixed/js/text-scanner.js | 378 | 
1 files changed, 378 insertions, 0 deletions
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js new file mode 100644 index 00000000..ac5d68d1 --- /dev/null +++ b/ext/mixed/js/text-scanner.js @@ -0,0 +1,378 @@ +/* + * 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, ignoreElements, ignorePoints) { +        this.node = node; +        this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null); +        this.ignoreElements = ignoreElements; +        this.ignorePoints = ignorePoints; + +        this.scanTimerPromise = 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.ignoreElements.includes(e.target)) { +            this.scanTimerClear(); +        } +    } + +    onMouseMove(e) { +        this.scanTimerClear(); + +        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.scanTimerWait()) { +                    // 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.isMouseButtonDown(e, 'primary')) { +            this.scanTimerClear(); +            this.onSearchClear(true); +        } +    } + +    onMouseOut() { +        this.scanTimerClear(); +    } + +    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 onSearchSource(_textSource, _cause) { +        throw new Error('Override me'); +    } + +    onError(error) { +        logError(error, false); +    } + +    async scanTimerWait() { +        const delay = this.options.scanning.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; +        } +    } + +    setEnabled(enabled) { +        if (enabled) { +            if (!this.enabled) { +                this.hookEvents(); +                this.enabled = true; +            } +        } else { +            if (this.enabled) { +                this.clearEventListeners(); +                this.enabled = false; +            } +            this.onSearchClear(false); +        } +    } + +    hookEvents() { +        let eventListeners = this.getMouseEventListeners(); +        if (this.options.scanning.touchInputEnabled) { +            eventListeners = eventListeners.concat(this.getTouchEventListeners()); +        } + +        for (const [node, type, listener, options] of eventListeners) { +            this.addEventListener(node, 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 [node, type, listener, options] of this.eventListeners) { +            node.removeEventListener(type, listener, options); +        } +        this.eventListeners = []; +    } + +    setOptions(options) { +        this.options = options; +    } + +    async searchAt(x, y, cause) { +        try { +            this.scanTimerClear(); + +            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; +            } + +            try { +                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(); +                } +            } +        } 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); +        } +    } + +    onSearchClear(_) { +        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; +    } +}  |