diff options
Diffstat (limited to 'ext/fg')
-rw-r--r-- | ext/fg/float.html | 6 | ||||
-rw-r--r-- | ext/fg/js/api.js | 8 | ||||
-rw-r--r-- | ext/fg/js/frontend.js | 411 | ||||
-rw-r--r-- | ext/fg/js/popup.js | 2 |
4 files changed, 201 insertions, 226 deletions
diff --git a/ext/fg/float.html b/ext/fg/float.html index 580a7963..aec3db20 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -4,6 +4,12 @@ <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title></title> + <link rel="icon" type="image/png" href="/mixed/img/icon16.png" sizes="16x16"> + <link rel="icon" type="image/png" href="/mixed/img/icon19.png" sizes="19x19"> + <link rel="icon" type="image/png" href="/mixed/img/icon38.png" sizes="38x38"> + <link rel="icon" type="image/png" href="/mixed/img/icon48.png" sizes="48x48"> + <link rel="icon" type="image/png" href="/mixed/img/icon64.png" sizes="64x64"> + <link rel="icon" type="image/png" href="/mixed/img/icon128.png" sizes="128x128"> <link rel="stylesheet" href="/mixed/css/display.css"> <link rel="stylesheet" type="text/css" href="/mixed/css/display-default.css" data-yomichan-theme-name="default"> <link rel="stylesheet alternate" type="text/css" href="/mixed/css/display-dark.css" data-yomichan-theme-name="dark"> diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js index b0746b85..54818702 100644 --- a/ext/fg/js/api.js +++ b/ext/fg/js/api.js @@ -21,6 +21,10 @@ function apiOptionsGet(optionsContext) { return utilInvoke('optionsGet', {optionsContext}); } +function apiOptionsSet(changedOptions, optionsContext, source) { + return utilInvoke('optionsSet', {changedOptions, optionsContext, source}); +} + function apiTermsFind(text, optionsContext) { return utilInvoke('termsFind', {text, optionsContext}); } @@ -72,3 +76,7 @@ function apiInjectStylesheet(css) { function apiGetEnvironmentInfo() { return utilInvoke('getEnvironmentInfo'); } + +function apiClipboardGet() { + return utilInvoke('clipboardGet'); +} diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index e854f74e..e67008df 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -20,8 +20,8 @@ class Frontend { constructor(popup, ignoreNodes) { this.popup = popup; - this.popupTimer = null; - this.textSourceLast = null; + this.popupTimerPromise = null; + this.textSourceCurrent = null; this.pendingLookup = false; this.options = null; this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null); @@ -32,12 +32,10 @@ class Frontend { }; this.primaryTouchIdentifier = null; - this.contextMenuChecking = false; - this.contextMenuPrevent = false; - this.contextMenuPreviousRange = null; - this.mouseDownPrevent = false; - this.clickPrevent = false; - this.scrollPrevent = false; + this.preventNextContextMenu = false; + this.preventNextMouseDown = false; + this.preventNextClick = false; + this.preventScroll = false; this.enabled = false; this.eventListeners = []; @@ -74,7 +72,7 @@ class Frontend { } onMouseOver(e) { - if (e.target === this.popup.container && this.popupTimer !== null) { + if (e.target === this.popup.container) { this.popupTimerClear(); } } @@ -82,10 +80,7 @@ class Frontend { onMouseMove(e) { this.popupTimerClear(); - if ( - this.pendingLookup || - (e.buttons & 0x1) !== 0x0 // Left mouse button - ) { + if (this.pendingLookup || Frontend.isMouseButton('primary', e)) { return; } @@ -93,65 +88,60 @@ class Frontend { const scanningModifier = scanningOptions.modifier; if (!( Frontend.isScanningModifierPressed(scanningModifier, e) || - (scanningOptions.middleMouse && (e.buttons & 0x4) !== 0x0) // Middle mouse button + (scanningOptions.middleMouse && Frontend.isMouseButton('auxiliary', e)) )) { return; } const search = async () => { - try { - await this.searchAt(e.clientX, e.clientY, 'mouse'); - } catch (e) { - this.onError(e); + if (scanningModifier === 'none') { + if (!await this.popupTimerWait()) { + // Aborted + return; + } } + + await this.searchAt(e.clientX, e.clientY, 'mouse'); }; - if (scanningModifier === 'none') { - this.popupTimerSet(search); - } else { - search(); - } + search(); } onMouseDown(e) { - if (this.mouseDownPrevent) { - this.setMouseDownPrevent(false, false); - this.setClickPrevent(true); + if (this.preventNextMouseDown) { + this.preventNextMouseDown = false; + this.preventNextClick = true; e.preventDefault(); e.stopPropagation(); return false; } - this.popupTimerClear(); - this.searchClear(true); + if (e.button === 0) { + this.popupTimerClear(); + this.searchClear(true); + } } onMouseOut(e) { this.popupTimerClear(); } - onWindowMessage(e) { - const action = e.data; - const handlers = Frontend.windowMessageHandlers; - if (handlers.hasOwnProperty(action)) { - const handler = handlers[action]; - handler(this); + onClick(e) { + if (this.preventNextClick) { + this.preventNextClick = false; + e.preventDefault(); + e.stopPropagation(); + return false; } } - async onResize() { - if (this.textSourceLast !== null && await this.popup.isVisibleAsync()) { - const textSource = this.textSourceLast; - this.lastShowPromise = this.popup.showContent( - textSource.getRect(), - textSource.getWritingMode() - ); - } + onAuxClick(e) { + this.preventNextContextMenu = false; } - onClick(e) { - if (this.clickPrevent) { - this.setClickPrevent(false); + onContextMenu(e) { + if (this.preventNextContextMenu) { + this.preventNextContextMenu = false; e.preventDefault(); e.stopPropagation(); return false; @@ -159,28 +149,58 @@ class Frontend { } onTouchStart(e) { - if (this.primaryTouchIdentifier !== null && this.getIndexOfTouch(e.touches, this.primaryTouchIdentifier) >= 0) { + if (this.primaryTouchIdentifier !== null || e.changedTouches.length === 0) { return; } - let touch = this.getPrimaryTouch(e.changedTouches); - if (this.selectionContainsPoint(window.getSelection(), touch.clientX, touch.clientY)) { - touch = null; + this.preventScroll = false; + this.preventNextContextMenu = false; + this.preventNextMouseDown = false; + this.preventNextClick = false; + + const primaryTouch = e.changedTouches[0]; + if (Frontend.selectionContainsPoint(window.getSelection(), primaryTouch.clientX, primaryTouch.clientY)) { + return; } - this.setPrimaryTouch(touch); - } + this.primaryTouchIdentifier = primaryTouch.identifier; - onTouchEnd(e) { - if (this.primaryTouchIdentifier === null) { + if (this.pendingLookup) { return; } - if (this.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0) { + 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 || + this.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0 + ) { return; } - this.setPrimaryTouch(this.getPrimaryTouch(this.excludeTouches(e.touches, e.changedTouches))); + 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) { @@ -188,7 +208,7 @@ class Frontend { } onTouchMove(e) { - if (!this.scrollPrevent || !e.cancelable || this.primaryTouchIdentifier === null) { + if (!this.preventScroll || !e.cancelable || this.primaryTouchIdentifier === null) { return; } @@ -198,39 +218,29 @@ class Frontend { return; } - const touch = touches[index]; - this.searchFromTouch(touch.clientX, touch.clientY, 'touchMove'); + const primaryTouch = touches[index]; + this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove'); e.preventDefault(); // Disable scroll } - onContextMenu(e) { - if (this.contextMenuPrevent) { - this.setContextMenuPrevent(false, false); - e.preventDefault(); - e.stopPropagation(); - return false; + async onResize() { + if (this.textSourceCurrent !== null && await this.popup.isVisibleAsync()) { + const textSource = this.textSourceCurrent; + this.lastShowPromise = this.popup.showContent( + textSource.getRect(), + textSource.getWritingMode() + ); } } - onAfterSearch(newRange, cause, searched, success) { - if (cause === 'mouse') { - return; - } - - if ( - !this.contextMenuChecking || - (this.contextMenuPreviousRange === null ? newRange === null : this.contextMenuPreviousRange.equals(newRange))) { - return; - } - - if (cause === 'touchStart' && newRange !== null) { - this.scrollPrevent = true; + onWindowMessage(e) { + const action = e.data; + const handlers = Frontend.windowMessageHandlers; + if (handlers.hasOwnProperty(action)) { + const handler = handlers[action]; + handler(this); } - - this.setContextMenuPrevent(true, false); - this.setMouseDownPrevent(true, false); - this.contextMenuChecking = false; } onRuntimeMessage({action, params}, sender, callback) { @@ -271,6 +281,7 @@ class Frontend { if (this.options.scanning.touchInputEnabled) { this.addEventListener(window, 'click', this.onClick.bind(this)); + this.addEventListener(window, 'auxclick', this.onAuxClick.bind(this)); this.addEventListener(window, 'touchstart', this.onTouchStart.bind(this)); this.addEventListener(window, 'touchend', this.onTouchEnd.bind(this)); this.addEventListener(window, 'touchcancel', this.onTouchCancel.bind(this)); @@ -297,47 +308,69 @@ class Frontend { await this.popup.setOptions(this.options); } - popupTimerSet(callback) { + async popupTimerWait() { const delay = this.options.scanning.delay; - if (delay > 0) { - this.popupTimer = window.setTimeout(callback, delay); - } else { - Promise.resolve().then(callback); + const promise = promiseTimeout(delay, true); + this.popupTimerPromise = promise; + try { + return await promise; + } finally { + if (this.popupTimerPromise === promise) { + this.popupTimerPromise = null; + } } } popupTimerClear() { - if (this.popupTimer !== null) { - window.clearTimeout(this.popupTimer); - this.popupTimer = null; + if (this.popupTimerPromise !== null) { + this.popupTimerPromise.resolve(false); + this.popupTimerPromise = null; } } async searchAt(x, y, cause) { - if (this.pendingLookup || await this.popup.containsPoint(x, y)) { - return; - } + try { + this.popupTimerClear(); - const textSource = docRangeFromPoint(x, y, this.options); - return await this.searchSource(textSource, cause); + if (this.pendingLookup || await this.popup.containsPoint(x, y)) { + return; + } + + const textSource = docRangeFromPoint(x, y, this.options); + if (this.textSourceCurrent !== null && this.textSourceCurrent.equals(textSource)) { + return; + } + + try { + await this.searchSource(textSource, cause); + } finally { + if (textSource !== null) { + textSource.cleanup(); + } + } + } catch (e) { + this.onError(e); + } } async searchSource(textSource, cause) { - let hideResults = textSource === null; - let searched = false; - let success = false; + let results = null; try { - if (!hideResults && (!this.textSourceLast || !this.textSourceLast.equals(textSource))) { - searched = true; - this.pendingLookup = true; - const focus = (cause === 'mouse'); - hideResults = !await this.searchTerms(textSource, focus) && !await this.searchKanji(textSource, focus); - success = true; + this.pendingLookup = true; + if (textSource !== null) { + results = ( + await this.findTerms(textSource) || + await this.findKanji(textSource) + ); + if (results !== null) { + const focus = (cause === 'mouse'); + this.showContent(textSource, focus, results.definitions, results.type); + } } } catch (e) { if (window.yomichan_orphaned) { - if (textSource && this.options.scanning.modifier !== 'none') { + if (textSource !== null && this.options.scanning.modifier !== 'none') { this.lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), @@ -348,93 +381,69 @@ class Frontend { this.onError(e); } } finally { - if (textSource !== null) { - textSource.cleanup(); - } - if (hideResults && this.options.scanning.autoHideResults) { + if (results === null && this.options.scanning.autoHideResults) { this.searchClear(true); } this.pendingLookup = false; - this.onAfterSearch(this.textSourceLast, cause, searched, success); - } - } - - async searchTerms(textSource, focus) { - this.setTextSourceScanLength(textSource, this.options.scanning.length); - - const searchText = textSource.text(); - if (searchText.length === 0) { - return false; } - const {definitions, length} = await apiTermsFind(searchText, this.getOptionsContext()); - if (definitions.length === 0) { - return false; - } - - textSource.setEndOffset(length); + return results; + } + showContent(textSource, focus, definitions, type) { const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); const url = window.location.href; this.lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), - 'terms', + type, {definitions, context: {sentence, url, focus}} ); - this.textSourceLast = textSource; + this.textSourceCurrent = textSource; if (this.options.scanning.selectText) { textSource.select(); } + } + + async findTerms(textSource) { + this.setTextSourceScanLength(textSource, this.options.scanning.length); + + const searchText = textSource.text(); + if (searchText.length === 0) { return null; } + + const {definitions, length} = await apiTermsFind(searchText, this.getOptionsContext()); + if (definitions.length === 0) { return null; } - return true; + textSource.setEndOffset(length); + + return {definitions, type: 'terms'}; } - async searchKanji(textSource, focus) { + async findKanji(textSource) { this.setTextSourceScanLength(textSource, 1); const searchText = textSource.text(); - if (searchText.length === 0) { - return false; - } + if (searchText.length === 0) { return null; } const definitions = await apiKanjiFind(searchText, this.getOptionsContext()); - if (definitions.length === 0) { - return false; - } - - const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); - const url = window.location.href; - this.lastShowPromise = this.popup.showContent( - textSource.getRect(), - textSource.getWritingMode(), - 'kanji', - {definitions, context: {sentence, url, focus}} - ); - - this.textSourceLast = textSource; - if (this.options.scanning.selectText) { - textSource.select(); - } + if (definitions.length === 0) { return null; } - return true; + return {definitions, type: 'kanji'}; } searchClear(changeFocus) { this.popup.hide(changeFocus); this.popup.clearAutoPlayTimer(); - if (this.options.scanning.selectText && this.textSourceLast) { - this.textSourceLast.deselect(); - } - - this.textSourceLast = null; - } + if (this.textSourceCurrent !== null) { + if (this.options.scanning.selectText) { + this.textSourceCurrent.deselect(); + } - getPrimaryTouch(touchList) { - return touchList.length > 0 ? touchList[0] : null; + this.textSourceCurrent = null; + } } getIndexOfTouch(touchList, identifier) { @@ -447,74 +456,7 @@ class Frontend { return -1; } - excludeTouches(touchList, excludeTouchList) { - const result = []; - for (let r of touchList) { - if (this.getIndexOfTouch(excludeTouchList, r.identifier) < 0) { - result.push(r); - } - } - return result; - } - - setPrimaryTouch(touch) { - if (touch === null) { - this.primaryTouchIdentifier = null; - this.contextMenuPreviousRange = null; - this.contextMenuChecking = false; - this.scrollPrevent = false; - this.setContextMenuPrevent(false, true); - this.setMouseDownPrevent(false, true); - this.setClickPrevent(false); - } - else { - this.primaryTouchIdentifier = touch.identifier; - this.contextMenuPreviousRange = this.textSourceLast ? this.textSourceLast.clone() : null; - this.contextMenuChecking = true; - this.scrollPrevent = false; - this.setContextMenuPrevent(false, false); - this.setMouseDownPrevent(false, false); - this.setClickPrevent(false); - - this.searchFromTouch(touch.clientX, touch.clientY, 'touchStart'); - } - } - - setContextMenuPrevent(value, delay) { - if (!delay) { - this.contextMenuPrevent = value; - } - } - - setMouseDownPrevent(value, delay) { - if (!delay) { - this.mouseDownPrevent = value; - } - } - - setClickPrevent(value) { - this.clickPrevent = value; - } - - searchFromTouch(x, y, cause) { - this.popupTimerClear(); - - if (this.pendingLookup) { - return; - } - - const search = async () => { - try { - await this.searchAt(x, y, cause); - } catch (e) { - this.onError(e); - } - }; - - search(); - } - - selectionContainsPoint(selection, x, y) { + static selectionContainsPoint(selection, x, y) { for (let i = 0; i < selection.rangeCount; ++i) { const range = selection.getRangeAt(i); for (const rect of range.getClientRects()) { @@ -557,6 +499,25 @@ class Frontend { default: return false; } } + + static isMouseButton(button, mouseEvent) { + switch (mouseEvent.type) { + case 'mouseup': + case 'mousedown': + case 'click': switch (button) { + case 'primary': return mouseEvent.button === 0; + case 'secondary': return mouseEvent.button === 2; + case 'auxiliary': return mouseEvent.button === 1; + default: return false; + } + default: switch (button) { + case 'primary': return (mouseEvent.buttons & 0x1) !== 0x0; + case 'secondary': return (mouseEvent.buttons & 0x2) !== 0x0; + case 'auxiliary': return (mouseEvent.buttons & 0x4) !== 0x0; + default: return false; + } + } + } } Frontend.windowMessageHandlers = { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index b5eb9fe2..1f9317e0 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -30,7 +30,7 @@ class Popup { this.container.className = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); this.container.addEventListener('scroll', e => e.stopPropagation()); - this.container.setAttribute('src', chrome.extension.getURL('/fg/float.html')); + this.container.setAttribute('src', chrome.runtime.getURL('/fg/float.html')); this.container.style.width = '0px'; this.container.style.height = '0px'; this.injectPromise = null; |