From 33ec369eff87ace3e8b49c6461fa1b9a5d2a1202 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 10 Dec 2018 17:09:06 -0500 Subject: [PATCH 1/8] Catch exceptions thrown by range.getClientRects() Firefox dev 65 was throwing an exception --- ext/fg/js/document.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 9366832e..73e89d99 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -92,7 +92,19 @@ function docRangeFromPoint(point) { if(imposter !== null) imposter.style.zIndex = -2147483646; - const rect = range.getClientRects()[0]; + let rects; + try { + rects = range.getClientRects(); + } + catch (e) { + return; + } + + if (rects.length === 0) { + return; + } + + const rect = rects[0]; if (point.y > rect.bottom + 2) { return; } From 7ac7f55436b42ccc55feee70d16f59fdde907324 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 10 Dec 2018 17:23:53 -0500 Subject: [PATCH 2/8] Use "options_ui" in manifest --- ext/manifest.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/manifest.json b/ext/manifest.json index 7ee66f4f..ef8b84ec 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -27,6 +27,10 @@ }], "minimum_chrome_version": "57.0.0.0", "options_page": "bg/settings.html", + "options_ui": { + "page": "bg/settings.html", + "open_in_tab": true + }, "permissions": [ "", "storage", From 01f611d1898f046de2f367e8fe0ec6692ca7e445 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 10 Feb 2019 20:44:16 -0500 Subject: [PATCH 3/8] Add support for touch input --- ext/bg/js/options.js | 1 + ext/bg/js/settings.js | 2 + ext/bg/settings.html | 4 + ext/fg/js/frontend.js | 203 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 208 insertions(+), 2 deletions(-) diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 373a1a6b..f5f0bca7 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -201,6 +201,7 @@ function optionsSetDefaults(options) { scanning: { middleMouse: true, + touchInputEnabled: true, selectText: true, alphanumeric: true, autoHideResults: false, diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 7bc6a651..9da869c7 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -36,6 +36,7 @@ async function formRead() { optionsNew.general.popupOffset = parseInt($('#popup-offset').val(), 10); optionsNew.scanning.middleMouse = $('#middle-mouse-button-scan').prop('checked'); + optionsNew.scanning.touchInputEnabled = $('#touch-input-enabled').prop('checked'); optionsNew.scanning.selectText = $('#select-matched-text').prop('checked'); optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); @@ -166,6 +167,7 @@ async function onReady() { $('#popup-offset').val(options.general.popupOffset); $('#middle-mouse-button-scan').prop('checked', options.scanning.middleMouse); + $('#touch-input-enabled').prop('checked', options.scanning.touchInputEnabled); $('#select-matched-text').prop('checked', options.scanning.selectText); $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index c77f550c..fe4c32b1 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -105,6 +105,10 @@ +
+ +
+
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 266f9640..001ffc79 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -26,6 +26,14 @@ class Frontend { this.textSourceLast = null; this.pendingLookup = false; this.options = null; + + this.primaryTouchIdentifier = null; + this.contextMenuChecking = false; + this.contextMenuPrevent = false; + this.contextMenuPreviousRange = null; + this.mouseDownPrevent = false; + this.clickPrevent = false; + this.scrollPrevent = false; } async prepare() { @@ -39,6 +47,15 @@ class Frontend { window.addEventListener('mouseup', this.onMouseUp.bind(this)); window.addEventListener('resize', this.onResize.bind(this)); + if (this.options.scanning.touchInputEnabled) { + window.addEventListener('click', this.onClick.bind(this)); + window.addEventListener('touchstart', this.onTouchStart.bind(this)); + window.addEventListener('touchend', this.onTouchEnd.bind(this)); + window.addEventListener('touchcancel', this.onTouchCancel.bind(this)); + window.addEventListener('touchmove', this.onTouchMove.bind(this), {passive: false}); + window.addEventListener('contextmenu', this.onContextMenu.bind(this)); + } + chrome.runtime.onMessage.addListener(this.onBgMessage.bind(this)); } catch (e) { this.onError(e); @@ -79,7 +96,7 @@ class Frontend { const search = async () => { try { - await this.searchAt({x: e.clientX, y: e.clientY}); + await this.searchAt({x: e.clientX, y: e.clientY}, Frontend.SearchType.Mouse); } catch (e) { this.onError(e); } @@ -93,6 +110,14 @@ class Frontend { } onMouseDown(e) { + if (this.mouseDownPrevent) { + this.setMouseDownPrevent(false, false); + this.setClickPrevent(true); + e.preventDefault(); + e.stopPropagation(); + return false; + } + this.mousePosLast = {x: e.clientX, y: e.clientY}; this.popupTimerClear(); this.searchClear(); @@ -133,6 +158,85 @@ class Frontend { this.searchClear(); } + onClick(e) { + if (this.clickPrevent) { + this.setClickPrevent(false); + e.preventDefault(); + e.stopPropagation(); + return false; + } + } + + onTouchStart(e) { + if (this.primaryTouchIdentifier !== null && this.getIndexOfTouch(e.touches, this.primaryTouchIdentifier) >= 0) { + return; + } + + this.setPrimaryTouch(this.getPrimaryTouch(e.changedTouches)); + } + + onTouchEnd(e) { + if (this.primaryTouchIdentifier === null) { + return; + } + + if (this.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0) { + return; + } + + this.setPrimaryTouch(this.getPrimaryTouch(this.excludeTouches(e.touches, e.changedTouches))); + } + + onTouchCancel(e) { + this.onTouchEnd(e); + } + + onTouchMove(e) { + if (!this.scrollPrevent || this.primaryTouchIdentifier === null) { + return; + } + + const touches = e.changedTouches; + const index = this.getIndexOfTouch(touches, this.primaryTouchIdentifier); + if (index < 0) { + return; + } + + const touch = touches[index]; + this.searchFromTouch(touch.clientX, touch.clientY, Frontend.SearchType.TouchMove); + + e.preventDefault(); // Disable scroll + } + + onContextMenu(e) { + if (this.contextMenuPrevent) { + this.setContextMenuPrevent(false, false); + e.preventDefault(); + e.stopPropagation(); + return false; + } + } + + onAfterSearch(newRange, type, searched, success) { + if (type === Frontend.SearchType.Mouse) { + return; + } + + if ( + !this.contextMenuChecking || + (this.contextMenuPreviousRange === null ? newRange === null : this.contextMenuPreviousRange.equals(newRange))) { + return; + } + + if (type === Frontend.SearchType.TouchStart && newRange !== null) { + this.scrollPrevent = true; + } + + this.setContextMenuPrevent(true, false); + this.setMouseDownPrevent(true, false); + this.contextMenuChecking = false; + } + onBgMessage({action, params}, sender, callback) { const handlers = { optionsSet: options => { @@ -167,18 +271,22 @@ class Frontend { } } - async searchAt(point) { + async searchAt(point, type) { if (this.pendingLookup || this.popup.containsPoint(point)) { return; } const textSource = docRangeFromPoint(point); let hideResults = !textSource || !textSource.containsPoint(point); + let searched = false; + let success = false; try { if (!hideResults && (!this.textSourceLast || !this.textSourceLast.equals(textSource))) { + searched = true; this.pendingLookup = true; hideResults = !await this.searchTerms(textSource) && !await this.searchKanji(textSource); + success = true; } } catch (e) { if (window.yomichan_orphaned) { @@ -196,6 +304,7 @@ class Frontend { } this.pendingLookup = false; + this.onAfterSearch(this.textSourceLast, type, searched, success); } } @@ -262,7 +371,97 @@ class Frontend { this.textSourceLast = null; } + + getPrimaryTouch(touchList) { + return touchList.length > 0 ? touchList[0] : null; + } + + getIndexOfTouch(touchList, identifier) { + for (let i = 0, ii = touchList.length; i < ii; ++i) { + let t = touchList[i]; + if (t.identifier === identifier) { + return i; + } + } + return -1; + } + + excludeTouches(touchList, excludeTouchList) { + const result = []; + for (let i = 0, ii = touchList.length; i < ii; ++i) { + let r = result[i]; + 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, Frontend.SearchType.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, type) { + this.popupTimerClear(); + + if (!this.options.general.enable || this.pendingLookup) { + return; + } + + const pos = {x: x, y: y}; + + const search = async () => { + try { + await this.searchAt(pos, type); + } catch (e) { + this.onError(e); + } + }; + + search(); + } } +Frontend.SearchType = { + Mouse: 0, + TouchStart: 1, + TouchMove: 2 +}; + window.yomichan_frontend = new Frontend(); window.yomichan_frontend.prepare(); From ba972d8547089d8f2c8e19749ee0a0ab93d36233 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Thu, 14 Feb 2019 21:42:59 -0500 Subject: [PATCH 4/8] Add popup display mode Allows the popup to be stretched to the full width of the screen, anchored to the top or bottom of the window. --- ext/bg/js/options.js | 1 + ext/bg/js/settings.js | 2 ++ ext/bg/settings.html | 8 ++++++++ ext/fg/css/client.css | 22 ++++++++++++++++++++++ ext/fg/js/popup.js | 4 ++++ 5 files changed, 37 insertions(+) diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index f5f0bca7..f1e02e18 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -190,6 +190,7 @@ function optionsSetDefaults(options) { debugInfo: false, maxResults: 32, showAdvanced: false, + popupDisplayMode: 'default', popupWidth: 400, popupHeight: 250, popupOffset: 10, diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 9da869c7..4bf7181f 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -31,6 +31,7 @@ async function formRead() { optionsNew.general.debugInfo = $('#show-debug-info').prop('checked'); optionsNew.general.showAdvanced = $('#show-advanced-options').prop('checked'); optionsNew.general.maxResults = parseInt($('#max-displayed-results').val(), 10); + optionsNew.general.popupDisplayMode = $('#popup-display-mode').val(); optionsNew.general.popupWidth = parseInt($('#popup-width').val(), 10); optionsNew.general.popupHeight = parseInt($('#popup-height').val(), 10); optionsNew.general.popupOffset = parseInt($('#popup-offset').val(), 10); @@ -162,6 +163,7 @@ async function onReady() { $('#show-debug-info').prop('checked', options.general.debugInfo); $('#show-advanced-options').prop('checked', options.general.showAdvanced); $('#max-displayed-results').val(options.general.maxResults); + $('#popup-display-mode').val(options.general.popupDisplayMode); $('#popup-width').val(options.general.popupWidth); $('#popup-height').val(options.general.popupHeight); $('#popup-offset').val(options.general.popupOffset); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index fe4c32b1..7f18a358 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -74,6 +74,14 @@ +
+ + +
+
diff --git a/ext/fg/css/client.css b/ext/fg/css/client.css index b5b1f6bd..a9b8e025 100644 --- a/ext/fg/css/client.css +++ b/ext/fg/css/client.css @@ -27,3 +27,25 @@ iframe#yomichan-float { visibility: hidden; z-index: 2147483647; } + +iframe#yomichan-float.yomichan-float-full-width { + border-left: none; + border-right: none; + left: 0 !important; + right: 0 !important; + width: 100% !important; + box-sizing: border-box; + resize: none; +} + +iframe#yomichan-float.yomichan-float-full-width:not(.yomichan-float-above) { + border-bottom: none; + top: auto !important; + bottom: 0 !important; +} + +iframe#yomichan-float.yomichan-float-full-width.yomichan-float-above { + border-top: none; + top: 0 !important; + bottom: auto !important; +} diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index a17b184a..14276efe 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -62,6 +62,7 @@ class Popup { } } + let above = false; let y = 0; let height = Math.max(containerHeight, options.general.popupHeight); const yBelow = elementRect.bottom + options.general.popupOffset; @@ -75,11 +76,14 @@ class Popup { } else { height = Math.max(height - overflowAbove, 0); y = Math.max(yAbove - height, 0); + above = true; } } else { y = yBelow; } + this.container.classList.toggle('yomichan-float-full-width', options.general.popupDisplayMode === 'full-width'); + this.container.classList.toggle('yomichan-float-above', above); this.container.style.left = `${x}px`; this.container.style.top = `${y}px`; this.container.style.width = `${width}px`; From b58b91d3fce466457521b87dda850c8514c5e381 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 16 Feb 2019 22:34:00 -0500 Subject: [PATCH 5/8] Fix audio URLs --- ext/bg/js/audio.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index 549288f5..c93b7550 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -69,10 +69,10 @@ async function audioBuildUrl(definition, mode, cache={}) { const dom = new DOMParser().parseFromString(response, 'text/html'); for (const row of dom.getElementsByClassName('dc-result-row')) { try { - const url = row.getElementsByClassName('ill-onebuttonplayer').item(0).getAttribute('data-url'); + const url = row.querySelector('audio>source[src]').getAttribute('src'); const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText; if (url && reading && (!definition.reading || definition.reading === reading)) { - return url; + return normalizeAudioUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); } } catch (e) { // NOP @@ -86,7 +86,7 @@ async function audioBuildUrl(definition, mode, cache={}) { resolve(response); } else { const xhr = new XMLHttpRequest(); - xhr.open('GET', `http://jisho.org/search/${definition.expression}`); + xhr.open('GET', `https://jisho.org/search/${definition.expression}`); xhr.addEventListener('error', () => reject('Failed to scrape audio data')); xhr.addEventListener('load', () => { cache[definition.expression] = xhr.responseText; @@ -100,7 +100,10 @@ async function audioBuildUrl(definition, mode, cache={}) { const dom = new DOMParser().parseFromString(response, 'text/html'); const audio = dom.getElementById(`audio_${definition.expression}:${definition.reading}`); if (audio) { - return audio.getElementsByTagName('source').item(0).getAttribute('src'); + const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); + if (url) { + return normalizeAudioUrl(url, 'https://jisho.org', '/search/'); + } } } catch (e) { // NOP @@ -112,6 +115,24 @@ async function audioBuildUrl(definition, mode, cache={}) { } } +function normalizeAudioUrl(url, baseUrl, basePath) { + if (url) { + if (url[0] === '/') { + if (url.length >= 2 && url[1] === '/') { + // Begins with "//" + url = baseUrl.substr(0, baseUrl.indexOf(':') + 1) + url; + } else { + // Begins with "/" + url = baseUrl + url; + } + } else if (!/^[a-z][a-z0-9\+\-\.]*:/i.test(url)) { + // No URI scheme => relative path + url = baseUrl + basePath + url; + } + } + return url; +} + function audioBuildFilename(definition) { if (definition.reading || definition.expression) { let filename = 'yomichan'; From ee5e47251eb416952e0a4f8dfe923fe48565e64b Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 20 Feb 2019 21:16:40 -0500 Subject: [PATCH 6/8] Rename normalizeAudioUrl to audioUrlNormalize --- ext/bg/js/audio.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js index c93b7550..2e5db7cc 100644 --- a/ext/bg/js/audio.js +++ b/ext/bg/js/audio.js @@ -72,7 +72,7 @@ async function audioBuildUrl(definition, mode, cache={}) { const url = row.querySelector('audio>source[src]').getAttribute('src'); const reading = row.getElementsByClassName('dc-vocab_kana').item(0).innerText; if (url && reading && (!definition.reading || definition.reading === reading)) { - return normalizeAudioUrl(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); + return audioUrlNormalize(url, 'https://www.japanesepod101.com', '/learningcenter/reference/'); } } catch (e) { // NOP @@ -102,7 +102,7 @@ async function audioBuildUrl(definition, mode, cache={}) { if (audio) { const url = audio.getElementsByTagName('source').item(0).getAttribute('src'); if (url) { - return normalizeAudioUrl(url, 'https://jisho.org', '/search/'); + return audioUrlNormalize(url, 'https://jisho.org', '/search/'); } } } catch (e) { @@ -115,7 +115,7 @@ async function audioBuildUrl(definition, mode, cache={}) { } } -function normalizeAudioUrl(url, baseUrl, basePath) { +function audioUrlNormalize(url, baseUrl, basePath) { if (url) { if (url[0] === '/') { if (url.length >= 2 && url[1] === '/') { From dd52a1c01af117d69c953241fc9380596e7bab20 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 20 Feb 2019 21:47:31 -0500 Subject: [PATCH 7/8] Improve handling of null values from caretRangeFromPoint --- ext/fg/js/document.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 73e89d99..13acb036 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -85,20 +85,18 @@ function docRangeFromPoint(point) { range.setEnd(position.offsetNode, position.offset); return range; } + return null; }; } const range = document.caretRangeFromPoint(point.x, point.y); + if (range === null) { + return; + } if(imposter !== null) imposter.style.zIndex = -2147483646; - let rects; - try { - rects = range.getClientRects(); - } - catch (e) { - return; - } + const rects = range.getClientRects(); if (rects.length === 0) { return; From 0b8ae6c53a4b5b358a0af0617ccada019630d683 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 20 Feb 2019 22:05:07 -0500 Subject: [PATCH 8/8] Update frontend.js --- ext/fg/js/frontend.js | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 001ffc79..a81cbef8 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -96,7 +96,7 @@ class Frontend { const search = async () => { try { - await this.searchAt({x: e.clientX, y: e.clientY}, Frontend.SearchType.Mouse); + await this.searchAt({x: e.clientX, y: e.clientY}, 'mouse'); } catch (e) { this.onError(e); } @@ -203,7 +203,7 @@ class Frontend { } const touch = touches[index]; - this.searchFromTouch(touch.clientX, touch.clientY, Frontend.SearchType.TouchMove); + this.searchFromTouch(touch.clientX, touch.clientY, 'touchMove'); e.preventDefault(); // Disable scroll } @@ -218,7 +218,7 @@ class Frontend { } onAfterSearch(newRange, type, searched, success) { - if (type === Frontend.SearchType.Mouse) { + if (type === 'mouse') { return; } @@ -228,7 +228,7 @@ class Frontend { return; } - if (type === Frontend.SearchType.TouchStart && newRange !== null) { + if (type === 'touchStart' && newRange !== null) { this.scrollPrevent = true; } @@ -377,7 +377,7 @@ class Frontend { } getIndexOfTouch(touchList, identifier) { - for (let i = 0, ii = touchList.length; i < ii; ++i) { + for (let i in touchList) { let t = touchList[i]; if (t.identifier === identifier) { return i; @@ -388,8 +388,7 @@ class Frontend { excludeTouches(touchList, excludeTouchList) { const result = []; - for (let i = 0, ii = touchList.length; i < ii; ++i) { - let r = result[i]; + for (let r of touchList) { if (this.getIndexOfTouch(excludeTouchList, r.identifier) < 0) { result.push(r); } @@ -416,7 +415,7 @@ class Frontend { this.setMouseDownPrevent(false, false); this.setClickPrevent(false); - this.searchFromTouch(touch.clientX, touch.clientY, Frontend.SearchType.TouchStart); + this.searchFromTouch(touch.clientX, touch.clientY, 'touchStart'); } } @@ -443,11 +442,9 @@ class Frontend { return; } - const pos = {x: x, y: y}; - const search = async () => { try { - await this.searchAt(pos, type); + await this.searchAt({x, y}, type); } catch (e) { this.onError(e); } @@ -457,11 +454,5 @@ class Frontend { } } -Frontend.SearchType = { - Mouse: 0, - TouchStart: 1, - TouchMove: 2 -}; - window.yomichan_frontend = new Frontend(); window.yomichan_frontend.prepare();