diff options
Diffstat (limited to 'ext/mixed')
-rw-r--r-- | ext/mixed/css/display-dark.css | 4 | ||||
-rw-r--r-- | ext/mixed/css/display-default.css | 4 | ||||
-rw-r--r-- | ext/mixed/css/display.css | 9 | ||||
-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 |
13 files changed, 771 insertions, 81 deletions
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css index 681d248c..e26c72aa 100644 --- a/ext/mixed/css/display-dark.css +++ b/ext/mixed/css/display-dark.css @@ -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/css/display-default.css b/ext/mixed/css/display-default.css index add0a9c8..ac237e79 100644 --- a/ext/mixed/css/display-default.css +++ b/ext/mixed/css/display-default.css @@ -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/css/display.css b/ext/mixed/css/display.css index 9152216f..7a00bccb 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -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/>. */ @@ -79,10 +79,7 @@ ol, ul { .term-navigation { position: fixed; top: 0px; -} - -.term-button-fade { - opacity: 0.4; + right: 0px; } 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; |