diff options
Diffstat (limited to 'ext/mixed/js')
| -rw-r--r-- | ext/mixed/js/display.js | 216 | ||||
| -rw-r--r-- | ext/mixed/js/document-util.js | 12 | ||||
| -rw-r--r-- | ext/mixed/js/text-scanner.js | 74 | 
3 files changed, 176 insertions, 126 deletions
| diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index e051893e..acab9345 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -27,6 +27,7 @@   * PopupFactory   * QueryParser   * TemplateRendererProxy + * TextScanner   * WindowScroll   * api   * dynamicLoader @@ -48,7 +49,6 @@ class Display extends EventDispatcher {          });          this._styleNode = null;          this._eventListeners = new EventListenerCollection(); -        this._clickScanPrevent = false;          this._setContentToken = null;          this._autoPlayAudioTimer = null;          this._autoPlayAudioDelay = 400; @@ -104,6 +104,7 @@ class Display extends EventDispatcher {          this._frameEndpoint = (pageType === 'popup' ? new FrameEndpoint() : null);          this._browser = null;          this._copyTextarea = null; +        this._definitionTextScanner = null;          this.registerActions([              ['close',             () => { this.onEscape(); }], @@ -311,6 +312,7 @@ class Display extends EventDispatcher {          });          this._updateNestedFrontend(options); +        this._updateDefinitionTextScanner(options);      }      autoPlayAudio() { @@ -348,6 +350,7 @@ class Display extends EventDispatcher {          const url = `${location.protocol}//${location.host}${location.pathname}?${urlSearchParams.toString()}`;          if (history && this._historyHasChanged) { +            this._updateHistoryState();              this._history.pushState(state, content, url);          } else {              this._history.clear(); @@ -648,24 +651,18 @@ class Display extends EventDispatcher {              e.preventDefault();              if (!this._historyHasState()) { return; } -            const link = e.target; -            const {state} = this._history; - -            state.focusEntry = this._getClosestDefinitionIndex(link); -            state.scrollX = this._windowScroll.x; -            state.scrollY = this._windowScroll.y; -            this._historyStateUpdate(state); - -            const query = link.textContent; -            const definitions = await api.kanjiFind(query, this.getOptionsContext()); +            const {state: {sentence}} = this._history; +            const optionsContext = this.getOptionsContext(); +            const query = e.currentTarget.textContent; +            const definitions = await api.kanjiFind(query, optionsContext);              const details = {                  focus: false,                  history: true,                  params: this._createSearchParams('kanji', query, false),                  state: {                      focusEntry: 0, -                    sentence: state.sentence, -                    optionsContext: state.optionsContext +                    sentence, +                    optionsContext                  },                  content: {                      definitions @@ -677,88 +674,6 @@ class Display extends EventDispatcher {          }      } -    _onGlossaryMouseDown(e) { -        if (DocumentUtil.isMouseButtonPressed(e, 'primary')) { -            this._clickScanPrevent = false; -        } -    } - -    _onGlossaryMouseMove() { -        this._clickScanPrevent = true; -    } - -    _onGlossaryMouseUp(e) { -        if (!this._clickScanPrevent && DocumentUtil.isMouseButtonPressed(e, 'primary')) { -            try { -                this._onTermLookup(e); -            } catch (error) { -                this.onError(error); -            } -        } -    } - -    async _onTermLookup(e) { -        if (!this._historyHasState()) { return; } - -        const termLookupResults = await this._termLookup(e); -        if (!termLookupResults || !this._historyHasState()) { return; } - -        const {state} = this._history; -        const {textSource, definitions} = termLookupResults; - -        const scannedElement = e.target; -        const sentenceExtent = this._options.anki.sentenceExt; -        const layoutAwareScan = this._options.scanning.layoutAwareScan; -        const sentence = this._documentUtil.extractSentence(textSource, sentenceExtent, layoutAwareScan); - -        state.focusEntry = this._getClosestDefinitionIndex(scannedElement); -        state.scrollX = this._windowScroll.x; -        state.scrollY = this._windowScroll.y; -        this._historyStateUpdate(state); - -        const query = textSource.text(); -        const details = { -            focus: false, -            history: true, -            params: this._createSearchParams('terms', query, false), -            state: { -                focusEntry: 0, -                sentence, -                optionsContext: state.optionsContext -            }, -            content: { -                definitions -            } -        }; -        this.setContent(details); -    } - -    async _termLookup(e) { -        e.preventDefault(); - -        const {length: scanLength, deepDomScan: deepScan, layoutAwareScan} = this._options.scanning; -        const textSource = this._documentUtil.getRangeFromPoint(e.clientX, e.clientY, deepScan); -        if (textSource === null) { -            return false; -        } - -        let definitions, length; -        try { -            textSource.setEndOffset(scanLength, layoutAwareScan); - -            ({definitions, length} = await api.termsFind(textSource.text(), {}, this.getOptionsContext())); -            if (definitions.length === 0) { -                return false; -            } - -            textSource.setEndOffset(length, layoutAwareScan); -        } finally { -            textSource.cleanup(); -        } - -        return {textSource, definitions}; -    } -      _onAudioPlay(e) {          e.preventDefault();          const link = e.currentTarget; @@ -942,7 +857,7 @@ class Display extends EventDispatcher {          if (this._setContentToken !== token) { return true; }          if (changeHistory) { -            this._historyStateUpdate(state, content); +            this._replaceHistoryStateNoNavigate(state, content);          }          eventArgs.source = source; @@ -1054,17 +969,17 @@ class Display extends EventDispatcher {      }      _setTitleText(text) { -        let title = ''; +        let title = this._defaultTitle;          if (text.length > 0) {              // Chrome limits title to 1024 characters              const ellipsis = '...';              const separator = ' - '; -            const maxLength = this._titleMaxLength - this._defaultTitle.length - separator.length; +            const maxLength = this._titleMaxLength - title.length - separator.length;              if (text.length > maxLength) {                  text = `${text.substring(0, Math.max(0, maxLength - ellipsis.length))}${ellipsis}`;              } -            title = `${text}${separator}${this._defaultTitle}`; +            title = `${text}${separator}${title}`;          }          document.title = title;      } @@ -1384,12 +1299,20 @@ class Display extends EventDispatcher {          return isObject(this._history.state);      } -    _historyStateUpdate(state, content) { +    _updateHistoryState() { +        const {state, content} = this._history; +        if (!isObject(state)) { return; } + +        state.focusEntry = this._index; +        state.scrollX = this._windowScroll.x; +        state.scrollY = this._windowScroll.y; +        this._replaceHistoryStateNoNavigate(state, content); +    } + +    _replaceHistoryStateNoNavigate(state, content) {          const historyChangeIgnorePre = this._historyChangeIgnore;          try {              this._historyChangeIgnore = true; -            if (typeof state === 'undefined') { state = this._history.state; } -            if (typeof content === 'undefined') { content = this._history.content; }              this._history.replaceState(state, content);          } finally {              this._historyChangeIgnore = historyChangeIgnorePre; @@ -1702,7 +1625,7 @@ class Display extends EventDispatcher {      }      _copyHostSelection() { -        if (window.getSelection().toString()) { return false; } +        if (this._ownerFrameId === null || window.getSelection().toString()) { return false; }          this._copyHostSelectionInner();          return true;      } @@ -1766,10 +1689,89 @@ class Display extends EventDispatcher {          this._addMultipleEventListeners(entry, '.action-play-audio', 'click', this._onAudioPlay.bind(this));          this._addMultipleEventListeners(entry, '.kanji-link', 'click', this._onKanjiLookup.bind(this));          this._addMultipleEventListeners(entry, '.debug-log-link', 'click', this._onDebugLogClick.bind(this)); -        if (this._options !== null && this._options.scanning.enablePopupSearch) { -            this._addMultipleEventListeners(entry, '.term-glossary-item,.tag', 'mouseup', this._onGlossaryMouseUp.bind(this)); -            this._addMultipleEventListeners(entry, '.term-glossary-item,.tag', 'mousedown', this._onGlossaryMouseDown.bind(this)); -            this._addMultipleEventListeners(entry, '.term-glossary-item,.tag', 'mousemove', this._onGlossaryMouseMove.bind(this)); +    } + +    _updateDefinitionTextScanner(options) { +        if (!options.scanning.enablePopupSearch) { +            if (this._definitionTextScanner !== null) { +                this._definitionTextScanner.setEnabled(false); +            } +            return;          } + +        if (this._definitionTextScanner === null) { +            this._definitionTextScanner = new TextScanner({ +                node: window, +                getOptionsContext: this.getOptionsContext.bind(this), +                documentUtil: this._documentUtil, +                searchTerms: true, +                searchKanji: false, +                searchOnClick: true, +                searchOnClickOnly: true +            }); +            this._definitionTextScanner.prepare(); +            this._definitionTextScanner.on('searched', this._onDefinitionTextScannerSearched.bind(this)); +        } + +        const scanningOptions = options.scanning; +        this._definitionTextScanner.setOptions({ +            inputs: [{ +                include: 'mouse0', +                exclude: '', +                types: {mouse: true, pen: false, touch: false}, +                options: { +                    searchTerms: true, +                    searchKanji: true, +                    scanOnTouchMove: false, +                    scanOnPenHover: false, +                    scanOnPenPress: false, +                    scanOnPenRelease: false, +                    preventTouchScrolling: false +                } +            }], +            deepContentScan: scanningOptions.deepDomScan, +            selectText: false, +            delay: scanningOptions.delay, +            touchInputEnabled: false, +            pointerEventsEnabled: false, +            scanLength: scanningOptions.length, +            sentenceExtent: options.anki.sentenceExt, +            layoutAwareScan: scanningOptions.layoutAwareScan, +            preventMiddleMouse: false +        }); + +        const includeSelector = '.term-glossary-item,.term-glossary-item *,.tag,.tag *'; +        this._definitionTextScanner.includeSelector = includeSelector; + +        this._definitionTextScanner.setEnabled(true); +    } + +    _onDefinitionTextScannerSearched({type, definitions, sentence, textSource, optionsContext, error}) { +        if (error !== null && !yomichan.isExtensionUnloaded) { +            yomichan.logError(error); +        } + +        if (type === null) { return; } + +        const query = textSource.text(); +        const details = { +            focus: false, +            history: true, +            params: { +                type, +                query, +                wildcards: 'off' +            }, +            state: { +                focusEntry: 0, +                sentence, +                optionsContext +            }, +            content: { +                definitions +            } +        }; +        this._definitionTextScanner.clearSelection(true); +        this.setContent(details);      }  } diff --git a/ext/mixed/js/document-util.js b/ext/mixed/js/document-util.js index da27a75d..611ff98c 100644 --- a/ext/mixed/js/document-util.js +++ b/ext/mixed/js/document-util.js @@ -261,6 +261,18 @@ class DocumentUtil {          return false;      } +    static everyNodeMatchesSelector(nodes, selector) { +        const ELEMENT_NODE = Node.ELEMENT_NODE; +        for (let node of nodes) { +            while (true) { +                if (node === null) { return false; } +                if (node.nodeType === ELEMENT_NODE && node.matches(selector)) { break; } +                node = node.parentNode; +            } +        } +        return true; +    } +      static getModifierKeys(os) {          switch (os) {              case 'win': diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 66d37c93..f0903370 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -21,19 +21,31 @@   */  class TextScanner extends EventDispatcher { -    constructor({node, ignoreElements, ignorePoint, documentUtil, getOptionsContext, searchTerms=false, searchKanji=false, searchOnClick=false}) { +    constructor({ +        node, +        documentUtil, +        getOptionsContext, +        ignoreElements=null, +        ignorePoint=null, +        searchTerms=false, +        searchKanji=false, +        searchOnClick=false, +        searchOnClickOnly=false +    }) {          super();          this._node = node; -        this._ignoreElements = ignoreElements; -        this._ignorePoint = ignorePoint;          this._documentUtil = documentUtil;          this._getOptionsContext = getOptionsContext; +        this._ignoreElements = ignoreElements; +        this._ignorePoint = ignorePoint;          this._searchTerms = searchTerms;          this._searchKanji = searchKanji;          this._searchOnClick = searchOnClick; +        this._searchOnClickOnly = searchOnClickOnly;          this._isPrepared = false; -        this._ignoreNodes = null; +        this._includeSelector = null; +        this._excludeSelector = null;          this._inputInfoCurrent = null;          this._scanTimerPromise = null; @@ -76,12 +88,20 @@ class TextScanner extends EventDispatcher {          this._canClearSelection = value;      } -    get ignoreNodes() { -        return this._ignoreNodes; +    get includeSelector() { +        return this._includeSelector; +    } + +    set includeSelector(value) { +        this._includeSelector = value;      } -    set ignoreNodes(value) { -        this._ignoreNodes = value; +    get excludeSelector() { +        return this._excludeSelector; +    } + +    set excludeSelector(value) { +        this._excludeSelector = value;      }      prepare() { @@ -178,15 +198,8 @@ class TextScanner extends EventDispatcher {          clonedTextSource.setEndOffset(length, layoutAwareScan); -        if (this._ignoreNodes !== null) { -            length = clonedTextSource.text().length; -            while ( -                length > 0 && -                DocumentUtil.anyNodeMatchesSelector(clonedTextSource.getNodesInRange(), this._ignoreNodes) -            ) { -                --length; -                clonedTextSource.setEndOffset(length, layoutAwareScan); -            } +        if (this._excludeSelector !== null) { +            this._constrainTextSource(clonedTextSource, this._includeSelector, this._excludeSelector, layoutAwareScan);          }          return clonedTextSource.text(); @@ -287,7 +300,7 @@ class TextScanner extends EventDispatcher {      }      _onMouseOver(e) { -        if (this._ignoreElements().includes(e.target)) { +        if (this._ignoreElements !== null && this._ignoreElements().includes(e.target)) {              this._scanTimerClear();          }      } @@ -613,7 +626,9 @@ class TextScanner extends EventDispatcher {      _hookEvents() {          let eventListenerInfos; -        if (this._arePointerEventsSupported()) { +        if (this._searchOnClickOnly) { +            eventListenerInfos = this._getMouseClickOnlyEventListeners(); +        } else if (this._arePointerEventsSupported()) {              eventListenerInfos = this._getPointerEventListeners();          } else {              eventListenerInfos = this._getMouseEventListeners(); @@ -652,6 +667,11 @@ class TextScanner extends EventDispatcher {          ];      } +    _getMouseClickOnlyEventListeners() { +        return [ +            [this._node, 'click', this._onClick.bind(this)] +        ]; +    }      _getTouchEventListeners() {          return [              [this._node, 'auxclick', this._onAuxClick.bind(this)], @@ -873,4 +893,20 @@ class TextScanner extends EventDispatcher {          const cachedPointerType = this._pointerIdTypeMap.get(e.pointerId);          return (typeof cachedPointerType !== 'undefined' ? cachedPointerType : e.pointerType);      } + +    _constrainTextSource(textSource, includeSelector, excludeSelector, layoutAwareScan) { +        let length = textSource.text().length; +        while (length > 0) { +            const nodes = textSource.getNodesInRange(); +            if ( +                (includeSelector !== null && !DocumentUtil.everyNodeMatchesSelector(nodes, includeSelector)) || +                (excludeSelector !== null && DocumentUtil.anyNodeMatchesSelector(nodes, excludeSelector)) +            ) { +                --length; +                textSource.setEndOffset(length, layoutAwareScan); +            } else { +                break; +            } +        } +    }  } |