diff options
Diffstat (limited to 'ext/mixed/js/text-scanner.js')
| -rw-r--r-- | ext/mixed/js/text-scanner.js | 436 | 
1 files changed, 229 insertions, 207 deletions
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 0cd12cd7..b8688b08 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -21,47 +21,172 @@   * docRangeFromPoint   */ -class TextScanner { -    constructor(node, ignoreElements, ignorePoints) { -        this.node = node; -        this.ignoreElements = ignoreElements; -        this.ignorePoints = ignorePoints; - -        this.ignoreNodes = null; - -        this.scanTimerPromise = null; -        this.causeCurrent = null; -        this.textSourceCurrent = null; -        this.pendingLookup = false; -        this.options = null; - -        this.enabled = false; -        this.eventListeners = new EventListenerCollection(); - -        this.primaryTouchIdentifier = null; -        this.preventNextContextMenu = false; -        this.preventNextMouseDown = false; -        this.preventNextClick = false; -        this.preventScroll = false; +class TextScanner extends EventDispatcher { +    constructor({node, ignoreElements, ignorePoint, search}) { +        super(); +        this._node = node; +        this._ignoreElements = ignoreElements; +        this._ignorePoint = ignorePoint; +        this._search = search; + +        this._ignoreNodes = null; + +        this._causeCurrent = null; +        this._scanTimerPromise = null; +        this._textSourceCurrent = null; +        this._textSourceCurrentSelected = false; +        this._pendingLookup = false; +        this._options = null; + +        this._enabled = false; +        this._eventListeners = new EventListenerCollection(); + +        this._primaryTouchIdentifier = null; +        this._preventNextContextMenu = false; +        this._preventNextMouseDown = false; +        this._preventNextClick = false; +        this._preventScroll = false; + +        this._canClearSelection = true;      } -    onMouseOver(e) { -        if (this.ignoreElements().includes(e.target)) { -            this.scanTimerClear(); +    get canClearSelection() { +        return this._canClearSelection; +    } + +    set canClearSelection(value) { +        this._canClearSelection = value; +    } + +    get ignoreNodes() { +        return this._ignoreNodes; +    } + +    set ignoreNodes(value) { +        this._ignoreNodes = value; +    } + +    get causeCurrent() { +        return this._causeCurrent; +    } + +    setEnabled(enabled) { +        this._eventListeners.removeAllEventListeners(); +        this._enabled = enabled; +        if (this._enabled) { +            this._hookEvents(); +        } else { +            this.clearSelection(true); +        } +    } + +    setOptions(options) { +        this._options = options; +    } + +    async searchAt(x, y, cause) { +        try { +            this._scanTimerClear(); + +            if (this._pendingLookup) { +                return; +            } + +            if (typeof this._ignorePoint === 'function' && await this._ignorePoint(x, y)) { +                return; +            } + +            const textSource = docRangeFromPoint(x, y, this._options.scanning.deepDomScan); +            try { +                if (this._textSourceCurrent !== null && this._textSourceCurrent.equals(textSource)) { +                    return; +                } + +                this._pendingLookup = true; +                const result = await this._search(textSource, cause); +                if (result !== null) { +                    this._causeCurrent = cause; +                    this.setCurrentTextSource(textSource); +                } +                this._pendingLookup = false; +            } finally { +                if (textSource !== null) { +                    textSource.cleanup(); +                } +            } +        } catch (e) { +            yomichan.logError(e); +        } +    } + +    getTextSourceContent(textSource, length) { +        const clonedTextSource = textSource.clone(); + +        clonedTextSource.setEndOffset(length); + +        if (this._ignoreNodes !== null && clonedTextSource.range) { +            length = clonedTextSource.text().length; +            while (clonedTextSource.range && length > 0) { +                const nodes = TextSourceRange.getNodesInRange(clonedTextSource.range); +                if (!TextSourceRange.anyNodeMatchesSelector(nodes, this._ignoreNodes)) { +                    break; +                } +                --length; +                clonedTextSource.setEndOffset(length); +            } +        } + +        return clonedTextSource.text(); +    } + +    clearSelection(passive) { +        if (!this._canClearSelection) { return; } +        if (this._textSourceCurrent !== null) { +            if (this._textSourceCurrentSelected) { +                this._textSourceCurrent.deselect(); +            } +            this._textSourceCurrent = null; +            this._textSourceCurrentSelected = false; +        } +        this.trigger('clearSelection', {passive}); +    } + +    getCurrentTextSource() { +        return this._textSourceCurrent; +    } + +    setCurrentTextSource(textSource) { +        this._textSourceCurrent = textSource; +        if (this._options.scanning.selectText) { +            this._textSourceCurrent.select(); +            this._textSourceCurrentSelected = true; +        } else { +            this._textSourceCurrentSelected = false; +        } +    } + +    // Private + +    _onMouseOver(e) { +        if (this._ignoreElements().includes(e.target)) { +            this._scanTimerClear();          }      } -    onMouseMove(e) { -        this.scanTimerClear(); +    _onMouseMove(e) { +        this._scanTimerClear(); -        if (this.pendingLookup || DOM.isMouseButtonDown(e, 'primary')) { +        if (this._pendingLookup || DOM.isMouseButtonDown(e, 'primary')) {              return;          } -        const scanningOptions = this.options.scanning; +        const modifiers = DOM.getActiveModifiers(e); +        this.trigger('activeModifiersChanged', {modifiers}); + +        const scanningOptions = this._options.scanning;          const scanningModifier = scanningOptions.modifier;          if (!( -            TextScanner.isScanningModifierPressed(scanningModifier, e) || +            this._isScanningModifierPressed(scanningModifier, e) ||              (scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary'))          )) {              return; @@ -69,7 +194,7 @@ class TextScanner {          const search = async () => {              if (scanningModifier === 'none') { -                if (!await this.scanTimerWait()) { +                if (!await this._scanTimerWait()) {                      // Aborted                      return;                  } @@ -81,112 +206,110 @@ class TextScanner {          search();      } -    onMouseDown(e) { -        if (this.preventNextMouseDown) { -            this.preventNextMouseDown = false; -            this.preventNextClick = true; +    _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); +            this._scanTimerClear(); +            this.clearSelection(false);          }      } -    onMouseOut() { -        this.scanTimerClear(); +    _onMouseOut() { +        this._scanTimerClear();      } -    onClick(e) { -        if (this.preventNextClick) { -            this.preventNextClick = false; +    _onClick(e) { +        if (this._preventNextClick) { +            this._preventNextClick = false;              e.preventDefault();              e.stopPropagation();              return false;          }      } -    onAuxClick() { -        this.preventNextContextMenu = false; +    _onAuxClick() { +        this._preventNextContextMenu = false;      } -    onContextMenu(e) { -        if (this.preventNextContextMenu) { -            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) { +    _onTouchStart(e) { +        if (this._primaryTouchIdentifier !== null || e.changedTouches.length === 0) {              return;          } -        this.preventScroll = false; -        this.preventNextContextMenu = false; -        this.preventNextMouseDown = false; -        this.preventNextClick = false; +        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; +        this._primaryTouchIdentifier = primaryTouch.identifier; -        if (this.pendingLookup) { +        if (this._pendingLookup) {              return;          } -        const textSourceCurrentPrevious = this.textSourceCurrent !== null ? this.textSourceCurrent.clone() : null; +        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) +                    this._textSourceCurrent === null || +                    this._textSourceCurrent.equals(textSourceCurrentPrevious)                  ) {                      return;                  } -                this.preventScroll = true; -                this.preventNextContextMenu = true; -                this.preventNextMouseDown = true; +                this._preventScroll = true; +                this._preventNextContextMenu = true; +                this._preventNextMouseDown = true;              });      } -    onTouchEnd(e) { +    _onTouchEnd(e) {          if ( -            this.primaryTouchIdentifier === null || -            TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier) === null +            this._primaryTouchIdentifier === null || +            this._getTouch(e.changedTouches, this._primaryTouchIdentifier) === null          ) {              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; +        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. +        // I.e. this._preventNextContextMenu and this._preventNextMouseDown should not be assigned to false.      } -    onTouchCancel(e) { -        this.onTouchEnd(e); +    _onTouchCancel(e) { +        this._onTouchEnd(e);      } -    onTouchMove(e) { -        if (!this.preventScroll || !e.cancelable || this.primaryTouchIdentifier === null) { +    _onTouchMove(e) { +        if (!this._preventScroll || !e.cancelable || this._primaryTouchIdentifier === null) {              return;          } -        const primaryTouch = TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier); +        const primaryTouch = this._getTouch(e.changedTouches, this._primaryTouchIdentifier);          if (primaryTouch === null) {              return;          } @@ -196,171 +319,70 @@ class TextScanner {          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; +    async _scanTimerWait() { +        const delay = this._options.scanning.delay;          const promise = promiseTimeout(delay, true); -        this.scanTimerPromise = promise; +        this._scanTimerPromise = promise;          try {              return await promise;          } finally { -            if (this.scanTimerPromise === promise) { -                this.scanTimerPromise = null; +            if (this._scanTimerPromise === promise) { +                this._scanTimerPromise = null;              }          }      } -    scanTimerClear() { -        if (this.scanTimerPromise !== null) { -            this.scanTimerPromise.resolve(false); -            this.scanTimerPromise = null; +    _scanTimerClear() { +        if (this._scanTimerPromise !== null) { +            this._scanTimerPromise.resolve(false); +            this._scanTimerPromise = null;          }      } -    setEnabled(enabled, canEnable) { -        if (enabled && canEnable) { -            if (!this.enabled) { -                this.hookEvents(); -                this.enabled = true; -            } -        } else { -            if (this.enabled) { -                this.eventListeners.removeAllEventListeners(); -                this.enabled = false; -            } -            this.onSearchClear(false); -        } -    } - -    hookEvents() { -        let eventListenerInfos = this.getMouseEventListeners(); -        if (this.options.scanning.touchInputEnabled) { -            eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners()); +    _hookEvents() { +        const eventListenerInfos = this._getMouseEventListeners(); +        if (this._options.scanning.touchInputEnabled) { +            eventListenerInfos.push(...this._getTouchEventListeners());          }          for (const [node, type, listener, options] of eventListenerInfos) { -            this.eventListeners.addEventListener(node, type, listener, options); +            this._eventListeners.addEventListener(node, type, listener, options);          }      } -    getMouseEventListeners() { +    _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)] +            [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() { +    _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)] +            [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)]          ];      } -    setOptions(options, canEnable=true) { -        this.options = options; -        this.setEnabled(this.options.general.enable, canEnable); -    } - -    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.scanning.deepDomScan); -            try { -                if (this.textSourceCurrent !== null && this.textSourceCurrent.equals(textSource)) { -                    return; -                } - -                this.pendingLookup = true; -                const result = await this.onSearchSource(textSource, cause); -                if (result !== null) { -                    this.causeCurrent = cause; -                    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) { +    _isScanningModifierPressed(scanningModifier, mouseEvent) {          switch (scanningModifier) {              case 'alt': return mouseEvent.altKey;              case 'ctrl': return mouseEvent.ctrlKey;              case 'shift': return mouseEvent.shiftKey; +            case 'meta': return mouseEvent.metaKey;              case 'none': return true;              default: return false;          }      } -    static getTouch(touchList, identifier) { +    _getTouch(touchList, identifier) {          for (const touch of touchList) {              if (touch.identifier === identifier) {                  return touch;  |