diff options
| author | Alex Yatskov <alex@foosoft.net> | 2020-01-04 11:54:54 -0800 | 
|---|---|---|
| committer | Alex Yatskov <alex@foosoft.net> | 2020-01-04 11:54:54 -0800 | 
| commit | 2a12036ca305044291f1f4105d6a8d249848b210 (patch) | |
| tree | 5cfd4a3d837bf99730233a805d72395c8c61fc07 /ext/mixed/js | |
| parent | 9105cb5618cfdd14c2bc37cd22db2b360fe8cd52 (diff) | |
| parent | 174b92366577b0a638003b15e2d73fdc91cd62c3 (diff) | |
Merge branch 'master' into testing
Diffstat (limited to 'ext/mixed/js')
| -rw-r--r-- | ext/mixed/js/api.js | 130 | ||||
| -rw-r--r-- | ext/mixed/js/audio.js | 4 | ||||
| -rw-r--r-- | ext/mixed/js/core.js | 101 | ||||
| -rw-r--r-- | ext/mixed/js/display-context.js | 4 | ||||
| -rw-r--r-- | ext/mixed/js/display.js | 101 | ||||
| -rw-r--r-- | ext/mixed/js/dom.js | 4 | ||||
| -rw-r--r-- | ext/mixed/js/japanese.js | 13 | ||||
| -rw-r--r-- | ext/mixed/js/scroll.js | 4 | ||||
| -rw-r--r-- | ext/mixed/js/text-scanner.js | 378 | ||||
| -rw-r--r-- | ext/mixed/js/timer.js | 96 | 
10 files changed, 764 insertions, 71 deletions
| diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js new file mode 100644 index 00000000..8ed1d996 --- /dev/null +++ b/ext/mixed/js/api.js @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2016-2020  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 <https://www.gnu.org/licenses/>. + */ + + +function apiOptionsSchemaGet() { +    return _apiInvoke('optionsSchemaGet'); +} + +function apiOptionsGet(optionsContext) { +    return _apiInvoke('optionsGet', {optionsContext}); +} + +function apiOptionsGetFull() { +    return _apiInvoke('optionsGetFull'); +} + +function apiOptionsSet(changedOptions, optionsContext, source) { +    return _apiInvoke('optionsSet', {changedOptions, optionsContext, source}); +} + +function apiOptionsSave(source) { +    return _apiInvoke('optionsSave', {source}); +} + +function apiTermsFind(text, details, optionsContext) { +    return _apiInvoke('termsFind', {text, details, optionsContext}); +} + +function apiTextParse(text, optionsContext) { +    return _apiInvoke('textParse', {text, optionsContext}); +} + +function apiTextParseMecab(text, optionsContext) { +    return _apiInvoke('textParseMecab', {text, optionsContext}); +} + +function apiKanjiFind(text, optionsContext) { +    return _apiInvoke('kanjiFind', {text, optionsContext}); +} + +function apiDefinitionAdd(definition, mode, context, optionsContext) { +    return _apiInvoke('definitionAdd', {definition, mode, context, optionsContext}); +} + +function apiDefinitionsAddable(definitions, modes, optionsContext) { +    return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}).catch(() => null); +} + +function apiNoteView(noteId) { +    return _apiInvoke('noteView', {noteId}); +} + +function apiTemplateRender(template, data, dynamic) { +    return _apiInvoke('templateRender', {data, template, dynamic}); +} + +function apiAudioGetUrl(definition, source, optionsContext) { +    return _apiInvoke('audioGetUrl', {definition, source, optionsContext}); +} + +function apiCommandExec(command, params) { +    return _apiInvoke('commandExec', {command, params}); +} + +function apiScreenshotGet(options) { +    return _apiInvoke('screenshotGet', {options}); +} + +function apiForward(action, params) { +    return _apiInvoke('forward', {action, params}); +} + +function apiFrameInformationGet() { +    return _apiInvoke('frameInformationGet'); +} + +function apiInjectStylesheet(css) { +    return _apiInvoke('injectStylesheet', {css}); +} + +function apiGetEnvironmentInfo() { +    return _apiInvoke('getEnvironmentInfo'); +} + +function apiClipboardGet() { +    return _apiInvoke('clipboardGet'); +} + +function _apiInvoke(action, params={}) { +    const data = {action, params}; +    return new Promise((resolve, reject) => { +        try { +            chrome.runtime.sendMessage(data, (response) => { +                _apiCheckLastError(chrome.runtime.lastError); +                if (response !== null && typeof response === 'object') { +                    if (typeof response.error !== 'undefined') { +                        reject(jsonToError(response.error)); +                    } else { +                        resolve(response.result); +                    } +                } else { +                    const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; +                    reject(new Error(`${message} (${JSON.stringify(data)})`)); +                } +            }); +        } catch (e) { +            reject(e); +            yomichan.triggerOrphaned(e); +        } +    }); +} + +function _apiCheckLastError() { +    // NOP +} diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js index 35f283a4..b0c5fa82 100644 --- a/ext/mixed/js/audio.js +++ b/ext/mixed/js/audio.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@   * 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/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index b5911535..54e8a9d2 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@   * 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/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ @@ -118,6 +118,10 @@ function toIterable(value) {      throw new Error('Could not convert to iterable');  } +function stringReverse(string) { +    return string.split('').reverse().join('').replace(/([\uDC00-\uDFFF])([\uD800-\uDBFF])/g, '$2$1'); +} +  /*   * Async utilities @@ -175,3 +179,96 @@ function stringReplaceAsync(str, regex, replacer) {      parts.push(str.substring(index));      return Promise.all(parts).then((v) => v.join(''));  } + + +/* + * Common events + */ + +class EventDispatcher { +    constructor() { +        this._eventMap = new Map(); +    } + +    trigger(eventName, details) { +        const callbacks = this._eventMap.get(eventName); +        if (typeof callbacks === 'undefined') { return false; } + +        for (const callback of callbacks) { +            callback(details); +        } +    } + +    on(eventName, callback) { +        let callbacks = this._eventMap.get(eventName); +        if (typeof callbacks === 'undefined') { +            callbacks = []; +            this._eventMap.set(eventName, callbacks); +        } +        callbacks.push(callback); +    } + +    off(eventName, callback) { +        const callbacks = this._eventMap.get(eventName); +        if (typeof callbacks === 'undefined') { return true; } + +        const ii = callbacks.length; +        for (let i = 0; i < ii; ++i) { +            if (callbacks[i] === callback) { +                callbacks.splice(i, 1); +                if (callbacks.length === 0) { +                    this._eventMap.delete(eventName); +                } +                return true; +            } +        } +        return false; +    } +} + + +/* + * Default message handlers + */ + +const yomichan = (() => { +    class Yomichan extends EventDispatcher { +        constructor() { +            super(); + +            this._messageHandlers = new Map([ +                ['getUrl', this._onMessageGetUrl.bind(this)], +                ['optionsUpdate', this._onMessageOptionsUpdate.bind(this)] +            ]); + +            chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); +        } + +        // Public + +        triggerOrphaned(error) { +            this.trigger('orphaned', {error}); +        } + +        // Private + +        _onMessage({action, params}, sender, callback) { +            const handler = this._messageHandlers.get(action); +            if (typeof handler !== 'function') { return false; } + +            const result = handler(params, sender); +            callback(result); +            return false; +        } + +        _onMessageGetUrl() { +            return {url: window.location.href}; +        } + +        _onMessageOptionsUpdate({source}) { +            this.trigger('optionsUpdate', {source}); +        } +    } + +    return new Yomichan(); +})(); diff --git a/ext/mixed/js/display-context.js b/ext/mixed/js/display-context.js index 4b399881..c11c2342 100644 --- a/ext/mixed/js/display-context.js +++ b/ext/mixed/js/display-context.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@   * 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/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 7d5e4e7d..e756f948 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2017  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2017-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@   * 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/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ @@ -144,7 +144,7 @@ class Display {          try {              e.preventDefault(); -            const textSource = docRangeFromPoint(e.clientX, e.clientY, this.options); +            const textSource = docRangeFromPoint(e.clientX, e.clientY, this.options.scanning.deepDomScan);              if (textSource === null) {                  return false;              } @@ -193,9 +193,8 @@ class Display {      onKeyDown(e) {          const key = Display.getKeyFromEvent(e); -        const handlers = Display.onKeyDownHandlers; -        if (hasOwn(handlers, key)) { -            const handler = handlers[key]; +        const handler = Display._onKeyDownHandlers.get(key); +        if (typeof handler === 'function') {              if (handler(this, e)) {                  e.preventDefault();                  return true; @@ -211,23 +210,18 @@ class Display {                  e.preventDefault();              }          } else if (e.shiftKey) { -            const delta = -e.deltaX || e.deltaY; -            if (delta > 0) { -                this.sourceTermView(); -                e.preventDefault(); -            } else if (delta < 0) { -                this.nextTermView(); -                e.preventDefault(); -            } +            this.onHistoryWheel(e);          }      } -    onRuntimeMessage({action, params}, sender, callback) { -        const handlers = Display.runtimeMessageHandlers; -        if (hasOwn(handlers, action)) { -            const handler = handlers[action]; -            const result = handler(this, params); -            callback(result); +    onHistoryWheel(e) { +        const delta = -e.deltaX || e.deltaY; +        if (delta > 0) { +            this.sourceTermView(); +            e.preventDefault(); +        } else if (delta < 0) { +            this.nextTermView(); +            e.preventDefault();          }      } @@ -241,7 +235,7 @@ class Display {      async initialize(options=null) {          await this.updateOptions(options); -        chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); +        yomichan.on('optionsUpdate', () => this.updateOptions(null));      }      async updateOptions(options) { @@ -301,6 +295,7 @@ class Display {              this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));              this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this));              this.addEventListeners('.next-term', 'click', this.onNextTermView.bind(this)); +            this.addEventListeners('.term-navigation', 'wheel', this.onHistoryWheel.bind(this), {passive: false});              if (this.options.scanning.enablePopupSearch) {                  this.addEventListeners('.glossary-item', 'mouseup', this.onGlossaryMouseUp.bind(this));                  this.addEventListeners('.glossary-item', 'mousedown', this.onGlossaryMouseDown.bind(this)); @@ -761,101 +756,101 @@ class Display {      }  } -Display.onKeyDownHandlers = { -    'Escape': (self) => { +Display._onKeyDownHandlers = new Map([ +    ['Escape', (self) => {          self.onSearchClear();          return true; -    }, +    }], -    'PageUp': (self, e) => { +    ['PageUp', (self, e) => {          if (e.altKey) {              self.entryScrollIntoView(self.index - 3, null, true);              return true;          }          return false; -    }, +    }], -    'PageDown': (self, e) => { +    ['PageDown', (self, e) => {          if (e.altKey) {              self.entryScrollIntoView(self.index + 3, null, true);              return true;          }          return false; -    }, +    }], -    'End': (self, e) => { +    ['End', (self, e) => {          if (e.altKey) {              self.entryScrollIntoView(self.definitions.length - 1, null, true);              return true;          }          return false; -    }, +    }], -    'Home': (self, e) => { +    ['Home', (self, e) => {          if (e.altKey) {              self.entryScrollIntoView(0, null, true);              return true;          }          return false; -    }, +    }], -    'ArrowUp': (self, e) => { +    ['ArrowUp', (self, e) => {          if (e.altKey) {              self.entryScrollIntoView(self.index - 1, null, true);              return true;          }          return false; -    }, +    }], -    'ArrowDown': (self, e) => { +    ['ArrowDown', (self, e) => {          if (e.altKey) {              self.entryScrollIntoView(self.index + 1, null, true);              return true;          }          return false; -    }, +    }], -    'B': (self, e) => { +    ['B', (self, e) => {          if (e.altKey) {              self.sourceTermView();              return true;          }          return false; -    }, +    }], -    'F': (self, e) => { +    ['F', (self, e) => {          if (e.altKey) {              self.nextTermView();              return true;          }          return false; -    }, +    }], -    'E': (self, e) => { +    ['E', (self, e) => {          if (e.altKey) {              self.noteTryAdd('term-kanji');              return true;          }          return false; -    }, +    }], -    'K': (self, e) => { +    ['K', (self, e) => {          if (e.altKey) {              self.noteTryAdd('kanji');              return true;          }          return false; -    }, +    }], -    'R': (self, e) => { +    ['R', (self, e) => {          if (e.altKey) {              self.noteTryAdd('term-kana');              return true;          }          return false; -    }, +    }], -    'P': (self, e) => { +    ['P', (self, e) => {          if (e.altKey) {              const entry = self.getEntry(self.index);              if (entry !== null && entry.dataset.type === 'term') { @@ -864,17 +859,13 @@ Display.onKeyDownHandlers = {              return true;          }          return false; -    }, +    }], -    'V': (self, e) => { +    ['V', (self, e) => {          if (e.altKey) {              self.noteTryView();              return true;          }          return false; -    } -}; - -Display.runtimeMessageHandlers = { -    optionsUpdate: (self) => self.updateOptions(null) -}; +    }] +]); diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js index 4e4d49e3..807a48e1 100644 --- a/ext/mixed/js/dom.js +++ b/ext/mixed/js/dom.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@   * 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/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js index 8b841b2e..23b2bd36 100644 --- a/ext/mixed/js/japanese.js +++ b/ext/mixed/js/japanese.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2016  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2016-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@   * 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/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ @@ -160,16 +160,17 @@ function jpDistributeFuriganaInflected(expression, reading, source) {      }      const offset = source.length - stemLength; -    const stemExpression = source.slice(0, source.length - offset); -    const stemReading = reading.slice( -        0, offset === 0 ? reading.length : reading.length - expression.length + stemLength +    const stemExpression = source.substring(0, source.length - offset); +    const stemReading = reading.substring( +        0, +        offset === 0 ? reading.length : reading.length - expression.length + stemLength      );      for (const segment of jpDistributeFurigana(stemExpression, stemReading)) {          output.push(segment);      }      if (stemLength !== source.length) { -        output.push({text: source.slice(stemLength)}); +        output.push({text: source.substring(stemLength)});      }      return output; diff --git a/ext/mixed/js/scroll.js b/ext/mixed/js/scroll.js index 824fd92b..5829d294 100644 --- a/ext/mixed/js/scroll.js +++ b/ext/mixed/js/scroll.js @@ -1,5 +1,5 @@  /* - * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Copyright (C) 2019-2020  Alex Yatskov <alex@foosoft.net>   * Author: Alex Yatskov <alex@foosoft.net>   *   * This program is free software: you can redistribute it and/or modify @@ -13,7 +13,7 @@   * 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/>. + * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js new file mode 100644 index 00000000..a05dd2ee --- /dev/null +++ b/ext/mixed/js/text-scanner.js @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2019-2020  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 <https://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.scanning.deepDomScan); +            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; +    } +} diff --git a/ext/mixed/js/timer.js b/ext/mixed/js/timer.js new file mode 100644 index 00000000..1caf7a05 --- /dev/null +++ b/ext/mixed/js/timer.js @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2019-2020  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 <https://www.gnu.org/licenses/>. + */ + + +class Timer { +    constructor(name) { +        this.samples = []; +        this.parent = null; + +        this.sample(name); +        const current = Timer._current; +        if (current !== null) { +            current.samples[current.samples.length - 1].children.push(this); +            this.parent = current; +        } +        Timer._current = this; +    } + +    sample(name) { +        const time = window.performance.now(); +        this.samples.push({ +            name, +            time, +            children: [] +        }); +    } + +    complete(skip) { +        this.sample('complete'); + +        Timer._current = this.parent; +        if (this.parent === null) { +            if (!skip) { +                console.log(this.toString()); +            } +        } else { +            if (skip) { +                const sample = this.parent.samples[this.parent.samples.length - 1]; +                sample.children.splice(sample.children.length - 1, 1); +            } +        } +    } + +    duration(sampleIndex) { +        const sampleIndexIsValid = (typeof sampleIndex === 'number'); +        const startIndex = (sampleIndexIsValid ? sampleIndex : 0); +        const endIndex = (sampleIndexIsValid ? sampleIndex + 1 : this.times.length - 1); +        return (this.times[endIndex].time - this.times[startIndex].time); +    } + +    toString() { +        const indent = '  '; +        const name = this.samples[0].name; +        const duration = this.samples[this.samples.length - 1].time - this.samples[0].time; +        const extensionName = chrome.runtime.getManifest().name; +        return `${name} took ${duration.toFixed(8)}ms  [${extensionName}]` + Timer._indentString(this.getSampleString(), indent); +    } + +    getSampleString() { +        const indent = '  '; +        const duration = this.samples[this.samples.length - 1].time - this.samples[0].time; +        let message = ''; + +        for (let i = 0, ii = this.samples.length - 1; i < ii; ++i) { +            const sample = this.samples[i]; +            const sampleDuration = this.samples[i + 1].time - sample.time; +            message += `\nSample[${i}] took ${sampleDuration.toFixed(8)}ms (${((sampleDuration / duration) * 100.0).toFixed(1)}%)  [${sample.name}]`; +            for (const child of sample.children) { +                message += Timer._indentString(child.getSampleString(), indent); +            } +        } + +        return message; +    } + +    static _indentString(message, indent) { +        return message.replace(/\n/g, `\n${indent}`); +    } +} + +Timer._current = null; |