From 3c48290cd83744983df2e708b892a8415bcf750f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 11 Apr 2020 15:17:25 -0400 Subject: Add isExtensionUrl utility function to yomichan object --- ext/fg/js/popup.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 42f08afa..99610e17 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -539,19 +539,10 @@ class Popup { }; } - static _isOnExtensionPage() { - try { - const url = chrome.runtime.getURL('/'); - return window.location.href.substring(0, url.length) === url; - } catch (e) { - // NOP - } - } - static async _injectStylesheet(id, type, value, useWebExtensionApi) { const injectedStylesheets = Popup._injectedStylesheets; - if (Popup._isOnExtensionPage()) { + if (yomichan.isExtensionUrl(window.location.href)) { // Permissions error will occur if trying to use the WebExtension API to inject // into an extension page. useWebExtensionApi = false; -- cgit v1.2.3 From 4fdc300b61ebc3d36c3f5a511df92248453f8d55 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Fri, 17 Apr 2020 23:09:55 +0300 Subject: disable root frame popup when iframe is fullscreen --- ext/fg/js/frontend-initialize.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 2b942258..83c0e606 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -88,7 +88,7 @@ async function main() { } let popup; - if (isIframe && options.general.showIframePopupsInRootFrame) { + if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen) { popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder); popups.iframe = popup; } else if (proxy) { @@ -117,6 +117,7 @@ async function main() { }; yomichan.on('optionsUpdated', applyOptions); + window.addEventListener('fullscreenchange', applyOptions, false); await applyOptions(); } -- cgit v1.2.3 From fbaf50def1934ef6fe0967233f4419efc44f1c30 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 00:33:49 +0300 Subject: support iframes inside open shadow dom --- ext/fg/js/frame-offset-forwarder.js | 23 +++++++++++++++++++++-- test/data/html/test-document2.html | 19 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index c658c55a..ac6e617d 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -79,9 +79,28 @@ class FrameOffsetForwarder { sourceFrame = frame; break; } + if (sourceFrame === null) { - this._forwardFrameOffsetOrigin(null, uniqueId); - return; + const getShadowRootElements = (documentOrElement) => { + const elements = Array.from(documentOrElement.querySelectorAll('*')) + .filter((el) => !!el.shadowRoot); + const childElements = elements + .map((el) => el.shadowRoot) + .map(getShadowRootElements); + elements.push(childElements.flat()); + + return elements.flat(); + }; + + sourceFrame = getShadowRootElements(document) + .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) + .flat() + .find((el) => el.contentWindow === e.source); + + if (!sourceFrame) { + this._forwardFrameOffsetOrigin(null, uniqueId); + return; + } } const [forwardedX, forwardedY] = offset; diff --git a/test/data/html/test-document2.html b/test/data/html/test-document2.html index 3a22a5bf..b2046dfd 100644 --- a/test/data/html/test-document2.html +++ b/test/data/html/test-document2.html @@ -77,5 +77,22 @@ document.querySelector('#fullscreen-link1').addEventListener('click', () => togg +
+
<iframe> element inside of an open shadow DOM.
+
+ + +
+ - \ No newline at end of file + -- cgit v1.2.3 From 85706c421b7496d2d73a0f3d1f7721d39d5d0b3f Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 00:50:05 +0300 Subject: show popup inside iframe for closed shadow dom --- ext/fg/js/frontend-initialize.js | 15 +++++++++++---- ext/fg/js/popup-proxy.js | 7 ++++++- 2 files changed, 17 insertions(+), 5 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 83c0e606..2e63c29f 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -24,7 +24,7 @@ * apiOptionsGet */ -async function createIframePopupProxy(url, frameOffsetForwarder) { +async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) { const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( chrome.runtime.onMessage, ({action, params}, {resolve}) => { @@ -38,7 +38,7 @@ async function createIframePopupProxy(url, frameOffsetForwarder) { const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); - const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset); + const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset, setDisabled); await popup.prepare(); return popup; @@ -78,6 +78,13 @@ async function main() { let frontendPreparePromise = null; let frameOffsetForwarder = null; + let iframePopupsInRootFrameAvailable = true; + + const disableIframePopupsInRootFrame = () => { + iframePopupsInRootFrameAvailable = false; + applyOptions(); + }; + const applyOptions = async () => { const optionsContext = {depth: isSearchPage ? 0 : depth, url}; const options = await apiOptionsGet(optionsContext); @@ -88,8 +95,8 @@ async function main() { } let popup; - if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen) { - popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder); + if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen && iframePopupsInRootFrameAvailable) { + popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame); popups.iframe = popup; } else if (proxy) { popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url); diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 82ad9a8f..3af83db2 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -20,7 +20,7 @@ */ class PopupProxy { - constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null) { + constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null, setDisabled=null) { this._parentId = parentId; this._parentFrameId = parentFrameId; this._id = id; @@ -28,6 +28,7 @@ class PopupProxy { this._url = url; this._apiSender = new FrontendApiSender(); this._getFrameOffset = getFrameOffset; + this._setDisabled = setDisabled; this._frameOffset = null; this._frameOffsetPromise = null; @@ -142,6 +143,10 @@ class PopupProxy { try { const offset = await this._frameOffsetPromise; this._frameOffset = offset !== null ? offset : [0, 0]; + if (offset === null && this._setDisabled !== null) { + this._setDisabled(); + return; + } this._frameOffsetUpdatedAt = now; } catch (e) { logError(e); -- cgit v1.2.3 From b786e2da1912dfa7d707db628d54fb914189f7d1 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 00:55:16 +0300 Subject: move open shadow root iframe finder to a function --- ext/fg/js/frame-offset-forwarder.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index ac6e617d..2b48ba26 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -81,23 +81,9 @@ class FrameOffsetForwarder { } if (sourceFrame === null) { - const getShadowRootElements = (documentOrElement) => { - const elements = Array.from(documentOrElement.querySelectorAll('*')) - .filter((el) => !!el.shadowRoot); - const childElements = elements - .map((el) => el.shadowRoot) - .map(getShadowRootElements); - elements.push(childElements.flat()); - - return elements.flat(); - }; - - sourceFrame = getShadowRootElements(document) - .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) - .flat() - .find((el) => el.contentWindow === e.source); - + sourceFrame = this._getOpenShadowRootSourceFrame(e.source); if (!sourceFrame) { + // closed shadow root etc. this._forwardFrameOffsetOrigin(null, uniqueId); return; } @@ -110,6 +96,24 @@ class FrameOffsetForwarder { this._forwardFrameOffset(offset, uniqueId); } + _getOpenShadowRootSourceFrame(sourceWindow) { + const getShadowRootElements = (documentOrElement) => { + const elements = Array.from(documentOrElement.querySelectorAll('*')) + .filter((el) => !!el.shadowRoot); + const childElements = elements + .map((el) => el.shadowRoot) + .map(getShadowRootElements); + elements.push(childElements.flat()); + + return elements.flat(); + }; + + return getShadowRootElements(document) + .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) + .flat() + .find((el) => el.contentWindow === sourceWindow); + } + _forwardFrameOffsetParent(offset, uniqueId) { window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); } -- cgit v1.2.3 From 350a1139968ec3db4da95cd27c4ce8b5be45c56a Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 02:05:18 +0300 Subject: use getFullscreenElement to check fullscreen --- ext/fg/js/frontend-initialize.js | 3 ++- ext/fg/js/popup.js | 13 ++----------- ext/mixed/js/dom.js | 10 ++++++++++ 3 files changed, 14 insertions(+), 12 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js index 2e63c29f..2df59e20 100644 --- a/ext/fg/js/frontend-initialize.js +++ b/ext/fg/js/frontend-initialize.js @@ -16,6 +16,7 @@ */ /* global + * DOM * FrameOffsetForwarder * Frontend * PopupProxy @@ -95,7 +96,7 @@ async function main() { } let popup; - if (isIframe && options.general.showIframePopupsInRootFrame && !document.fullscreen && iframePopupsInRootFrameAvailable) { + if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) { popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame); popups.iframe = popup; } else if (proxy) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 99610e17..ae158263 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -16,6 +16,7 @@ */ /* global + * DOM * apiGetMessageToken * apiInjectStylesheet */ @@ -271,7 +272,7 @@ class Popup { } _onFullscreenChanged() { - const parent = (Popup._getFullscreenElement() || document.body || null); + const parent = (DOM.getFullscreenElement() || document.body || null); if (parent !== null && this._container.parentNode !== parent) { parent.appendChild(this._container); } @@ -365,16 +366,6 @@ class Popup { contentWindow.postMessage({action, params, token}, this._targetOrigin); } - static _getFullscreenElement() { - return ( - document.fullscreenElement || - document.msFullscreenElement || - document.mozFullScreenElement || - document.webkitFullscreenElement || - null - ); - } - static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js index 03acbb80..31ba33d6 100644 --- a/ext/mixed/js/dom.js +++ b/ext/mixed/js/dom.js @@ -62,4 +62,14 @@ class DOM { default: return false; } } + + static getFullscreenElement() { + return ( + document.fullscreenElement || + document.msFullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + null + ); + } } -- cgit v1.2.3 From bb3ad78e373b01b64a24fc46712f24964528a24f Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 16:48:49 +0300 Subject: optimize source frame finding --- ext/fg/js/frame-offset-forwarder.js | 66 +++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 29 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 2b48ba26..c2df7581 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -22,6 +22,7 @@ class FrameOffsetForwarder { constructor() { this._started = false; + this._frameCache = new Set(); this._forwardFrameOffset = ( window !== window.parent ? @@ -73,20 +74,11 @@ class FrameOffsetForwarder { } _onGetFrameOffset(offset, uniqueId, e) { - let sourceFrame = null; - for (const frame of document.querySelectorAll('frame, iframe:not(.yomichan-float)')) { - if (frame.contentWindow !== e.source) { continue; } - sourceFrame = frame; - break; - } - + const sourceFrame = this._findFrameWithContentWindow(e.source); if (sourceFrame === null) { - sourceFrame = this._getOpenShadowRootSourceFrame(e.source); - if (!sourceFrame) { - // closed shadow root etc. - this._forwardFrameOffsetOrigin(null, uniqueId); - return; - } + // closed shadow root etc. + this._forwardFrameOffsetOrigin(null, uniqueId); + return; } const [forwardedX, forwardedY] = offset; @@ -96,22 +88,38 @@ class FrameOffsetForwarder { this._forwardFrameOffset(offset, uniqueId); } - _getOpenShadowRootSourceFrame(sourceWindow) { - const getShadowRootElements = (documentOrElement) => { - const elements = Array.from(documentOrElement.querySelectorAll('*')) - .filter((el) => !!el.shadowRoot); - const childElements = elements - .map((el) => el.shadowRoot) - .map(getShadowRootElements); - elements.push(childElements.flat()); - - return elements.flat(); - }; - - return getShadowRootElements(document) - .map((el) => Array.from(el.shadowRoot.querySelectorAll('frame, iframe:not(.yomichan-float)'))) - .flat() - .find((el) => el.contentWindow === sourceWindow); + _findFrameWithContentWindow(contentWindow) { + const elements = [ + ...this._frameCache, + // will contain duplicates, but frame elements are cheap to handle + ...document.querySelectorAll('frame, iframe:not(.yomichan-float)'), + document.documentElement + ]; + const ELEMENT_NODE = Node.ELEMENT_NODE; + while (elements.length > 0) { + const element = elements.shift(); + if (element.contentWindow === contentWindow) { + this._frameCache.add(element); + return element; + } + + const shadowRoot = element.shadowRoot; + if (shadowRoot) { + for (const child of shadowRoot.children) { + if (child.nodeType === ELEMENT_NODE) { + elements.push(child); + } + } + } + + for (const child of element.children) { + if (child.nodeType === ELEMENT_NODE) { + elements.push(child); + } + } + } + + return null; } _forwardFrameOffsetParent(offset, uniqueId) { -- cgit v1.2.3 From 66354f1f9e866fd31f6bb0365024a39697a54079 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 17:18:33 +0300 Subject: lazy load element sources --- ext/fg/js/frame-offset-forwarder.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index c2df7581..4b77d5ed 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -89,14 +89,27 @@ class FrameOffsetForwarder { } _findFrameWithContentWindow(contentWindow) { - const elements = [ - ...this._frameCache, + const elementSources = [ + () => [...this._frameCache], // will contain duplicates, but frame elements are cheap to handle - ...document.querySelectorAll('frame, iframe:not(.yomichan-float)'), - document.documentElement + () => [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')], + () => [document.documentElement] ]; + const getMoreElements = () => { + while (true) { + const source = elementSources.shift(); + if (source) { + const elements = source(); + if (elements.length === 0) { continue; } + return elements; + } + return []; + } + }; + + const elements = []; const ELEMENT_NODE = Node.ELEMENT_NODE; - while (elements.length > 0) { + while (elements.length > 0 || elements.push(...getMoreElements())) { const element = elements.shift(); if (element.contentWindow === contentWindow) { this._frameCache.add(element); -- cgit v1.2.3 From 691b7398490bbf247070cd38603e51c7a6b66121 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 17:54:49 +0300 Subject: cache closed shadow dom content windows --- ext/fg/js/frame-offset-forwarder.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 4b77d5ed..72731605 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -23,6 +23,7 @@ class FrameOffsetForwarder { constructor() { this._started = false; this._frameCache = new Set(); + this._unreachableContentWindowCache = new Set(); this._forwardFrameOffset = ( window !== window.parent ? @@ -74,9 +75,13 @@ class FrameOffsetForwarder { } _onGetFrameOffset(offset, uniqueId, e) { - const sourceFrame = this._findFrameWithContentWindow(e.source); + let sourceFrame = null; + if (!this._unreachableContentWindowCache.has(e.source)) { + sourceFrame = this._findFrameWithContentWindow(e.source); + } if (sourceFrame === null) { // closed shadow root etc. + this._unreachableContentWindowCache.add(e.source); this._forwardFrameOffsetOrigin(null, uniqueId); return; } -- cgit v1.2.3 From a81c33b60aac0752ccca06f5183632146f6c6bf0 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 22:08:38 +0300 Subject: simplify element source lazy load --- ext/fg/js/frame-offset-forwarder.js | 56 +++++++++++++++---------------------- 1 file changed, 23 insertions(+), 33 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index 72731605..f40c642d 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -94,52 +94,42 @@ class FrameOffsetForwarder { } _findFrameWithContentWindow(contentWindow) { - const elementSources = [ - () => [...this._frameCache], - // will contain duplicates, but frame elements are cheap to handle - () => [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')], - () => [document.documentElement] - ]; - const getMoreElements = () => { - while (true) { - const source = elementSources.shift(); - if (source) { - const elements = source(); - if (elements.length === 0) { continue; } - return elements; + const ELEMENT_NODE = Node.ELEMENT_NODE; + for (const elements of this._getFrameElementSources()) { + while (elements.length > 0) { + const element = elements.shift(); + if (element.contentWindow === contentWindow) { + this._frameCache.add(element); + return element; } - return []; - } - }; - const elements = []; - const ELEMENT_NODE = Node.ELEMENT_NODE; - while (elements.length > 0 || elements.push(...getMoreElements())) { - const element = elements.shift(); - if (element.contentWindow === contentWindow) { - this._frameCache.add(element); - return element; - } + const shadowRoot = element.shadowRoot; + if (shadowRoot) { + for (const child of shadowRoot.children) { + if (child.nodeType === ELEMENT_NODE) { + elements.push(child); + } + } + } - const shadowRoot = element.shadowRoot; - if (shadowRoot) { - for (const child of shadowRoot.children) { + for (const child of element.children) { if (child.nodeType === ELEMENT_NODE) { elements.push(child); } } } - - for (const child of element.children) { - if (child.nodeType === ELEMENT_NODE) { - elements.push(child); - } - } } return null; } + *_getFrameElementSources() { + yield [...this._frameCache]; + // will contain duplicates, but frame elements are cheap to handle + yield [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')]; + yield [document.documentElement]; + } + _forwardFrameOffsetParent(offset, uniqueId) { window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); } -- cgit v1.2.3 From d66ca93ce4d6a4c9814bac4cc508c24ff87b8f69 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 18 Apr 2020 22:26:11 +0300 Subject: cache invalidation --- ext/fg/js/frame-offset-forwarder.js | 29 ++++++++++++++++++++++++++--- ext/manifest.json | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index f40c642d..1a2f3e1e 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -22,6 +22,8 @@ class FrameOffsetForwarder { constructor() { this._started = false; + + this._cacheMaxSize = 1000; this._frameCache = new Set(); this._unreachableContentWindowCache = new Set(); @@ -81,7 +83,7 @@ class FrameOffsetForwarder { } if (sourceFrame === null) { // closed shadow root etc. - this._unreachableContentWindowCache.add(e.source); + this._addToCache(this._unreachableContentWindowCache, e.source); this._forwardFrameOffsetOrigin(null, uniqueId); return; } @@ -99,7 +101,7 @@ class FrameOffsetForwarder { while (elements.length > 0) { const element = elements.shift(); if (element.contentWindow === contentWindow) { - this._frameCache.add(element); + this._addToCache(this._frameCache, element); return element; } @@ -124,12 +126,33 @@ class FrameOffsetForwarder { } *_getFrameElementSources() { - yield [...this._frameCache]; + const frameCache = []; + for (const frame of this._frameCache) { + // removed from DOM + if (!frame.isConnected) { + this._frameCache.delete(frame); + continue; + } + frameCache.push(frame); + } + yield frameCache; // will contain duplicates, but frame elements are cheap to handle yield [...document.querySelectorAll('frame, iframe:not(.yomichan-float)')]; yield [document.documentElement]; } + _addToCache(cache, value) { + let freeSlots = this._cacheMaxSize - cache.size; + if (freeSlots <= 0) { + for (const cachedValue of cache) { + cache.delete(cachedValue); + ++freeSlots; + if (freeSlots > 0) { break; } + } + } + cache.add(value); + } + _forwardFrameOffsetParent(offset, uniqueId) { window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); } diff --git a/ext/manifest.json b/ext/manifest.json index 452b642c..d383dab0 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -71,7 +71,7 @@ "applications": { "gecko": { "id": "alex@foosoft.net", - "strict_min_version": "52.0" + "strict_min_version": "53.0" } } } -- cgit v1.2.3 From 6c93d1984fbfe4fc21df4d0675fb4a82f2991ea3 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 19 Apr 2020 14:21:43 -0400 Subject: Change frontend-initialize.js to content-script-main.js --- ext/bg/js/search-frontend.js | 2 +- ext/fg/js/content-script-main.js | 133 +++++++++++++++++++++++++++++++++++++++ ext/fg/js/frontend-initialize.js | 133 --------------------------------------- ext/fg/js/popup-nested.js | 2 +- ext/manifest.json | 2 +- 5 files changed, 136 insertions(+), 136 deletions(-) create mode 100644 ext/fg/js/content-script-main.js delete mode 100644 ext/fg/js/frontend-initialize.js (limited to 'ext/fg/js') diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index e534e771..bda46a98 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -27,7 +27,7 @@ function injectSearchFrontend() { '/fg/js/popup.js', '/fg/js/popup-proxy-host.js', '/fg/js/frontend.js', - '/fg/js/frontend-initialize.js' + '/fg/js/content-script-main.js' ]; for (const src of scriptSrcs) { const node = document.querySelector(`script[src='${src}']`); diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js new file mode 100644 index 00000000..974320cc --- /dev/null +++ b/ext/fg/js/content-script-main.js @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2019-2020 Yomichan Authors + * + * 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 . + */ + +/* global + * DOM + * FrameOffsetForwarder + * Frontend + * PopupProxy + * PopupProxyHost + * apiBroadcastTab + * apiOptionsGet + */ + +async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) { + const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( + chrome.runtime.onMessage, + ({action, params}, {resolve}) => { + if (action === 'rootPopupInformation') { + resolve(params); + } + } + ); + apiBroadcastTab('rootPopupRequestInformationBroadcast'); + const {popupId, frameId} = await rootPopupInformationPromise; + + const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); + + const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset, setDisabled); + await popup.prepare(); + + return popup; +} + +async function getOrCreatePopup(depth) { + const popupHost = new PopupProxyHost(); + await popupHost.prepare(); + + const popup = popupHost.getOrCreatePopup(null, null, depth); + + return popup; +} + +async function createPopupProxy(depth, id, parentFrameId, url) { + const popup = new PopupProxy(null, depth + 1, id, parentFrameId, url); + await popup.prepare(); + + return popup; +} + +async function contentScriptMain() { + await yomichan.prepare(); + + const data = window.frontendInitializationData || {}; + const {id, depth=0, parentFrameId, url=window.location.href, proxy=false, isSearchPage=false} = data; + + const isIframe = !proxy && (window !== window.parent); + + const popups = { + iframe: null, + proxy: null, + normal: null + }; + + let frontend = null; + let frontendPreparePromise = null; + let frameOffsetForwarder = null; + + let iframePopupsInRootFrameAvailable = true; + + const disableIframePopupsInRootFrame = () => { + iframePopupsInRootFrameAvailable = false; + applyOptions(); + }; + + const applyOptions = async () => { + const optionsContext = {depth: isSearchPage ? 0 : depth, url}; + const options = await apiOptionsGet(optionsContext); + + if (!proxy && frameOffsetForwarder === null) { + frameOffsetForwarder = new FrameOffsetForwarder(); + frameOffsetForwarder.start(); + } + + let popup; + if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) { + popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame); + popups.iframe = popup; + } else if (proxy) { + popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url); + popups.proxy = popup; + } else { + popup = popups.normal || await getOrCreatePopup(depth); + popups.normal = popup; + } + + if (frontend === null) { + frontend = new Frontend(popup); + frontendPreparePromise = frontend.prepare(); + await frontendPreparePromise; + } else { + await frontendPreparePromise; + if (isSearchPage) { + const disabled = !options.scanning.enableOnSearchPage; + frontend.setDisabledOverride(disabled); + } + + if (isIframe) { + await frontend.setPopup(popup); + } + } + }; + + yomichan.on('optionsUpdated', applyOptions); + window.addEventListener('fullscreenchange', applyOptions, false); + + await applyOptions(); +} + +contentScriptMain(); diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js deleted file mode 100644 index 2df59e20..00000000 --- a/ext/fg/js/frontend-initialize.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2019-2020 Yomichan Authors - * - * 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 . - */ - -/* global - * DOM - * FrameOffsetForwarder - * Frontend - * PopupProxy - * PopupProxyHost - * apiBroadcastTab - * apiOptionsGet - */ - -async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) { - const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( - chrome.runtime.onMessage, - ({action, params}, {resolve}) => { - if (action === 'rootPopupInformation') { - resolve(params); - } - } - ); - apiBroadcastTab('rootPopupRequestInformationBroadcast'); - const {popupId, frameId} = await rootPopupInformationPromise; - - const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); - - const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset, setDisabled); - await popup.prepare(); - - return popup; -} - -async function getOrCreatePopup(depth) { - const popupHost = new PopupProxyHost(); - await popupHost.prepare(); - - const popup = popupHost.getOrCreatePopup(null, null, depth); - - return popup; -} - -async function createPopupProxy(depth, id, parentFrameId, url) { - const popup = new PopupProxy(null, depth + 1, id, parentFrameId, url); - await popup.prepare(); - - return popup; -} - -async function main() { - await yomichan.prepare(); - - const data = window.frontendInitializationData || {}; - const {id, depth=0, parentFrameId, url=window.location.href, proxy=false, isSearchPage=false} = data; - - const isIframe = !proxy && (window !== window.parent); - - const popups = { - iframe: null, - proxy: null, - normal: null - }; - - let frontend = null; - let frontendPreparePromise = null; - let frameOffsetForwarder = null; - - let iframePopupsInRootFrameAvailable = true; - - const disableIframePopupsInRootFrame = () => { - iframePopupsInRootFrameAvailable = false; - applyOptions(); - }; - - const applyOptions = async () => { - const optionsContext = {depth: isSearchPage ? 0 : depth, url}; - const options = await apiOptionsGet(optionsContext); - - if (!proxy && frameOffsetForwarder === null) { - frameOffsetForwarder = new FrameOffsetForwarder(); - frameOffsetForwarder.start(); - } - - let popup; - if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) { - popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame); - popups.iframe = popup; - } else if (proxy) { - popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url); - popups.proxy = popup; - } else { - popup = popups.normal || await getOrCreatePopup(depth); - popups.normal = popup; - } - - if (frontend === null) { - frontend = new Frontend(popup); - frontendPreparePromise = frontend.prepare(); - await frontendPreparePromise; - } else { - await frontendPreparePromise; - if (isSearchPage) { - const disabled = !options.scanning.enableOnSearchPage; - frontend.setDisabledOverride(disabled); - } - - if (isIframe) { - await frontend.setPopup(popup); - } - } - }; - - yomichan.on('optionsUpdated', applyOptions); - window.addEventListener('fullscreenchange', applyOptions, false); - - await applyOptions(); -} - -main(); diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index c140f9c8..27482931 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -26,7 +26,7 @@ function injectPopupNested() { '/fg/js/popup.js', '/fg/js/popup-proxy.js', '/fg/js/frontend.js', - '/fg/js/frontend-initialize.js' + '/fg/js/content-script-main.js' ]; for (const src of scriptSrcs) { const script = document.createElement('script'); diff --git a/ext/manifest.json b/ext/manifest.json index d383dab0..4f35b03c 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -31,7 +31,7 @@ "fg/js/popup-proxy.js", "fg/js/popup-proxy-host.js", "fg/js/frontend.js", - "fg/js/frontend-initialize.js" + "fg/js/content-script-main.js" ], "match_about_blank": true, "all_frames": true -- cgit v1.2.3 From 9ca906ef900dacfd10a548757d8a2b842a549088 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 19 Apr 2020 14:29:29 -0400 Subject: Create float-main.js --- ext/fg/float.html | 3 ++- ext/fg/js/float-main.js | 26 ++++++++++++++++++++++++++ ext/fg/js/float.js | 2 -- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 ext/fg/js/float-main.js (limited to 'ext/fg/js') diff --git a/ext/fg/float.html b/ext/fg/float.html index c8ea9b67..85c806fd 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -56,7 +56,8 @@ - + + diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js new file mode 100644 index 00000000..b752cdee --- /dev/null +++ b/ext/fg/js/float-main.js @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 . + */ + +/* global + * DisplayFloat + */ + +async function main() { + new DisplayFloat(); +} + +main(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 5c2c50c2..18d15a72 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -189,5 +189,3 @@ class DisplayFloat extends Display { } } } - -DisplayFloat.instance = new DisplayFloat(); -- cgit v1.2.3 From d106c638ed69b2c72895c1040b0e7bea2e31cdb7 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 21 Apr 2020 18:38:55 -0400 Subject: Move popup-nested content into float-main --- ext/fg/float.html | 1 - ext/fg/js/float-main.js | 48 +++++++++++++++++++++++++++++++++ ext/fg/js/popup-nested.js | 67 ----------------------------------------------- 3 files changed, 48 insertions(+), 68 deletions(-) delete mode 100644 ext/fg/js/popup-nested.js (limited to 'ext/fg/js') diff --git a/ext/fg/float.html b/ext/fg/float.html index 85c806fd..07c3c9e6 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -56,7 +56,6 @@ - diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index b752cdee..072c86e0 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -17,8 +17,56 @@ /* global * DisplayFloat + * apiOptionsGet */ +function injectPopupNested() { + const scriptSrcs = [ + '/mixed/js/text-scanner.js', + '/fg/js/frontend-api-sender.js', + '/fg/js/popup.js', + '/fg/js/popup-proxy.js', + '/fg/js/frontend.js', + '/fg/js/content-script-main.js' + ]; + for (const src of scriptSrcs) { + const script = document.createElement('script'); + script.async = false; + script.src = src; + document.body.appendChild(script); + } +} + +async function popupNestedInitialize(id, depth, parentFrameId, url) { + let optionsApplied = false; + + const applyOptions = async () => { + const optionsContext = {depth, url}; + const options = await apiOptionsGet(optionsContext); + const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth; + + const maxPopupDepthExceeded = !( + typeof popupNestingMaxDepth === 'number' && + typeof depth === 'number' && + depth < popupNestingMaxDepth + ); + if (maxPopupDepthExceeded || optionsApplied) { + return; + } + + optionsApplied = true; + + window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true}; + injectPopupNested(); + + yomichan.off('optionsUpdated', applyOptions); + }; + + yomichan.on('optionsUpdated', applyOptions); + + await applyOptions(); +} + async function main() { new DisplayFloat(); } diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js deleted file mode 100644 index 27482931..00000000 --- a/ext/fg/js/popup-nested.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2019-2020 Yomichan Authors - * - * 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 . - */ - -/* global - * apiOptionsGet - */ - -function injectPopupNested() { - const scriptSrcs = [ - '/mixed/js/text-scanner.js', - '/fg/js/frontend-api-sender.js', - '/fg/js/popup.js', - '/fg/js/popup-proxy.js', - '/fg/js/frontend.js', - '/fg/js/content-script-main.js' - ]; - for (const src of scriptSrcs) { - const script = document.createElement('script'); - script.async = false; - script.src = src; - document.body.appendChild(script); - } -} - -async function popupNestedInitialize(id, depth, parentFrameId, url) { - let optionsApplied = false; - - const applyOptions = async () => { - const optionsContext = {depth, url}; - const options = await apiOptionsGet(optionsContext); - const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth; - - const maxPopupDepthExceeded = !( - typeof popupNestingMaxDepth === 'number' && - typeof depth === 'number' && - depth < popupNestingMaxDepth - ); - if (maxPopupDepthExceeded || optionsApplied) { - return; - } - - optionsApplied = true; - - window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true}; - injectPopupNested(); - - yomichan.off('optionsUpdated', applyOptions); - }; - - yomichan.on('optionsUpdated', applyOptions); - - await applyOptions(); -} -- cgit v1.2.3 From d8276a9d5d119edf1747593608d3e135947019f0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Wed, 22 Apr 2020 21:42:20 -0400 Subject: Use IIFE for entry points --- ext/bg/js/background-main.js | 6 ++---- ext/bg/js/context-main.js | 6 ++---- ext/bg/js/search-main.js | 6 ++---- ext/bg/js/settings/popup-preview-frame-main.js | 6 ++---- ext/fg/js/content-script-main.js | 6 ++---- ext/fg/js/float-main.js | 6 ++---- 6 files changed, 12 insertions(+), 24 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/bg/js/background-main.js b/ext/bg/js/background-main.js index c000c38d..24117f4e 100644 --- a/ext/bg/js/background-main.js +++ b/ext/bg/js/background-main.js @@ -19,9 +19,7 @@ * Backend */ -async function main() { +(async () => { window.yomichanBackend = new Backend(); await window.yomichanBackend.prepare(); -} - -main(); +})(); diff --git a/ext/bg/js/context-main.js b/ext/bg/js/context-main.js index 67bb4e18..e2086a96 100644 --- a/ext/bg/js/context-main.js +++ b/ext/bg/js/context-main.js @@ -88,8 +88,6 @@ async function mainInner() { }); } -async function main() { +(async () => { window.addEventListener('DOMContentLoaded', mainInner, false); -} - -main(); +})(); diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 91c39222..38b6d99a 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -52,7 +52,7 @@ function injectSearchFrontend() { } } -async function main() { +(async () => { await yomichan.prepare(); const displaySearch = new DisplaySearch(); @@ -78,6 +78,4 @@ async function main() { yomichan.on('optionsUpdated', applyOptions); await applyOptions(); -} - -main(); +})(); diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js index e6e4727f..86c2814c 100644 --- a/ext/bg/js/settings/popup-preview-frame-main.js +++ b/ext/bg/js/settings/popup-preview-frame-main.js @@ -19,9 +19,7 @@ * SettingsPopupPreview */ -async function main() { +(async () => { const instance = new SettingsPopupPreview(); await instance.prepare(); -} - -main(); +})(); diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index 974320cc..14285536 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -61,7 +61,7 @@ async function createPopupProxy(depth, id, parentFrameId, url) { return popup; } -async function contentScriptMain() { +(async () => { await yomichan.prepare(); const data = window.frontendInitializationData || {}; @@ -128,6 +128,4 @@ async function contentScriptMain() { window.addEventListener('fullscreenchange', applyOptions, false); await applyOptions(); -} - -contentScriptMain(); +})(); diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 072c86e0..f056f707 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -67,8 +67,6 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { await applyOptions(); } -async function main() { +(async () => { new DisplayFloat(); -} - -main(); +})(); -- cgit v1.2.3 From ca033a87a0d302151b430acfdf9d480514c14aed Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 26 Apr 2020 22:33:50 +0300 Subject: Update Popup and DisplayFloat optionsContext from Frontend (#464) * set optionsContext from Frontend * update Popup+Display options on Frontend change * remove popup setOptions * only update DisplayFloat options from Frontend * fix optionsContext usage * fix preview frame arguments * keep Frontend URL up to date * cache url * fix preview frame * trigger modifyingProfileChange in correct places * remove async from function not using await * refactor optionsContext in Frontend --- ext/bg/js/search.js | 4 +- ext/bg/js/settings/popup-preview-frame-main.js | 5 +-- ext/bg/js/settings/popup-preview-frame.js | 22 ++++++++--- ext/bg/js/settings/popup-preview.js | 18 +++++++++ ext/bg/js/settings/profiles.js | 10 +++++ ext/fg/js/content-script-main.js | 31 +++++++++++---- ext/fg/js/float.js | 23 ++++++----- ext/fg/js/frontend.js | 55 ++++++++++++++------------ ext/fg/js/popup-proxy-host.js | 17 +++++--- ext/fg/js/popup-proxy.js | 19 +++++---- ext/fg/js/popup.js | 27 ++++++++----- ext/mixed/js/display.js | 2 - 12 files changed, 154 insertions(+), 79 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index 684ea6d3..a5484fc3 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -76,6 +76,8 @@ class DisplaySearch extends Display { async prepare() { try { await super.prepare(); + await this.updateOptions(); + yomichan.on('optionsUpdated', () => this.updateOptions()); await this.queryParser.prepare(); const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); @@ -231,7 +233,7 @@ class DisplaySearch extends Display { this.setIntroVisible(!valid, animate); this.updateSearchButton(); if (valid) { - const {definitions} = await apiTermsFind(query, details, this.optionsContext); + const {definitions} = await apiTermsFind(query, details, this.getOptionsContext()); this.setContent('terms', {definitions, context: { focus: false, disableHistory: true, diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js index 86c2814c..2ab6af6b 100644 --- a/ext/bg/js/settings/popup-preview-frame-main.js +++ b/ext/bg/js/settings/popup-preview-frame-main.js @@ -19,7 +19,6 @@ * SettingsPopupPreview */ -(async () => { - const instance = new SettingsPopupPreview(); - await instance.prepare(); +(() => { + new SettingsPopupPreview(); })(); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 420a7c5a..05a2a41b 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -32,19 +32,24 @@ class SettingsPopupPreview { this.popupShown = false; this.themeChangeTimeout = null; this.textSource = null; + this.optionsContext = null; this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); this._windowMessageHandlers = new Map([ + ['prepare', ({optionsContext}) => this.prepare(optionsContext)], ['setText', ({text}) => this.setText(text)], ['setCustomCss', ({css}) => this.setCustomCss(css)], - ['setCustomOuterCss', ({css}) => this.setCustomOuterCss(css)] + ['setCustomOuterCss', ({css}) => this.setCustomOuterCss(css)], + ['updateOptionsContext', ({optionsContext}) => this.updateOptionsContext(optionsContext)] ]); - } - async prepare() { - // Setup events window.addEventListener('message', this.onMessage.bind(this), false); + } + + async prepare(optionsContext) { + this.optionsContext = optionsContext; + // Setup events document.querySelector('#theme-dark-checkbox').addEventListener('change', this.onThemeDarkCheckboxChanged.bind(this), false); // Overwrite API functions @@ -62,8 +67,9 @@ class SettingsPopupPreview { this.frontend = new Frontend(this.popup); + this.frontend.getOptionsContext = async () => this.optionsContext; this.frontend.setEnabled = () => {}; - this.frontend.searchClear = () => {}; + this.frontend.onSearchClear = () => {}; await this.frontend.prepare(); @@ -145,6 +151,12 @@ class SettingsPopupPreview { this.frontend.popup.setCustomOuterCss(css, false); } + async updateOptionsContext(optionsContext) { + this.optionsContext = optionsContext; + await this.frontend.updateOptions(); + await this.updateSearch(); + } + async updateSearch() { const exampleText = document.querySelector('#example-text'); if (exampleText === null) { return; } diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js index 45bf531e..fdc3dd94 100644 --- a/ext/bg/js/settings/popup-preview.js +++ b/ext/bg/js/settings/popup-preview.js @@ -16,6 +16,7 @@ */ /* global + * getOptionsContext * wanakana */ @@ -60,6 +61,23 @@ function showAppearancePreview() { frame.contentWindow.postMessage({action, params}, targetOrigin); }); + const updateOptionsContext = () => { + const action = 'updateOptionsContext'; + const params = { + optionsContext: getOptionsContext() + }; + frame.contentWindow.postMessage({action, params}, targetOrigin); + }; + yomichan.on('modifyingProfileChange', updateOptionsContext); + + frame.addEventListener('load', () => { + const action = 'prepare'; + const params = { + optionsContext: getOptionsContext() + }; + frame.contentWindow.postMessage({action, params}, targetOrigin); + }); + container.append(frame); buttonContainer.remove(); settings.css('display', ''); diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js index 867b17aa..3f4b1da7 100644 --- a/ext/bg/js/settings/profiles.js +++ b/ext/bg/js/settings/profiles.js @@ -190,6 +190,8 @@ async function onTargetProfileChanged() { currentProfileIndex = index; await profileOptionsUpdateTarget(optionsFull); + + yomichan.trigger('modifyingProfileChange'); } async function onProfileAdd() { @@ -197,9 +199,13 @@ async function onProfileAdd() { const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]); profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100); optionsFull.profiles.push(profile); + currentProfileIndex = optionsFull.profiles.length - 1; + await profileOptionsUpdateTarget(optionsFull); await settingsSaveOptions(); + + yomichan.trigger('modifyingProfileChange'); } async function onProfileRemove(e) { @@ -238,6 +244,8 @@ async function onProfileRemoveConfirm() { await profileOptionsUpdateTarget(optionsFull); await settingsSaveOptions(); + + yomichan.trigger('modifyingProfileChange'); } function onProfileNameChanged() { @@ -263,6 +271,8 @@ async function onProfileMove(offset) { await profileOptionsUpdateTarget(optionsFull); await settingsSaveOptions(); + + yomichan.trigger('modifyingProfileChange'); } async function onProfileCopy() { diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index 14285536..0b852644 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -25,7 +25,7 @@ * apiOptionsGet */ -async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) { +async function createIframePopupProxy(frameOffsetForwarder, setDisabled) { const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( chrome.runtime.onMessage, ({action, params}, {resolve}) => { @@ -39,7 +39,7 @@ async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) { const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); - const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset, setDisabled); + const popup = new PopupProxy(popupId, 0, null, frameId, getFrameOffset, setDisabled); await popup.prepare(); return popup; @@ -54,8 +54,8 @@ async function getOrCreatePopup(depth) { return popup; } -async function createPopupProxy(depth, id, parentFrameId, url) { - const popup = new PopupProxy(null, depth + 1, id, parentFrameId, url); +async function createPopupProxy(depth, id, parentFrameId) { + const popup = new PopupProxy(null, depth + 1, id, parentFrameId); await popup.prepare(); return popup; @@ -86,8 +86,22 @@ async function createPopupProxy(depth, id, parentFrameId, url) { applyOptions(); }; + let urlUpdatedAt = 0; + let proxyHostUrlCached = url; + const getProxyHostUrl = async () => { + const now = Date.now(); + if (popups.proxy !== null && now - urlUpdatedAt > 500) { + proxyHostUrlCached = await popups.proxy.getHostUrl(); + urlUpdatedAt = now; + } + return proxyHostUrlCached; + }; + const applyOptions = async () => { - const optionsContext = {depth: isSearchPage ? 0 : depth, url}; + const optionsContext = { + depth: isSearchPage ? 0 : depth, + url: proxy ? await getProxyHostUrl() : window.location.href + }; const options = await apiOptionsGet(optionsContext); if (!proxy && frameOffsetForwarder === null) { @@ -97,10 +111,10 @@ async function createPopupProxy(depth, id, parentFrameId, url) { let popup; if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) { - popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame); + popup = popups.iframe || await createIframePopupProxy(frameOffsetForwarder, disableIframePopupsInRootFrame); popups.iframe = popup; } else if (proxy) { - popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url); + popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId); popups.proxy = popup; } else { popup = popups.normal || await getOrCreatePopup(depth); @@ -108,7 +122,8 @@ async function createPopupProxy(depth, id, parentFrameId, url) { } if (frontend === null) { - frontend = new Frontend(popup); + const getUrl = proxy ? getProxyHostUrl : null; + frontend = new Frontend(popup, getUrl); frontendPreparePromise = frontend.prepare(); await frontendPreparePromise; } else { diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 18d15a72..2a5eba83 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -29,11 +29,6 @@ class DisplayFloat extends Display { this._popupId = null; - this.optionsContext = { - depth: 0, - url: window.location.href - }; - this._orphaned = false; this._prepareInvoked = false; this._messageToken = null; @@ -51,10 +46,11 @@ class DisplayFloat extends Display { ]); this._windowMessageHandlers = new Map([ + ['setOptionsContext', ({optionsContext}) => this.setOptionsContext(optionsContext)], ['setContent', ({type, details}) => this.setContent(type, details)], ['clearAutoPlayTimer', () => this.clearAutoPlayTimer()], ['setCustomCss', ({css}) => this.setCustomCss(css)], - ['prepare', ({popupInfo, url, childrenSupported, scale}) => this.prepare(popupInfo, url, childrenSupported, scale)], + ['prepare', ({popupInfo, optionsContext, childrenSupported, scale}) => this.prepare(popupInfo, optionsContext, childrenSupported, scale)], ['setContentScale', ({scale}) => this.setContentScale(scale)] ]); @@ -62,18 +58,20 @@ class DisplayFloat extends Display { window.addEventListener('message', this.onMessage.bind(this), false); } - async prepare(popupInfo, url, childrenSupported, scale) { + async prepare(popupInfo, optionsContext, childrenSupported, scale) { if (this._prepareInvoked) { return; } this._prepareInvoked = true; - const {id, depth, parentFrameId} = popupInfo; + const {id, parentFrameId} = popupInfo; this._popupId = id; - this.optionsContext.depth = depth; - this.optionsContext.url = url; + + this.optionsContext = optionsContext; await super.prepare(); + await this.updateOptions(); if (childrenSupported) { + const {depth, url} = optionsContext; popupNestedInitialize(id, depth, parentFrameId, url); } @@ -158,6 +156,11 @@ class DisplayFloat extends Display { } } + async setOptionsContext(optionsContext) { + this.optionsContext = optionsContext; + await this.updateOptions(); + } + setContentScale(scale) { document.body.style.fontSize = `${scale}em`; } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index eecfe2e1..46921d36 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -26,24 +26,23 @@ */ class Frontend extends TextScanner { - constructor(popup) { + constructor(popup, getUrl=null) { super( window, () => this.popup.isProxy() ? [] : [this.popup.getContainer()], [(x, y) => this.popup.containsPoint(x, y)] ); + this._id = yomichan.generateId(16); + this.popup = popup; + this._getUrl = getUrl; + this._disabledOverride = false; this.options = null; - this.optionsContext = { - depth: popup.depth, - url: popup.url - }; - this._pageZoomFactor = 1.0; this._contentScale = 1.0; this._orphaned = false; @@ -143,11 +142,12 @@ class Frontend extends TextScanner { async setPopup(popup) { this.onSearchClear(false); this.popup = popup; - await popup.setOptions(this.options); + await popup.setOptionsContext(await this.getOptionsContext(), this._id); } async updateOptions() { - this.options = await apiOptionsGet(this.getOptionsContext()); + const optionsContext = await this.getOptionsContext(); + this.options = await apiOptionsGet(optionsContext); this.setOptions(this.options, this._canEnable()); const ignoreNodes = ['.scan-disable', '.scan-disable *']; @@ -156,7 +156,7 @@ class Frontend extends TextScanner { } this.ignoreNodes = ignoreNodes.join(','); - await this.popup.setOptions(this.options); + await this.popup.setOptionsContext(optionsContext, this._id); this._updateContentScale(); @@ -170,19 +170,20 @@ class Frontend extends TextScanner { try { if (textSource !== null) { + const optionsContext = await this.getOptionsContext(); results = ( - await this.findTerms(textSource) || - await this.findKanji(textSource) + await this.findTerms(textSource, optionsContext) || + await this.findKanji(textSource, optionsContext) ); if (results !== null) { const focus = (cause === 'mouse'); - this.showContent(textSource, focus, results.definitions, results.type); + this.showContent(textSource, focus, results.definitions, results.type, optionsContext); } } } catch (e) { if (this._orphaned) { if (textSource !== null && this.options.scanning.modifier !== 'none') { - this._showPopupContent(textSource, 'orphaned'); + this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned'); } } else { this.onError(e); @@ -196,11 +197,12 @@ class Frontend extends TextScanner { return results; } - showContent(textSource, focus, definitions, type) { + showContent(textSource, focus, definitions, type, optionsContext) { + const {url} = optionsContext; const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); - const url = window.location.href; this._showPopupContent( textSource, + optionsContext, type, {definitions, context: {sentence, url, focus, disableHistory: true}} ); @@ -210,13 +212,13 @@ class Frontend extends TextScanner { return this._lastShowPromise; } - async findTerms(textSource) { + async findTerms(textSource, optionsContext) { 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()); + const {definitions, length} = await apiTermsFind(searchText, {}, optionsContext); if (definitions.length === 0) { return null; } textSource.setEndOffset(length); @@ -224,13 +226,13 @@ class Frontend extends TextScanner { return {definitions, type: 'terms'}; } - async findKanji(textSource) { + async findKanji(textSource, optionsContext) { this.setTextSourceScanLength(textSource, 1); const searchText = textSource.text(); if (searchText.length === 0) { return null; } - const definitions = await apiKanjiFind(searchText, this.getOptionsContext()); + const definitions = await apiKanjiFind(searchText, optionsContext); if (definitions.length === 0) { return null; } return {definitions, type: 'kanji'}; @@ -242,17 +244,20 @@ class Frontend extends TextScanner { super.onSearchClear(changeFocus); } - getOptionsContext() { - this.optionsContext.url = this.popup.url; - return this.optionsContext; + async getOptionsContext() { + const url = this._getUrl !== null ? await this._getUrl() : window.location.href; + const depth = this.popup.depth; + return {depth, url}; } - _showPopupContent(textSource, type=null, details=null) { + _showPopupContent(textSource, optionsContext, type=null, details=null) { + const context = {optionsContext, source: this._id}; this._lastShowPromise = this.popup.showContent( textSource.getRect(), textSource.getWritingMode(), type, - details + details, + context ); return this._lastShowPromise; } @@ -294,7 +299,7 @@ class Frontend extends TextScanner { async _updatePopupPosition() { const textSource = this.getCurrentTextSource(); if (textSource !== null && await this.popup.isVisible()) { - this._showPopupContent(textSource); + this._showPopupContent(textSource, await this.getOptionsContext()); } } diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 958462ff..d87553e6 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -37,7 +37,7 @@ class PopupProxyHost { this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${this._frameId}`, new Map([ ['getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)], - ['setOptions', this._onApiSetOptions.bind(this)], + ['setOptionsContext', this._onApiSetOptionsContext.bind(this)], ['hide', this._onApiHide.bind(this)], ['isVisible', this._onApiIsVisibleAsync.bind(this)], ['setVisibleOverride', this._onApiSetVisibleOverride.bind(this)], @@ -45,7 +45,8 @@ class PopupProxyHost { ['showContent', this._onApiShowContent.bind(this)], ['setCustomCss', this._onApiSetCustomCss.bind(this)], ['clearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)], - ['setContentScale', this._onApiSetContentScale.bind(this)] + ['setContentScale', this._onApiSetContentScale.bind(this)], + ['getHostUrl', this._onApiGetHostUrl.bind(this)] ])); } @@ -103,9 +104,9 @@ class PopupProxyHost { }; } - async _onApiSetOptions({id, options}) { + async _onApiSetOptionsContext({id, optionsContext, source}) { const popup = this._getPopup(id); - return await popup.setOptions(options); + return await popup.setOptionsContext(optionsContext, source); } async _onApiHide({id, changeFocus}) { @@ -129,11 +130,11 @@ class PopupProxyHost { return await popup.containsPoint(x, y); } - async _onApiShowContent({id, elementRect, writingMode, type, details}) { + async _onApiShowContent({id, elementRect, writingMode, type, details, context}) { const popup = this._getPopup(id); elementRect = PopupProxyHost._convertJsonRectToDOMRect(popup, elementRect); if (!PopupProxyHost._popupCanShow(popup)) { return; } - return await popup.showContent(elementRect, writingMode, type, details); + return await popup.showContent(elementRect, writingMode, type, details, context); } async _onApiSetCustomCss({id, css}) { @@ -151,6 +152,10 @@ class PopupProxyHost { return popup.setContentScale(scale); } + async _onApiGetHostUrl() { + return window.location.href; + } + // Private functions _getPopup(id) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 3af83db2..cd3c1bc9 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -20,12 +20,11 @@ */ class PopupProxy { - constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null, setDisabled=null) { + constructor(id, depth, parentId, parentFrameId, getFrameOffset=null, setDisabled=null) { this._parentId = parentId; this._parentFrameId = parentFrameId; this._id = id; this._depth = depth; - this._url = url; this._apiSender = new FrontendApiSender(); this._getFrameOffset = getFrameOffset; this._setDisabled = setDisabled; @@ -49,10 +48,6 @@ class PopupProxy { return this._depth; } - get url() { - return this._url; - } - // Public functions async prepare() { @@ -64,8 +59,8 @@ class PopupProxy { return true; } - async setOptions(options) { - return await this._invokeHostApi('setOptions', {id: this._id, options}); + async setOptionsContext(optionsContext, source) { + return await this._invokeHostApi('setOptionsContext', {id: this._id, optionsContext, source}); } hide(changeFocus) { @@ -88,14 +83,14 @@ class PopupProxy { return await this._invokeHostApi('containsPoint', {id: this._id, x, y}); } - async showContent(elementRect, writingMode, type=null, details=null) { + async showContent(elementRect, writingMode, type, details, context) { let {x, y, width, height} = elementRect; if (this._getFrameOffset !== null) { await this._updateFrameOffset(); [x, y] = this._applyFrameOffset(x, y); } elementRect = {x, y, width, height}; - return await this._invokeHostApi('showContent', {id: this._id, elementRect, writingMode, type, details}); + return await this._invokeHostApi('showContent', {id: this._id, elementRect, writingMode, type, details, context}); } async setCustomCss(css) { @@ -110,6 +105,10 @@ class PopupProxy { this._invokeHostApi('setContentScale', {id: this._id, scale}); } + async getHostUrl() { + return await this._invokeHostApi('getHostUrl', {}); + } + // Private _invokeHostApi(action, params={}) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index ae158263..e735431b 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -19,6 +19,7 @@ * DOM * apiGetMessageToken * apiInjectStylesheet + * apiOptionsGet */ class Popup { @@ -33,10 +34,12 @@ class Popup { this._visible = false; this._visibleOverride = null; this._options = null; + this._optionsContext = null; this._contentScale = 1.0; this._containerSizeContentScale = null; this._targetOrigin = chrome.runtime.getURL('/').replace(/\/$/, ''); this._messageToken = null; + this._previousOptionsContextSource = null; this._container = document.createElement('iframe'); this._container.className = 'yomichan-float'; @@ -72,19 +75,20 @@ class Popup { return this._frameId; } - get url() { - return window.location.href; - } - // Public functions isProxy() { return false; } - async setOptions(options) { - this._options = options; + async setOptionsContext(optionsContext, source) { + this._optionsContext = optionsContext; + this._previousOptionsContextSource = source; + + this._options = await apiOptionsGet(optionsContext); this.updateTheme(); + + this._invokeApi('setOptionsContext', {optionsContext}); } hide(changeFocus) { @@ -120,8 +124,14 @@ class Popup { return false; } - async showContent(elementRect, writingMode, type=null, details=null) { + async showContent(elementRect, writingMode, type, details, context) { if (this._options === null) { throw new Error('Options not assigned'); } + + const {optionsContext, source} = context; + if (source !== this._previousOptionsContextSource) { + await this.setOptionsContext(optionsContext, source); + } + await this._show(elementRect, writingMode); if (type === null) { return; } this._invokeApi('setContent', {type, details}); @@ -219,10 +229,9 @@ class Popup { this._invokeApi('prepare', { popupInfo: { id: this._id, - depth: this._depth, parentFrameId }, - url: this.url, + optionsContext: this._optionsContext, childrenSupported: this._childrenSupported, scale: this._contentScale }); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 9587ec3b..70b7fcd3 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -177,8 +177,6 @@ class Display { async prepare() { await yomichan.prepare(); await this.displayGenerator.prepare(); - await this.updateOptions(); - yomichan.on('optionsUpdated', () => this.updateOptions()); } onError(_error) { -- cgit v1.2.3 From 5b96559df819f496b39acb75c679f6b3d8c8e65d Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Apr 2020 16:55:25 -0400 Subject: Error logging refactoring (#454) * Create new logging methods on yomichan object * Use new yomichan.logError instead of global logError * Remove old logError * Handle unhandledrejection events * Add addEventListener stub * Update log function * Update error conversion to support more types * Add log event * Add API log function * Log errors to the backend * Make error/warning logs update the badge * Clear log error indicator on extension button click * Log correct URL on the background page * Fix incorrect error conversion * Remove unhandledrejection handling Firefox doesn't support it properly. * Remove unused argument type from log function * Improve function name * Change console.warn to yomichan.logWarning * Move log forwarding initialization into main scripts --- .eslintrc.json | 1 - ext/bg/js/backend.js | 50 ++++++++++- ext/bg/js/context-main.js | 5 ++ ext/bg/js/database.js | 2 +- ext/bg/js/mecab.js | 2 +- ext/bg/js/search-main.js | 2 + ext/bg/js/search-query-parser.js | 2 +- ext/bg/js/search.js | 2 +- ext/bg/js/settings/backup.js | 2 +- ext/bg/js/settings/dictionaries.js | 2 +- ext/bg/js/settings/main.js | 2 + ext/bg/js/settings/popup-preview-frame-main.js | 2 + ext/fg/js/content-script-main.js | 2 + ext/fg/js/float-main.js | 2 + ext/fg/js/float.js | 2 +- ext/fg/js/frontend-api-sender.js | 8 +- ext/fg/js/popup-proxy.js | 2 +- ext/mixed/js/api.js | 22 +++++ ext/mixed/js/core.js | 112 +++++++++++++++++++------ ext/mixed/js/text-scanner.js | 2 +- test/test-database.js | 5 +- 21 files changed, 186 insertions(+), 45 deletions(-) (limited to 'ext/fg/js') diff --git a/.eslintrc.json b/.eslintrc.json index 8882cb42..78fec27c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -80,7 +80,6 @@ "yomichan": "readonly", "errorToJson": "readonly", "jsonToError": "readonly", - "logError": "readonly", "isObject": "readonly", "hasOwn": "readonly", "toIterable": "readonly", diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 693a9ad6..3c47b14e 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -78,6 +78,7 @@ class Backend { this._isPrepared = false; this._prepareError = false; this._badgePrepareDelayTimer = null; + this._logErrorLevel = null; this._messageHandlers = new Map([ ['yomichanCoreReady', {handler: this._onApiYomichanCoreReady.bind(this), async: false}], @@ -112,7 +113,9 @@ class Backend { ['getDictionaryInfo', {handler: this._onApiGetDictionaryInfo.bind(this), async: true}], ['getDictionaryCounts', {handler: this._onApiGetDictionaryCounts.bind(this), async: true}], ['purgeDatabase', {handler: this._onApiPurgeDatabase.bind(this), async: true}], - ['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}] + ['getMedia', {handler: this._onApiGetMedia.bind(this), async: true}], + ['log', {handler: this._onApiLog.bind(this), async: false}], + ['logIndicatorClear', {handler: this._onApiLogIndicatorClear.bind(this), async: false}] ]); this._commandHandlers = new Map([ @@ -164,7 +167,7 @@ class Backend { this._isPrepared = true; } catch (e) { this._prepareError = true; - logError(e); + yomichan.logError(e); throw e; } finally { if (this._badgePrepareDelayTimer !== null) { @@ -260,7 +263,7 @@ class Backend { this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, utilIsolate(options)); } catch (e) { // This shouldn't happen, but catch errors just in case of bugs - logError(e); + yomichan.logError(e); } } @@ -767,8 +770,34 @@ class Backend { return await this.database.getMedia(targets); } + _onApiLog({error, level, context}) { + yomichan.log(jsonToError(error), level, context); + + const levelValue = this._getErrorLevelValue(level); + if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; } + + this._logErrorLevel = level; + this._updateBadge(); + } + + _onApiLogIndicatorClear() { + if (this._logErrorLevel === null) { return; } + this._logErrorLevel = null; + this._updateBadge(); + } + // Command handlers + _getErrorLevelValue(errorLevel) { + switch (errorLevel) { + case 'info': return 0; + case 'debug': return 0; + case 'warn': return 1; + case 'error': return 2; + default: return 0; + } + } + async _onCommandSearch(params) { const {mode='existingOrNewTab', query} = params || {}; @@ -890,7 +919,20 @@ class Backend { let color = null; let status = null; - if (!this._isPrepared) { + if (this._logErrorLevel !== null) { + switch (this._logErrorLevel) { + case 'error': + text = '!!'; + color = '#f04e4e'; + status = 'Error'; + break; + default: // 'warn' + text = '!'; + color = '#f0ad4e'; + status = 'Warning'; + break; + } + } else if (!this._isPrepared) { if (this._prepareError) { text = '!!'; color = '#f04e4e'; diff --git a/ext/bg/js/context-main.js b/ext/bg/js/context-main.js index e2086a96..dbba0272 100644 --- a/ext/bg/js/context-main.js +++ b/ext/bg/js/context-main.js @@ -17,7 +17,9 @@ /* global * apiCommandExec + * apiForwardLogsToBackend * apiGetEnvironmentInfo + * apiLogIndicatorClear * apiOptionsGet */ @@ -52,8 +54,11 @@ function setupButtonEvents(selector, command, url) { } async function mainInner() { + apiForwardLogsToBackend(); await yomichan.prepare(); + await apiLogIndicatorClear(); + showExtensionInfo(); apiGetEnvironmentInfo().then(({browser}) => { diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js index 16612403..a94aa720 100644 --- a/ext/bg/js/database.js +++ b/ext/bg/js/database.js @@ -104,7 +104,7 @@ class Database { }); return true; } catch (e) { - logError(e); + yomichan.logError(e); return false; } } diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js index 597dceae..815ee860 100644 --- a/ext/bg/js/mecab.js +++ b/ext/bg/js/mecab.js @@ -24,7 +24,7 @@ class Mecab { } onError(error) { - logError(error, false); + yomichan.logError(error); } async checkVersion() { diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 38b6d99a..5e4d7a20 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -17,6 +17,7 @@ /* global * DisplaySearch + * apiForwardLogsToBackend * apiOptionsGet */ @@ -53,6 +54,7 @@ function injectSearchFrontend() { } (async () => { + apiForwardLogsToBackend(); await yomichan.prepare(); const displaySearch = new DisplaySearch(); diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index eb3b681c..0001c9ff 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -45,7 +45,7 @@ class QueryParser extends TextScanner { } onError(error) { - logError(error, false); + yomichan.logError(error); } onClick(e) { diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index a5484fc3..cbd7b562 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -122,7 +122,7 @@ class DisplaySearch extends Display { } onError(error) { - logError(error, true); + yomichan.logError(error); } onSearchClear() { diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js index bdfef658..faf4e592 100644 --- a/ext/bg/js/settings/backup.js +++ b/ext/bg/js/settings/backup.js @@ -133,7 +133,7 @@ async function _settingsImportSetOptionsFull(optionsFull) { } function _showSettingsImportError(error) { - logError(error); + yomichan.logError(error); document.querySelector('#settings-import-error-modal-message').textContent = `${error}`; $('#settings-import-error-modal').modal('show'); } diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js index 7eed4273..50add4c7 100644 --- a/ext/bg/js/settings/dictionaries.js +++ b/ext/bg/js/settings/dictionaries.js @@ -554,7 +554,7 @@ function dictionaryErrorsShow(errors) { if (errors !== null && errors.length > 0) { const uniqueErrors = new Map(); for (let e of errors) { - logError(e); + yomichan.logError(e); e = dictionaryErrorToString(e); let count = uniqueErrors.get(e); if (typeof count === 'undefined') { diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index 308e92eb..f03cc631 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -21,6 +21,7 @@ * ankiInitialize * ankiTemplatesInitialize * ankiTemplatesUpdateValue + * apiForwardLogsToBackend * apiOptionsSave * appearanceInitialize * audioSettingsInitialize @@ -284,6 +285,7 @@ function showExtensionInformation() { async function onReady() { + apiForwardLogsToBackend(); await yomichan.prepare(); showExtensionInformation(); diff --git a/ext/bg/js/settings/popup-preview-frame-main.js b/ext/bg/js/settings/popup-preview-frame-main.js index 2ab6af6b..8228125f 100644 --- a/ext/bg/js/settings/popup-preview-frame-main.js +++ b/ext/bg/js/settings/popup-preview-frame-main.js @@ -17,8 +17,10 @@ /* global * SettingsPopupPreview + * apiForwardLogsToBackend */ (() => { + apiForwardLogsToBackend(); new SettingsPopupPreview(); })(); diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index 0b852644..277e6567 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -22,6 +22,7 @@ * PopupProxy * PopupProxyHost * apiBroadcastTab + * apiForwardLogsToBackend * apiOptionsGet */ @@ -62,6 +63,7 @@ async function createPopupProxy(depth, id, parentFrameId) { } (async () => { + apiForwardLogsToBackend(); await yomichan.prepare(); const data = window.frontendInitializationData || {}; diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index f056f707..5ef4b07c 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -17,6 +17,7 @@ /* global * DisplayFloat + * apiForwardLogsToBackend * apiOptionsGet */ @@ -68,5 +69,6 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { } (async () => { + apiForwardLogsToBackend(); new DisplayFloat(); })(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 2a5eba83..fd3b92cc 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -84,7 +84,7 @@ class DisplayFloat extends Display { if (this._orphaned) { this.setContent('orphaned'); } else { - logError(error, true); + yomichan.logError(error); } } diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js index 1d539cab..0ad3f085 100644 --- a/ext/fg/js/frontend-api-sender.js +++ b/ext/fg/js/frontend-api-sender.js @@ -81,12 +81,12 @@ class FrontendApiSender { onAck(id) { const info = this.callbacks.get(id); if (typeof info === 'undefined') { - console.warn(`ID ${id} not found for ack`); + yomichan.logWarning(new Error(`ID ${id} not found for ack`)); return; } if (info.ack) { - console.warn(`Request ${id} already ack'd`); + yomichan.logWarning(new Error(`Request ${id} already ack'd`)); return; } @@ -98,12 +98,12 @@ class FrontendApiSender { onResult(id, data) { const info = this.callbacks.get(id); if (typeof info === 'undefined') { - console.warn(`ID ${id} not found`); + yomichan.logWarning(new Error(`ID ${id} not found`)); return; } if (!info.ack) { - console.warn(`Request ${id} not ack'd`); + yomichan.logWarning(new Error(`Request ${id} not ack'd`)); return; } diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index cd3c1bc9..93418202 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -148,7 +148,7 @@ class PopupProxy { } this._frameOffsetUpdatedAt = now; } catch (e) { - logError(e); + yomichan.logError(e); } finally { this._frameOffsetPromise = null; } diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js index 52f41646..afd68aa2 100644 --- a/ext/mixed/js/api.js +++ b/ext/mixed/js/api.js @@ -144,6 +144,14 @@ function apiGetMedia(targets) { return _apiInvoke('getMedia', {targets}); } +function apiLog(error, level, context) { + return _apiInvoke('log', {error, level, context}); +} + +function apiLogIndicatorClear() { + return _apiInvoke('logIndicatorClear'); +} + function _apiInvoke(action, params={}) { const data = {action, params}; return new Promise((resolve, reject) => { @@ -171,3 +179,17 @@ function _apiInvoke(action, params={}) { function _apiCheckLastError() { // NOP } + +let _apiForwardLogsToBackendEnabled = false; +function apiForwardLogsToBackend() { + if (_apiForwardLogsToBackendEnabled) { return; } + _apiForwardLogsToBackendEnabled = true; + + yomichan.on('log', async ({error, level, context}) => { + try { + await apiLog(errorToJson(error), level, context); + } catch (e) { + // NOP + } + }); +} diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 6a3298fc..fbe9943a 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -52,15 +52,28 @@ if (EXTENSION_IS_BROWSER_EDGE) { */ function errorToJson(error) { + try { + if (isObject(error)) { + return { + name: error.name, + message: error.message, + stack: error.stack, + data: error.data + }; + } + } catch (e) { + // NOP + } return { - name: error.name, - message: error.message, - stack: error.stack, - data: error.data + value: error, + hasValue: true }; } function jsonToError(jsonError) { + if (jsonError.hasValue) { + return jsonError.value; + } const error = new Error(jsonError.message); error.name = jsonError.name; error.stack = jsonError.stack; @@ -68,28 +81,6 @@ function jsonToError(jsonError) { return error; } -function logError(error, alert) { - const manifest = chrome.runtime.getManifest(); - let errorMessage = `${manifest.name} v${manifest.version} has encountered an error.\n`; - errorMessage += `Originating URL: ${window.location.href}\n`; - - const errorString = `${error.toString ? error.toString() : error}`; - const stack = `${error.stack}`.trimRight(); - if (!stack.startsWith(errorString)) { errorMessage += `${errorString}\n`; } - errorMessage += stack; - - const data = error.data; - if (typeof data !== 'undefined') { errorMessage += `\nData: ${JSON.stringify(data, null, 4)}`; } - - errorMessage += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; - - console.error(errorMessage); - - if (alert) { - window.alert(`${errorString}\n\nCheck the developer console for more details.`); - } -} - /* * Common helpers @@ -361,8 +352,77 @@ const yomichan = (() => { }); } + logWarning(error) { + this.log(error, 'warn'); + } + + logError(error) { + this.log(error, 'error'); + } + + log(error, level, context=null) { + if (!isObject(context)) { + context = this._getLogContext(); + } + + let errorString; + try { + errorString = error.toString(); + if (/^\[object \w+\]$/.test(errorString)) { + errorString = JSON.stringify(error); + } + } catch (e) { + errorString = `${error}`; + } + + let errorStack; + try { + errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : ''); + } catch (e) { + errorStack = ''; + } + + let errorData; + try { + errorData = error.data; + } catch (e) { + // NOP + } + + if (errorStack.startsWith(errorString)) { + errorString = errorStack; + } else if (errorStack.length > 0) { + errorString += `\n${errorStack}`; + } + + const manifest = chrome.runtime.getManifest(); + let message = `${manifest.name} v${manifest.version} has encountered a problem.`; + message += `\nOriginating URL: ${context.url}\n`; + message += errorString; + if (typeof errorData !== 'undefined') { + message += `\nData: ${JSON.stringify(errorData, null, 4)}`; + } + message += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; + + switch (level) { + case 'info': console.info(message); break; + case 'debug': console.debug(message); break; + case 'warn': console.warn(message); break; + case 'error': console.error(message); break; + default: console.log(message); break; + } + + this.trigger('log', {error, level, context}); + } + // Private + _getLogContext() { + return { + url: window.location.href + }; + } + _onMessage({action, params}, sender, callback) { const handler = this._messageHandlers.get(action); if (typeof handler !== 'function') { return false; } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 0cd12cd7..1c32714b 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -201,7 +201,7 @@ class TextScanner { } onError(error) { - logError(error, false); + yomichan.logError(error); } async scanTimerWait() { diff --git a/test/test-database.js b/test/test-database.js index e9ec3f0b..3684051b 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -145,7 +145,10 @@ const vm = new VM({ XMLHttpRequest, indexedDB: global.indexedDB, IDBKeyRange: global.IDBKeyRange, - JSZip: yomichanTest.JSZip + JSZip: yomichanTest.JSZip, + addEventListener() { + // NOP + } }); vm.context.window = vm.context; -- cgit v1.2.3 From 887d769786f2909dbd74e3465cef3551b780a49b Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 26 Apr 2020 16:56:34 -0400 Subject: Use dynamicLoader for main (#481) * Update style of search-main and float-main to have better parity * Use dynamicLoader to inject scripts and CSS --- ext/bg/js/search-main.js | 41 +++++++++++------------------------------ ext/bg/search.html | 1 + ext/fg/float.html | 1 + ext/fg/js/float-main.js | 30 ++++++++---------------------- 4 files changed, 21 insertions(+), 52 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 5e4d7a20..1075d46e 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -19,10 +19,14 @@ * DisplaySearch * apiForwardLogsToBackend * apiOptionsGet + * dynamicLoader */ -function injectSearchFrontend() { - const scriptSrcs = [ +async function injectSearchFrontend() { + dynamicLoader.loadStyles([ + '/fg/css/client.css' + ]); + await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', '/fg/js/frontend-api-receiver.js', '/fg/js/frame-offset-forwarder.js', @@ -30,27 +34,7 @@ function injectSearchFrontend() { '/fg/js/popup-proxy-host.js', '/fg/js/frontend.js', '/fg/js/content-script-main.js' - ]; - for (const src of scriptSrcs) { - const node = document.querySelector(`script[src='${src}']`); - if (node !== null) { continue; } - - const script = document.createElement('script'); - script.async = false; - script.src = src; - document.body.appendChild(script); - } - - const styleSrcs = [ - '/fg/css/client.css' - ]; - for (const src of styleSrcs) { - const style = document.createElement('link'); - style.rel = 'stylesheet'; - style.type = 'text/css'; - style.href = src; - document.head.appendChild(style); - } + ]); } (async () => { @@ -63,18 +47,15 @@ function injectSearchFrontend() { let optionsApplied = false; const applyOptions = async () => { - const optionsContext = { - depth: 0, - url: window.location.href - }; + const optionsContext = {depth: 0, url: window.location.href}; const options = await apiOptionsGet(optionsContext); if (!options.scanning.enableOnSearchPage || optionsApplied) { return; } + optionsApplied = true; + yomichan.off('optionsUpdated', applyOptions); window.frontendInitializationData = {depth: 1, proxy: false, isSearchPage: true}; - injectSearchFrontend(); - - yomichan.off('optionsUpdated', applyOptions); + await injectSearchFrontend(); }; yomichan.on('optionsUpdated', applyOptions); diff --git a/ext/bg/search.html b/ext/bg/search.html index 8ed6c838..52915b76 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -86,6 +86,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index deb9e9d2..6f37de52 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -52,6 +52,7 @@ + diff --git a/ext/fg/js/float-main.js b/ext/fg/js/float-main.js index 5ef4b07c..e7e50a54 100644 --- a/ext/fg/js/float-main.js +++ b/ext/fg/js/float-main.js @@ -19,23 +19,18 @@ * DisplayFloat * apiForwardLogsToBackend * apiOptionsGet + * dynamicLoader */ -function injectPopupNested() { - const scriptSrcs = [ +async function injectPopupNested() { + await dynamicLoader.loadScripts([ '/mixed/js/text-scanner.js', '/fg/js/frontend-api-sender.js', '/fg/js/popup.js', '/fg/js/popup-proxy.js', '/fg/js/frontend.js', '/fg/js/content-script-main.js' - ]; - for (const src of scriptSrcs) { - const script = document.createElement('script'); - script.async = false; - script.src = src; - document.body.appendChild(script); - } + ]); } async function popupNestedInitialize(id, depth, parentFrameId, url) { @@ -44,23 +39,14 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) { const applyOptions = async () => { const optionsContext = {depth, url}; const options = await apiOptionsGet(optionsContext); - const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth; - - const maxPopupDepthExceeded = !( - typeof popupNestingMaxDepth === 'number' && - typeof depth === 'number' && - depth < popupNestingMaxDepth - ); - if (maxPopupDepthExceeded || optionsApplied) { - return; - } + const maxPopupDepthExceeded = !(typeof depth === 'number' && depth < options.scanning.popupNestingMaxDepth); + if (maxPopupDepthExceeded || optionsApplied) { return; } optionsApplied = true; + yomichan.off('optionsUpdated', applyOptions); window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true}; - injectPopupNested(); - - yomichan.off('optionsUpdated', applyOptions); + await injectPopupNested(); }; yomichan.on('optionsUpdated', applyOptions); -- cgit v1.2.3 From 48c7010f4ea8daafd30e5650625c377affa0cecd Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Apr 2020 18:10:37 -0400 Subject: Frontend refactor (part 1) (#484) * Remove _getVisualViewportScale * Use super's mouse event listener definitions * Remove redundant override * Remove getTouchEventListeners override * Rename Display.onSearchClear to onEscape * Change onSearchClear to clearSelection and use an event * Update how text is marked for selection and deselection * Replace onError with yomichan.logError * Update setEnabled to refresh all event listeners --- ext/bg/js/search-query-parser.js | 25 ++--------------- ext/bg/js/search.js | 2 +- ext/bg/js/settings/popup-preview-frame.js | 2 +- ext/fg/js/float.js | 2 +- ext/fg/js/frontend.js | 26 ++++++++--------- ext/mixed/js/display.js | 4 +-- ext/mixed/js/text-scanner.js | 46 +++++++++++++++---------------- 7 files changed, 42 insertions(+), 65 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 0001c9ff..3215f8e4 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -44,12 +44,7 @@ class QueryParser extends TextScanner { await this.queryParserGenerator.prepare(); } - onError(error) { - yomichan.logError(error); - } - - onClick(e) { - super.onClick(e); + onClick2(e) { this.searchAt(e.clientX, e.clientY, 'click'); } @@ -84,22 +79,8 @@ class QueryParser extends TextScanner { getMouseEventListeners() { return [ - [this.node, 'click', this.onClick.bind(this)], - [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, '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)] + ...super.getMouseEventListeners(), + [this.node, 'click', this.onClick2.bind(this)] ]; } diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index cbd7b562..b7d2eed8 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -125,7 +125,7 @@ class DisplaySearch extends Display { yomichan.logError(error); } - onSearchClear() { + onEscape() { if (this.query === null) { return; } diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index 05a2a41b..e73c04a0 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -69,7 +69,7 @@ class SettingsPopupPreview { this.frontend.getOptionsContext = async () => this.optionsContext; this.frontend.setEnabled = () => {}; - this.frontend.onSearchClear = () => {}; + this.frontend.clearSelection = () => {}; await this.frontend.prepare(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index fd3b92cc..294093cd 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -92,7 +92,7 @@ class DisplayFloat extends Display { this._orphaned = true; } - onSearchClear() { + onEscape() { window.parent.postMessage('popupClose', '*'); } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 46921d36..50f52724 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -49,7 +49,7 @@ class Frontend extends TextScanner { this._lastShowPromise = Promise.resolve(); this._windowMessageHandlers = new Map([ - ['popupClose', () => this.onSearchClear(true)], + ['popupClose', () => this.clearSelection(false)], ['selectionCopy', () => document.execCommand('copy')] ]); @@ -79,10 +79,12 @@ class Frontend extends TextScanner { yomichan.on('zoomChanged', this.onZoomChanged.bind(this)); chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); + this.on('clearSelection', this.onClearSelection.bind(this)); + this._updateContentScale(); this._broadcastRootPopupInformation(); } catch (e) { - this.onError(e); + yomichan.logError(e); } } @@ -140,7 +142,7 @@ class Frontend extends TextScanner { } async setPopup(popup) { - this.onSearchClear(false); + this.clearSelection(true); this.popup = popup; await popup.setOptionsContext(await this.getOptionsContext(), this._id); } @@ -186,11 +188,11 @@ class Frontend extends TextScanner { this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned'); } } else { - this.onError(e); + yomichan.logError(e); } } finally { if (results === null && this.options.scanning.autoHideResults) { - this.onSearchClear(true); + this.clearSelection(false); } } @@ -238,10 +240,9 @@ class Frontend extends TextScanner { return {definitions, type: 'kanji'}; } - onSearchClear(changeFocus) { - this.popup.hide(changeFocus); + onClearSelection({passive}) { + this.popup.hide(!passive); this.popup.clearAutoPlayTimer(); - super.onSearchClear(changeFocus); } async getOptionsContext() { @@ -269,7 +270,9 @@ class Frontend extends TextScanner { contentScale /= this._pageZoomFactor; } if (popupScaleRelativeToVisualViewport) { - contentScale /= Frontend._getVisualViewportScale(); + const visualViewport = window.visualViewport; + const visualViewportScale = (visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0); + contentScale /= visualViewportScale; } if (contentScale === this._contentScale) { return; } @@ -302,9 +305,4 @@ class Frontend extends TextScanner { this._showPopupContent(textSource, await this.getOptionsContext()); } } - - static _getVisualViewportScale() { - const visualViewport = window.visualViewport; - return visualViewport !== null && typeof visualViewport === 'object' ? visualViewport.scale : 1.0; - } } diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 70b7fcd3..32081c70 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -69,7 +69,7 @@ class Display { this._onKeyDownHandlers = new Map([ ['Escape', () => { - this.onSearchClear(); + this.onEscape(); return true; }], ['PageUp', (e) => { @@ -183,7 +183,7 @@ class Display { throw new Error('Override me'); } - onSearchClear() { + onEscape() { throw new Error('Override me'); } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index 1c32714b..c582ccd8 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -21,8 +21,9 @@ * docRangeFromPoint */ -class TextScanner { +class TextScanner extends EventDispatcher { constructor(node, ignoreElements, ignorePoints) { + super(); this.node = node; this.ignoreElements = ignoreElements; this.ignorePoints = ignorePoints; @@ -32,6 +33,7 @@ class TextScanner { this.scanTimerPromise = null; this.causeCurrent = null; this.textSourceCurrent = null; + this.textSourceCurrentSelected = false; this.pendingLookup = false; this.options = null; @@ -92,7 +94,7 @@ class TextScanner { if (DOM.isMouseButtonDown(e, 'primary')) { this.scanTimerClear(); - this.onSearchClear(true); + this.clearSelection(false); } } @@ -200,10 +202,6 @@ class TextScanner { throw new Error('Override me'); } - onError(error) { - yomichan.logError(error); - } - async scanTimerWait() { const delay = this.options.scanning.delay; const promise = promiseTimeout(delay, true); @@ -225,17 +223,12 @@ class TextScanner { } setEnabled(enabled, canEnable) { - if (enabled && canEnable) { - if (!this.enabled) { - this.hookEvents(); - this.enabled = true; - } + this.eventListeners.removeAllEventListeners(); + this.enabled = enabled && canEnable; + if (this.enabled) { + this.hookEvents(); } else { - if (this.enabled) { - this.eventListeners.removeAllEventListeners(); - this.enabled = false; - } - this.onSearchClear(false); + this.clearSelection(true); } } @@ -300,10 +293,7 @@ class TextScanner { const result = await this.onSearchSource(textSource, cause); if (result !== null) { this.causeCurrent = cause; - this.textSourceCurrent = textSource; - if (this.options.scanning.selectText) { - textSource.select(); - } + this.setCurrentTextSource(textSource); } this.pendingLookup = false; } finally { @@ -312,7 +302,7 @@ class TextScanner { } } } catch (e) { - this.onError(e); + yomichan.logError(e); } } @@ -333,13 +323,15 @@ class TextScanner { } } - onSearchClear(_) { + clearSelection(passive) { if (this.textSourceCurrent !== null) { - if (this.options.scanning.selectText) { + if (this.textSourceCurrentSelected) { this.textSourceCurrent.deselect(); } this.textSourceCurrent = null; + this.textSourceCurrentSelected = false; } + this.trigger('clearSelection', {passive}); } getCurrentTextSource() { @@ -347,7 +339,13 @@ class TextScanner { } setCurrentTextSource(textSource) { - return this.textSourceCurrent = textSource; + this.textSourceCurrent = textSource; + if (this.options.scanning.selectText) { + this.textSourceCurrent.select(); + this.textSourceCurrentSelected = true; + } else { + this.textSourceCurrentSelected = false; + } } static isScanningModifierPressed(scanningModifier, mouseEvent) { -- cgit v1.2.3 From 08ada6844af424e8ff28e592fc6b9dbc1a9a97eb Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 12:47:15 -0400 Subject: Remove Frontend inheritance (#486) * Make Frontend use composition instead of inheritance for TextScanner * Use push instead of concat * Update setOptions and setEnabled APIs * Update how onWindowMessage event listener is added/removed * Rename options to _options * Use bind instead of arrow function * Fix selection being cleared due to settings changes --- ext/bg/js/search-query-parser.js | 1 + ext/bg/js/settings/popup-preview-frame.js | 9 +-- ext/fg/js/frontend.js | 94 +++++++++++++++++++------------ ext/mixed/js/text-scanner.js | 22 ++++++-- 4 files changed, 77 insertions(+), 49 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js index 3215f8e4..137234e8 100644 --- a/ext/bg/js/search-query-parser.js +++ b/ext/bg/js/search-query-parser.js @@ -86,6 +86,7 @@ class QueryParser extends TextScanner { setOptions(options) { super.setOptions(options); + super.setEnabled(true); this.queryParser.dataset.termSpacing = `${options.parsing.termSpacing}`; } diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index e73c04a0..cb548ed7 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -66,12 +66,10 @@ class SettingsPopupPreview { this.popup.setCustomOuterCss = this.popupSetCustomOuterCss.bind(this); this.frontend = new Frontend(this.popup); - this.frontend.getOptionsContext = async () => this.optionsContext; - this.frontend.setEnabled = () => {}; - this.frontend.clearSelection = () => {}; - await this.frontend.prepare(); + this.frontend.setDisabledOverride(true); + this.frontend.canClearSelection = false; // Update search this.updateSearch(); @@ -169,8 +167,7 @@ class SettingsPopupPreview { const source = new TextSourceRange(range, range.toString(), null, null); try { - await this.frontend.onSearchSource(source, 'script'); - this.frontend.setCurrentTextSource(source); + await this.frontend.setTextSource(source); } finally { source.cleanup(); } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 50f52724..76ad27e0 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -25,14 +25,8 @@ * docSentenceExtract */ -class Frontend extends TextScanner { +class Frontend { constructor(popup, getUrl=null) { - super( - window, - () => this.popup.isProxy() ? [] : [this.popup.getContainer()], - [(x, y) => this.popup.containsPoint(x, y)] - ); - this._id = yomichan.generateId(16); this.popup = popup; @@ -41,15 +35,23 @@ class Frontend extends TextScanner { this._disabledOverride = false; - this.options = null; + this._options = null; this._pageZoomFactor = 1.0; this._contentScale = 1.0; this._orphaned = false; this._lastShowPromise = Promise.resolve(); + this._enabledEventListeners = new EventListenerCollection(); + this._textScanner = new TextScanner( + window, + () => this.popup.isProxy() ? [] : [this.popup.getContainer()], + [(x, y) => this.popup.containsPoint(x, y)] + ); + this._textScanner.onSearchSource = this.onSearchSource.bind(this); + this._windowMessageHandlers = new Map([ - ['popupClose', () => this.clearSelection(false)], + ['popupClose', () => this._textScanner.clearSelection(false)], ['selectionCopy', () => document.execCommand('copy')] ]); @@ -60,6 +62,14 @@ class Frontend extends TextScanner { ]); } + get canClearSelection() { + return this._textScanner.canClearSelection; + } + + set canClearSelection(value) { + this._textScanner.canClearSelection = value; + } + async prepare() { try { await this.updateOptions(); @@ -79,7 +89,7 @@ class Frontend extends TextScanner { yomichan.on('zoomChanged', this.onZoomChanged.bind(this)); chrome.runtime.onMessage.addListener(this.onRuntimeMessage.bind(this)); - this.on('clearSelection', this.onClearSelection.bind(this)); + this._textScanner.on('clearSelection', this.onClearSelection.bind(this)); this._updateContentScale(); this._broadcastRootPopupInformation(); @@ -129,44 +139,45 @@ class Frontend extends TextScanner { this._updateContentScale(); } - getMouseEventListeners() { - return [ - ...super.getMouseEventListeners(), - [window, 'message', this.onWindowMessage.bind(this)] - ]; - } - setDisabledOverride(disabled) { this._disabledOverride = disabled; - this.setEnabled(this.options.general.enable, this._canEnable()); + this._updateTextScannerEnabled(); } async setPopup(popup) { - this.clearSelection(true); + this._textScanner.clearSelection(true); this.popup = popup; await popup.setOptionsContext(await this.getOptionsContext(), this._id); } async updateOptions() { const optionsContext = await this.getOptionsContext(); - this.options = await apiOptionsGet(optionsContext); - this.setOptions(this.options, this._canEnable()); + this._options = await apiOptionsGet(optionsContext); + this._textScanner.setOptions(this._options); + this._updateTextScannerEnabled(); const ignoreNodes = ['.scan-disable', '.scan-disable *']; - if (!this.options.scanning.enableOnPopupExpressions) { + if (!this._options.scanning.enableOnPopupExpressions) { ignoreNodes.push('.source-text', '.source-text *'); } - this.ignoreNodes = ignoreNodes.join(','); + this._textScanner.ignoreNodes = ignoreNodes.join(','); await this.popup.setOptionsContext(optionsContext, this._id); this._updateContentScale(); - if (this.textSourceCurrent !== null && this.causeCurrent !== null) { - await this.onSearchSource(this.textSourceCurrent, this.causeCurrent); + const textSourceCurrent = this._textScanner.getCurrentTextSource(); + const causeCurrent = this._textScanner.causeCurrent; + if (textSourceCurrent !== null && causeCurrent !== null) { + await this.onSearchSource(textSourceCurrent, causeCurrent); } } + async setTextSource(textSource) { + await this.onSearchSource(textSource, 'script'); + this._textScanner.setCurrentTextSource(textSource); + } + async onSearchSource(textSource, cause) { let results = null; @@ -184,15 +195,15 @@ class Frontend extends TextScanner { } } catch (e) { if (this._orphaned) { - if (textSource !== null && this.options.scanning.modifier !== 'none') { + if (textSource !== null && this._options.scanning.modifier !== 'none') { this._showPopupContent(textSource, await this.getOptionsContext(), 'orphaned'); } } else { yomichan.logError(e); } } finally { - if (results === null && this.options.scanning.autoHideResults) { - this.clearSelection(false); + if (results === null && this._options.scanning.autoHideResults) { + this._textScanner.clearSelection(false); } } @@ -201,7 +212,7 @@ class Frontend extends TextScanner { showContent(textSource, focus, definitions, type, optionsContext) { const {url} = optionsContext; - const sentence = docSentenceExtract(textSource, this.options.anki.sentenceExt); + const sentence = docSentenceExtract(textSource, this._options.anki.sentenceExt); this._showPopupContent( textSource, optionsContext, @@ -215,7 +226,7 @@ class Frontend extends TextScanner { } async findTerms(textSource, optionsContext) { - this.setTextSourceScanLength(textSource, this.options.scanning.length); + this._textScanner.setTextSourceScanLength(textSource, this._options.scanning.length); const searchText = textSource.text(); if (searchText.length === 0) { return null; } @@ -229,7 +240,7 @@ class Frontend extends TextScanner { } async findKanji(textSource, optionsContext) { - this.setTextSourceScanLength(textSource, 1); + this._textScanner.setTextSourceScanLength(textSource, 1); const searchText = textSource.text(); if (searchText.length === 0) { return null; } @@ -263,8 +274,21 @@ class Frontend extends TextScanner { return this._lastShowPromise; } + _updateTextScannerEnabled() { + const enabled = ( + this._options.general.enable && + this.popup.depth <= this._options.scanning.popupNestingMaxDepth && + !this._disabledOverride + ); + this._enabledEventListeners.removeAllEventListeners(); + this._textScanner.setEnabled(enabled); + if (enabled) { + this._enabledEventListeners.addEventListener(window, 'message', this.onWindowMessage.bind(this)); + } + } + _updateContentScale() { - const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this.options.general; + const {popupScalingFactor, popupScaleRelativeToPageZoom, popupScaleRelativeToVisualViewport} = this._options.general; let contentScale = popupScalingFactor; if (popupScaleRelativeToPageZoom) { contentScale /= this._pageZoomFactor; @@ -295,12 +319,8 @@ class Frontend extends TextScanner { }); } - _canEnable() { - return this.popup.depth <= this.options.scanning.popupNestingMaxDepth && !this._disabledOverride; - } - async _updatePopupPosition() { - const textSource = this.getCurrentTextSource(); + const textSource = this._textScanner.getCurrentTextSource(); if (textSource !== null && await this.popup.isVisible()) { this._showPopupContent(textSource, await this.getOptionsContext()); } diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js index c582ccd8..774eef44 100644 --- a/ext/mixed/js/text-scanner.js +++ b/ext/mixed/js/text-scanner.js @@ -45,6 +45,16 @@ class TextScanner extends EventDispatcher { this.preventNextMouseDown = false; this.preventNextClick = false; this.preventScroll = false; + + this._canClearSelection = true; + } + + get canClearSelection() { + return this._canClearSelection; + } + + set canClearSelection(value) { + this._canClearSelection = value; } onMouseOver(e) { @@ -222,9 +232,9 @@ class TextScanner extends EventDispatcher { } } - setEnabled(enabled, canEnable) { + setEnabled(enabled) { this.eventListeners.removeAllEventListeners(); - this.enabled = enabled && canEnable; + this.enabled = enabled; if (this.enabled) { this.hookEvents(); } else { @@ -233,9 +243,9 @@ class TextScanner extends EventDispatcher { } hookEvents() { - let eventListenerInfos = this.getMouseEventListeners(); + const eventListenerInfos = this.getMouseEventListeners(); if (this.options.scanning.touchInputEnabled) { - eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners()); + eventListenerInfos.push(...this.getTouchEventListeners()); } for (const [node, type, listener, options] of eventListenerInfos) { @@ -264,9 +274,8 @@ class TextScanner extends EventDispatcher { ]; } - setOptions(options, canEnable=true) { + setOptions(options) { this.options = options; - this.setEnabled(this.options.general.enable, canEnable); } async searchAt(x, y, cause) { @@ -324,6 +333,7 @@ class TextScanner extends EventDispatcher { } clearSelection(passive) { + if (!this._canClearSelection) { return; } if (this.textSourceCurrent !== null) { if (this.textSourceCurrentSelected) { this.textSourceCurrent.deselect(); -- cgit v1.2.3 From 6c341a13d813fc63b76fbbffe7920eeaf116d3a8 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 12:50:44 -0400 Subject: Refactor frontend API classes (#482) * Mark functions as private * Remove constructor side effects * Use safer handler invocation * Mark functions as private * Mark fields as private * Update BackendApiForwarder public API --- ext/bg/js/backend-api-forwarder.js | 6 +-- ext/bg/js/backend.js | 3 +- ext/fg/js/frontend-api-receiver.js | 32 +++++++++------- ext/fg/js/frontend-api-sender.js | 77 +++++++++++++++++++------------------- ext/fg/js/popup-proxy-host.js | 4 +- 5 files changed, 63 insertions(+), 59 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js index 93db77d7..4ac12730 100644 --- a/ext/bg/js/backend-api-forwarder.js +++ b/ext/bg/js/backend-api-forwarder.js @@ -17,11 +17,11 @@ class BackendApiForwarder { - constructor() { - chrome.runtime.onConnect.addListener(this.onConnect.bind(this)); + prepare() { + chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); } - onConnect(port) { + _onConnect(port) { if (port.name !== 'backend-api-forwarder') { return; } let tabId; diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 8a8f00eb..2fce4be9 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -70,7 +70,8 @@ class Backend { this.popupWindow = null; - this.apiForwarder = new BackendApiForwarder(); + const apiForwarder = new BackendApiForwarder(); + apiForwarder.prepare(); this.messageToken = yomichan.generateId(16); diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js index 4abd4e81..c5bb58af 100644 --- a/ext/fg/js/frontend-api-receiver.js +++ b/ext/fg/js/frontend-api-receiver.js @@ -20,38 +20,42 @@ class FrontendApiReceiver { constructor(source='', handlers=new Map()) { this._source = source; this._handlers = handlers; + } - chrome.runtime.onConnect.addListener(this.onConnect.bind(this)); + prepare() { + chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); } - onConnect(port) { + _onConnect(port) { if (port.name !== 'frontend-api-receiver') { return; } - port.onMessage.addListener(this.onMessage.bind(this, port)); + port.onMessage.addListener(this._onMessage.bind(this, port)); } - onMessage(port, {id, action, params, target, senderId}) { + _onMessage(port, {id, action, params, target, senderId}) { if (target !== this._source) { return; } const handler = this._handlers.get(action); if (typeof handler !== 'function') { return; } - this.sendAck(port, id, senderId); + this._sendAck(port, id, senderId); + this._invokeHandler(handler, params, port, id, senderId); + } - handler(params).then( - (result) => { - this.sendResult(port, id, senderId, {result}); - }, - (error) => { - this.sendResult(port, id, senderId, {error: errorToJson(error)}); - }); + async _invokeHandler(handler, params, port, id, senderId) { + try { + const result = await handler(params); + this._sendResult(port, id, senderId, {result}); + } catch (error) { + this._sendResult(port, id, senderId, {error: errorToJson(error)}); + } } - sendAck(port, id, senderId) { + _sendAck(port, id, senderId) { port.postMessage({type: 'ack', id, senderId}); } - sendResult(port, id, senderId, data) { + _sendResult(port, id, senderId, data) { port.postMessage({type: 'result', id, senderId, data}); } } diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js index 0ad3f085..d5084c29 100644 --- a/ext/fg/js/frontend-api-sender.js +++ b/ext/fg/js/frontend-api-sender.js @@ -18,68 +18,67 @@ class FrontendApiSender { constructor() { - this.senderId = yomichan.generateId(16); - this.ackTimeout = 3000; // 3 seconds - this.responseTimeout = 10000; // 10 seconds - this.callbacks = new Map(); - this.disconnected = false; - this.nextId = 0; - - this.port = null; + this._senderId = yomichan.generateId(16); + this._ackTimeout = 3000; // 3 seconds + this._responseTimeout = 10000; // 10 seconds + this._callbacks = new Map(); + this._disconnected = false; + this._nextId = 0; + this._port = null; } invoke(action, params, target) { - if (this.disconnected) { + if (this._disconnected) { // attempt to reconnect the next time - this.disconnected = false; + this._disconnected = false; return Promise.reject(new Error('Disconnected')); } - if (this.port === null) { - this.createPort(); + if (this._port === null) { + this._createPort(); } - const id = `${this.nextId}`; - ++this.nextId; + const id = `${this._nextId}`; + ++this._nextId; return new Promise((resolve, reject) => { const info = {id, resolve, reject, ack: false, timer: null}; - this.callbacks.set(id, info); - info.timer = setTimeout(() => this.onError(id, 'Timeout (ack)'), this.ackTimeout); + this._callbacks.set(id, info); + info.timer = setTimeout(() => this._onError(id, 'Timeout (ack)'), this._ackTimeout); - this.port.postMessage({id, action, params, target, senderId: this.senderId}); + this._port.postMessage({id, action, params, target, senderId: this._senderId}); }); } - createPort() { - this.port = chrome.runtime.connect(null, {name: 'backend-api-forwarder'}); - this.port.onDisconnect.addListener(this.onDisconnect.bind(this)); - this.port.onMessage.addListener(this.onMessage.bind(this)); + _createPort() { + this._port = chrome.runtime.connect(null, {name: 'backend-api-forwarder'}); + this._port.onDisconnect.addListener(this._onDisconnect.bind(this)); + this._port.onMessage.addListener(this._onMessage.bind(this)); } - onMessage({type, id, data, senderId}) { - if (senderId !== this.senderId) { return; } + _onMessage({type, id, data, senderId}) { + if (senderId !== this._senderId) { return; } switch (type) { case 'ack': - this.onAck(id); + this._onAck(id); break; case 'result': - this.onResult(id, data); + this._onResult(id, data); break; } } - onDisconnect() { - this.disconnected = true; - this.port = null; + _onDisconnect() { + this._disconnected = true; + this._port = null; - for (const id of this.callbacks.keys()) { - this.onError(id, 'Disconnected'); + for (const id of this._callbacks.keys()) { + this._onError(id, 'Disconnected'); } } - onAck(id) { - const info = this.callbacks.get(id); + _onAck(id) { + const info = this._callbacks.get(id); if (typeof info === 'undefined') { yomichan.logWarning(new Error(`ID ${id} not found for ack`)); return; @@ -92,11 +91,11 @@ class FrontendApiSender { info.ack = true; clearTimeout(info.timer); - info.timer = setTimeout(() => this.onError(id, 'Timeout (response)'), this.responseTimeout); + info.timer = setTimeout(() => this._onError(id, 'Timeout (response)'), this._responseTimeout); } - onResult(id, data) { - const info = this.callbacks.get(id); + _onResult(id, data) { + const info = this._callbacks.get(id); if (typeof info === 'undefined') { yomichan.logWarning(new Error(`ID ${id} not found`)); return; @@ -107,7 +106,7 @@ class FrontendApiSender { return; } - this.callbacks.delete(id); + this._callbacks.delete(id); clearTimeout(info.timer); info.timer = null; @@ -118,10 +117,10 @@ class FrontendApiSender { } } - onError(id, reason) { - const info = this.callbacks.get(id); + _onError(id, reason) { + const info = this._callbacks.get(id); if (typeof info === 'undefined') { return; } - this.callbacks.delete(id); + this._callbacks.delete(id); info.timer = null; info.reject(new Error(reason)); } diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index d87553e6..39150bc7 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -24,7 +24,6 @@ class PopupProxyHost { constructor() { this._popups = new Map(); - this._apiReceiver = null; this._frameId = null; } @@ -35,7 +34,7 @@ class PopupProxyHost { if (typeof frameId !== 'number') { return; } this._frameId = frameId; - this._apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${this._frameId}`, new Map([ + const apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${this._frameId}`, new Map([ ['getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)], ['setOptionsContext', this._onApiSetOptionsContext.bind(this)], ['hide', this._onApiHide.bind(this)], @@ -48,6 +47,7 @@ class PopupProxyHost { ['setContentScale', this._onApiSetContentScale.bind(this)], ['getHostUrl', this._onApiGetHostUrl.bind(this)] ])); + apiReceiver.prepare(); } getOrCreatePopup(id=null, parentId=null, depth=null) { -- cgit v1.2.3 From 8a368aaddc62024b04419970736bb07dabe796bc Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 12:58:24 -0400 Subject: Don't parent the popup frame to elements which cause unload (#488) --- ext/fg/js/popup.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index e735431b..00658f58 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -281,7 +281,7 @@ class Popup { } _onFullscreenChanged() { - const parent = (DOM.getFullscreenElement() || document.body || null); + const parent = this._getFrameParentElement(); if (parent !== null && this._container.parentNode !== parent) { parent.appendChild(this._container); } @@ -375,6 +375,22 @@ class Popup { contentWindow.postMessage({action, params, token}, this._targetOrigin); } + _getFrameParentElement() { + const defaultParent = document.body; + const fullscreenElement = DOM.getFullscreenElement(); + if (fullscreenElement === null || fullscreenElement.shadowRoot) { + return defaultParent; + } + + switch (fullscreenElement.nodeName.toUpperCase()) { + case 'IFRAME': + case 'FRAME': + return defaultParent; + } + + return fullscreenElement; + } + static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; -- cgit v1.2.3 From efa7a5ecc3bf6def28c12478fbbcd5fb56f1f63c Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 12:59:13 -0400 Subject: Remove and unload the popup frame if an unexpected load occurs (#490) * Remove and unload the popup frame if an unexpected load occurs * Remove unused fields * Only update _injectPromiseComplete if the promise is the most recent one * Remove redundant this._injectPromise !== null check --- ext/fg/js/popup.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 00658f58..2b33b714 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -31,6 +31,7 @@ class Popup { this._child = null; this._childrenSupported = true; this._injectPromise = null; + this._injectPromiseComplete = false; this._visible = false; this._visibleOverride = null; this._options = null; @@ -47,6 +48,7 @@ class Popup { this._container.addEventListener('scroll', (e) => e.stopPropagation()); this._container.style.width = '0px'; this._container.style.height = '0px'; + this._container.addEventListener('load', this._onFrameLoad.bind(this)); this._fullscreenEventListeners = new EventListenerCollection(); @@ -199,10 +201,19 @@ class Popup { // Private functions _inject() { - if (this._injectPromise === null) { - this._injectPromise = this._createInjectPromise(); + let injectPromise = this._injectPromise; + if (injectPromise === null) { + injectPromise = this._createInjectPromise(); + this._injectPromise = injectPromise; + injectPromise.then( + () => { + if (injectPromise !== this._injectPromise) { return; } + this._injectPromiseComplete = true; + }, + () => { this._resetFrame(); } + ); } - return this._injectPromise; + return injectPromise; } async _createInjectPromise() { @@ -243,6 +254,23 @@ class Popup { return popupPreparedPromise; } + _onFrameLoad() { + if (!this._injectPromiseComplete) { return; } + this._resetFrame(); + } + + _resetFrame() { + const parent = this._container.parentNode; + if (parent !== null) { + parent.removeChild(this._container); + } + this._container.removeAttribute('src'); + this._container.removeAttribute('srcdoc'); + + this._injectPromise = null; + this._injectPromiseComplete = false; + } + async _injectStyles() { try { await Popup._injectStylesheet('yomichan-popup-outer-stylesheet', 'file', '/fg/css/client.css', true); -- cgit v1.2.3 From 51032d1eca04820a80f34dfd511a927c55975c1f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 12:59:59 -0400 Subject: Catch WrongDocumentError thrown by compareBoundaryPoints (#491) * Catch WrongDocumentError thrown by compareBoundaryPoints * Filter error based on name --- ext/fg/js/source.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index 3d9afe0f..0b21b5cc 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -94,7 +94,15 @@ class TextSourceRange { this.rangeStartOffset === other.rangeStartOffset ); } else { - return this.range.compareBoundaryPoints(Range.START_TO_START, other.range) === 0; + try { + return this.range.compareBoundaryPoints(Range.START_TO_START, other.range) === 0; + } catch (e) { + if (e.name === 'WrongDocumentError') { + // This can happen with shadow DOMs if the ranges are in different documents. + return false; + } + throw e; + } } } -- cgit v1.2.3 From c4ea9321dcffbda9004461a7b0027cf5c893f3c0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 13:00:46 -0400 Subject: Validate document nodes before use (#493) * Validate document.body before use in loadScripts This also fixes an issue where reject wasn't being passed to loadScriptSentinel. * Validate document nodes before use in _getSiteColor * Validate document.body before use in _getViewport * Validate document.body before use in setContentScale * Validate document.body before use in docImposterCreate --- ext/fg/js/document.js | 5 ++++- ext/fg/js/float.js | 4 +++- ext/fg/js/popup.js | 12 +++++++++--- ext/mixed/js/dynamic-loader.js | 12 ++++++++---- 4 files changed, 24 insertions(+), 9 deletions(-) (limited to 'ext/fg/js') diff --git a/ext/fg/js/document.js b/ext/fg/js/document.js index 3b4cc28f..6103c7c5 100644 --- a/ext/fg/js/document.js +++ b/ext/fg/js/document.js @@ -28,6 +28,9 @@ function docSetImposterStyle(style, propertyName, value) { } function docImposterCreate(element, isTextarea) { + const body = document.body; + if (body === null) { return [null, null]; } + const elementStyle = window.getComputedStyle(element); const elementRect = element.getBoundingClientRect(); const documentRect = document.documentElement.getBoundingClientRect(); @@ -78,7 +81,7 @@ function docImposterCreate(element, isTextarea) { } container.appendChild(imposter); - document.body.appendChild(container); + body.appendChild(container); // Adjust size const imposterRect = imposter.getBoundingClientRect(); diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index 294093cd..77e8edd8 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -162,7 +162,9 @@ class DisplayFloat extends Display { } setContentScale(scale) { - document.body.style.fontSize = `${scale}em`; + const body = document.body; + if (body === null) { return; } + body.style.fontSize = `${scale}em`; } async getDocumentTitle() { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 2b33b714..f5cb6f77 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -389,8 +389,13 @@ class Popup { _getSiteColor() { const color = [255, 255, 255]; - Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.documentElement).backgroundColor)); - Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(document.body).backgroundColor)); + const {documentElement, body} = document; + if (documentElement !== null) { + Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(documentElement).backgroundColor)); + } + if (body !== null) { + Popup._addColor(color, Popup._getColorInfo(window.getComputedStyle(body).backgroundColor)); + } const dark = (color[0] < 128 && color[1] < 128 && color[2] < 128); return dark ? 'dark' : 'light'; } @@ -575,10 +580,11 @@ class Popup { } } + const body = document.body; return { left: 0, top: 0, - right: document.body.clientWidth, + right: (body !== null ? body.clientWidth : 0), bottom: window.innerHeight }; } diff --git a/ext/mixed/js/dynamic-loader.js b/ext/mixed/js/dynamic-loader.js index 29672d36..51b6821b 100644 --- a/ext/mixed/js/dynamic-loader.js +++ b/ext/mixed/js/dynamic-loader.js @@ -31,8 +31,13 @@ const dynamicLoader = (() => { } function loadScripts(urls) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const parent = document.body; + if (parent === null) { + reject(new Error('Missing body')); + return; + } + for (const url of urls) { const node = parent.querySelector(`script[src='${escapeCSSAttribute(url)}']`); if (node !== null) { continue; } @@ -43,12 +48,11 @@ const dynamicLoader = (() => { parent.appendChild(script); } - loadScriptSentinel(resolve); + loadScriptSentinel(parent, resolve, reject); }); } - function loadScriptSentinel(resolve, reject) { - const parent = document.body; + function loadScriptSentinel(parent, resolve, reject) { const script = document.createElement('script'); const sentinelEventName = 'dynamicLoaderSentinel'; -- cgit v1.2.3 From d581bffa15419b3b55773f1ed08a2e787e574f1f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 13:04:17 -0400 Subject: Style fixes (#494) * Place multi-line expression parentheses on the correct line * Add function-paren-newline eslint rule * Add some additional eslint rules --- .eslintrc.json | 9 +++++++++ ext/bg/js/backend.js | 4 +++- ext/fg/js/source.js | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) (limited to 'ext/fg/js') diff --git a/.eslintrc.json b/.eslintrc.json index 78fec27c..a2de6671 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,6 +23,7 @@ "eqeqeq": "error", "func-names": ["error", "always"], "guard-for-in": "error", + "new-parens": "error", "no-case-declarations": "error", "no-const-assign": "error", "no-constant-condition": "off", @@ -31,6 +32,7 @@ "no-prototype-builtins": "error", "no-shadow": ["error", {"builtinGlobals": false}], "no-undef": "error", + "no-unexpected-multiline": "error", "no-unneeded-ternary": "error", "no-unused-vars": ["error", {"vars": "local", "args": "after-used", "argsIgnorePattern": "^_", "caughtErrors": "none"}], "no-unused-expressions": "error", @@ -40,6 +42,7 @@ "quotes": ["error", "single", "avoid-escape"], "require-atomic-updates": "off", "semi": "error", + "wrap-iife": ["error", "inside"], // Whitespace rules "brace-style": ["error", "1tbs", {"allowSingleLine": true}], @@ -53,6 +56,7 @@ "comma-spacing": ["error", { "before": false, "after": true }], "computed-property-spacing": ["error", "never"], "func-call-spacing": ["error", "never"], + "function-paren-newline": ["error", "multiline-arguments"], "generator-star-spacing": ["error", "before"], "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}], "keyword-spacing": ["error", {"before": true, "after": true}], @@ -61,6 +65,11 @@ "object-curly-spacing": ["error", "never"], "rest-spread-spacing": ["error", "never"], "semi-spacing": ["error", {"before": false, "after": true}], + "space-before-function-paren": ["error", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + }], "space-in-parens": ["error", "never"], "space-unary-ops": "error", "spaced-comment": ["error", "always"], diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index ed01c8df..43fa8190 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -896,7 +896,9 @@ class Backend { await Backend._focusTab(tab); if (queryParams.query) { await new Promise((resolve) => chrome.tabs.sendMessage( - tab.id, {action: 'searchQueryUpdate', params: {text: queryParams.query}}, resolve + tab.id, + {action: 'searchQueryUpdate', params: {text: queryParams.query}}, + resolve )); } return true; diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index 0b21b5cc..b3119d40 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -118,7 +118,8 @@ class TextSourceRange { return !( style.visibility === 'hidden' || style.display === 'none' || - parseFloat(style.fontSize) === 0); + parseFloat(style.fontSize) === 0 + ); } static getRubyElement(node) { -- cgit v1.2.3 From d4ae9aa501ece99ea6c5e6b8fb01c3005f5b7f03 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 13:05:43 -0400 Subject: DOMTextScanner (#458) * Create new class for scanning text in a document * Update test styles * Add tests --- ext/fg/js/dom-text-scanner.js | 538 ++++++++++++++++++++++++++++++ test/data/html/test-dom-text-scanner.html | 393 ++++++++++++++++++++++ test/data/html/test-stylesheet.css | 12 +- test/test-dom-text-scanner.js | 181 ++++++++++ 4 files changed, 1121 insertions(+), 3 deletions(-) create mode 100644 ext/fg/js/dom-text-scanner.js create mode 100644 test/data/html/test-dom-text-scanner.html create mode 100644 test/test-dom-text-scanner.js (limited to 'ext/fg/js') diff --git a/ext/fg/js/dom-text-scanner.js b/ext/fg/js/dom-text-scanner.js new file mode 100644 index 00000000..2de65041 --- /dev/null +++ b/ext/fg/js/dom-text-scanner.js @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 . + */ + +/** + * A class used to scan text in a document. + */ +class DOMTextScanner { + /** + * Creates a new instance of a DOMTextScanner. + * @param node The DOM Node to start at. + * @param offset The character offset in to start at when node is a text node. + * Use 0 for non-text nodes. + */ + constructor(node, offset, forcePreserveWhitespace=false, generateLayoutContent=true) { + const ruby = DOMTextScanner.getParentRubyElement(node); + const resetOffset = (ruby !== null); + if (resetOffset) { node = ruby; } + + this._node = node; + this._offset = offset; + this._content = ''; + this._remainder = 0; + this._resetOffset = resetOffset; + this._newlines = 0; + this._lineHasWhitespace = false; + this._lineHasContent = false; + this._forcePreserveWhitespace = forcePreserveWhitespace; + this._generateLayoutContent = generateLayoutContent; + } + + /** + * Gets the current node being scanned. + * @returns A DOM Node. + */ + get node() { + return this._node; + } + + /** + * Gets the current offset corresponding to the node being scanned. + * This value is only applicable for text nodes. + * @returns An integer. + */ + get offset() { + return this._offset; + } + + /** + * Gets the accumulated content string resulting from calls to seek(). + * @returns A string. + */ + get content() { + return this._content; + } + + /** + * Seeks a given length in the document and accumulates the text content. + * @param length A positive or negative integer corresponding to how many characters + * should be added to content. Content is only added to the accumulation string, + * never removed, so mixing seek calls with differently signed length values + * may give unexpected results. + * @returns this + */ + seek(length) { + const forward = (length >= 0); + this._remainder = (forward ? length : -length); + if (length === 0) { return this; } + + const TEXT_NODE = Node.TEXT_NODE; + const ELEMENT_NODE = Node.ELEMENT_NODE; + + const generateLayoutContent = this._generateLayoutContent; + let node = this._node; + let resetOffset = this._resetOffset; + let newlines = 0; + while (node !== null) { + let enterable = false; + const nodeType = node.nodeType; + + if (nodeType === TEXT_NODE) { + if (!( + forward ? + this._seekTextNodeForward(node, resetOffset) : + this._seekTextNodeBackward(node, resetOffset) + )) { + // Length reached + break; + } + } else if (nodeType === ELEMENT_NODE) { + [enterable, newlines] = DOMTextScanner.getElementSeekInfo(node); + if (newlines > this._newlines && generateLayoutContent) { + this._newlines = newlines; + } + } + + const exitedNodes = []; + node = DOMTextScanner.getNextNode(node, forward, enterable, exitedNodes); + + for (const exitedNode of exitedNodes) { + if (exitedNode.nodeType !== ELEMENT_NODE) { continue; } + newlines = DOMTextScanner.getElementSeekInfo(exitedNode)[1]; + if (newlines > this._newlines && generateLayoutContent) { + this._newlines = newlines; + } + } + + resetOffset = true; + } + + this._node = node; + this._resetOffset = resetOffset; + + return this; + } + + // Private + + /** + * Seeks forward in a text node. + * @param textNode The text node to use. + * @param resetOffset Whether or not the text offset should be reset. + * @returns true if scanning should continue, or false if the scan length has been reached. + */ + _seekTextNodeForward(textNode, resetOffset) { + const nodeValue = textNode.nodeValue; + const nodeValueLength = nodeValue.length; + const [preserveNewlines, preserveWhitespace] = ( + this._forcePreserveWhitespace ? + [true, true] : + DOMTextScanner.getWhitespaceSettings(textNode) + ); + + let lineHasWhitespace = this._lineHasWhitespace; + let lineHasContent = this._lineHasContent; + let content = this._content; + let offset = resetOffset ? 0 : this._offset; + let remainder = this._remainder; + let newlines = this._newlines; + + while (offset < nodeValueLength) { + const char = nodeValue[offset]; + const charAttributes = DOMTextScanner.getCharacterAttributes(char, preserveNewlines, preserveWhitespace); + ++offset; + + if (charAttributes === 0) { + // Character should be ignored + continue; + } else if (charAttributes === 1) { + // Character is collapsable whitespace + lineHasWhitespace = true; + } else { + // Character should be added to the content + if (newlines > 0) { + if (content.length > 0) { + const useNewlineCount = Math.min(remainder, newlines); + content += '\n'.repeat(useNewlineCount); + remainder -= useNewlineCount; + newlines -= useNewlineCount; + } else { + newlines = 0; + } + lineHasContent = false; + lineHasWhitespace = false; + if (remainder <= 0) { + --offset; // Revert character offset + break; + } + } + + lineHasContent = (charAttributes === 2); // 3 = character is a newline + + if (lineHasWhitespace) { + if (lineHasContent) { + content += ' '; + lineHasWhitespace = false; + if (--remainder <= 0) { + --offset; // Revert character offset + break; + } + } else { + lineHasWhitespace = false; + } + } + + content += char; + + if (--remainder <= 0) { break; } + } + } + + this._lineHasWhitespace = lineHasWhitespace; + this._lineHasContent = lineHasContent; + this._content = content; + this._offset = offset; + this._remainder = remainder; + this._newlines = newlines; + + return (remainder > 0); + } + + /** + * Seeks backward in a text node. + * This function is nearly the same as _seekTextNodeForward, with the following differences: + * - Iteration condition is reversed to check if offset is greater than 0. + * - offset is reset to nodeValueLength instead of 0. + * - offset is decremented instead of incremented. + * - offset is decremented before getting the character. + * - offset is reverted by incrementing instead of decrementing. + * - content string is prepended instead of appended. + * @param textNode The text node to use. + * @param resetOffset Whether or not the text offset should be reset. + * @returns true if scanning should continue, or false if the scan length has been reached. + */ + _seekTextNodeBackward(textNode, resetOffset) { + const nodeValue = textNode.nodeValue; + const nodeValueLength = nodeValue.length; + const [preserveNewlines, preserveWhitespace] = ( + this._forcePreserveWhitespace ? + [true, true] : + DOMTextScanner.getWhitespaceSettings(textNode) + ); + + let lineHasWhitespace = this._lineHasWhitespace; + let lineHasContent = this._lineHasContent; + let content = this._content; + let offset = resetOffset ? nodeValueLength : this._offset; + let remainder = this._remainder; + let newlines = this._newlines; + + while (offset > 0) { + --offset; + const char = nodeValue[offset]; + const charAttributes = DOMTextScanner.getCharacterAttributes(char, preserveNewlines, preserveWhitespace); + + if (charAttributes === 0) { + // Character should be ignored + continue; + } else if (charAttributes === 1) { + // Character is collapsable whitespace + lineHasWhitespace = true; + } else { + // Character should be added to the content + if (newlines > 0) { + if (content.length > 0) { + const useNewlineCount = Math.min(remainder, newlines); + content = '\n'.repeat(useNewlineCount) + content; + remainder -= useNewlineCount; + newlines -= useNewlineCount; + } else { + newlines = 0; + } + lineHasContent = false; + lineHasWhitespace = false; + if (remainder <= 0) { + ++offset; // Revert character offset + break; + } + } + + lineHasContent = (charAttributes === 2); // 3 = character is a newline + + if (lineHasWhitespace) { + if (lineHasContent) { + content = ' ' + content; + lineHasWhitespace = false; + if (--remainder <= 0) { + ++offset; // Revert character offset + break; + } + } else { + lineHasWhitespace = false; + } + } + + content = char + content; + + if (--remainder <= 0) { break; } + } + } + + this._lineHasWhitespace = lineHasWhitespace; + this._lineHasContent = lineHasContent; + this._content = content; + this._offset = offset; + this._remainder = remainder; + this._newlines = newlines; + + return (remainder > 0); + } + + // Static helpers + + /** + * Gets the next node in the document for a specified scanning direction. + * @param node The current DOM Node. + * @param forward Whether to scan forward in the document or backward. + * @param visitChildren Whether the children of the current node should be visited. + * @param exitedNodes An array which stores nodes which were exited. + * @returns The next node in the document, or null if there is no next node. + */ + static getNextNode(node, forward, visitChildren, exitedNodes) { + let next = visitChildren ? (forward ? node.firstChild : node.lastChild) : null; + if (next === null) { + while (true) { + exitedNodes.push(node); + + next = (forward ? node.nextSibling : node.previousSibling); + if (next !== null) { break; } + + next = node.parentNode; + if (next === null) { break; } + + node = next; + } + } + return next; + } + + /** + * Gets the parent element of a given Node. + * @param node The node to check. + * @returns The parent element if one exists, otherwise null. + */ + static getParentElement(node) { + while (node !== null && node.nodeType !== Node.ELEMENT_NODE) { + node = node.parentNode; + } + return node; + } + + /** + * Gets the parent element of a given node, if one exists. For efficiency purposes, + * this only checks the immediate parent elements and does not check all ancestors, so + * there are cases where the node may be in a ruby element but it is not returned. + * @param node The node to check. + * @returns A node if the input node is contained in one, otherwise null. + */ + static getParentRubyElement(node) { + node = DOMTextScanner.getParentElement(node); + if (node !== null && node.nodeName.toUpperCase() === 'RT') { + node = node.parentNode; + if (node !== null && node.nodeName.toUpperCase() === 'RUBY') { + return node; + } + } + return null; + } + + /** + * @returns [enterable: boolean, newlines: integer] + * The enterable value indicates whether the content of this node should be entered. + * The newlines value corresponds to the number of newline characters that should be added. + * 1 newline corresponds to a simple new line in the layout. + * 2 newlines corresponds to a significant visual distinction since the previous content. + */ + static getElementSeekInfo(element) { + let enterable = true; + switch (element.nodeName.toUpperCase()) { + case 'HEAD': + case 'RT': + case 'SCRIPT': + case 'STYLE': + return [false, 0]; + case 'BR': + return [false, 1]; + case 'TEXTAREA': + case 'INPUT': + case 'BUTTON': + enterable = false; + break; + } + + const style = window.getComputedStyle(element); + const display = style.display; + + const visible = (display !== 'none' && DOMTextScanner.isStyleVisible(style)); + let newlines = 0; + + if (!visible) { + enterable = false; + } else { + switch (style.position) { + case 'absolute': + case 'fixed': + case 'sticky': + newlines = 2; + break; + } + if (newlines === 0 && DOMTextScanner.doesCSSDisplayChangeLayout(display)) { + newlines = 1; + } + } + + return [enterable, newlines]; + } + + /** + * Gets information about how whitespace characters are treated. + * @param textNode The Text node to check. + * @returns [preserveNewlines: boolean, preserveWhitespace: boolean] + * The value of preserveNewlines indicates whether or not newline characters are treated as line breaks. + * The value of preserveWhitespace indicates whether or not sequences of whitespace characters are collapsed. + */ + static getWhitespaceSettings(textNode) { + const element = DOMTextScanner.getParentElement(textNode); + if (element !== null) { + const style = window.getComputedStyle(element); + switch (style.whiteSpace) { + case 'pre': + case 'pre-wrap': + case 'break-spaces': + return [true, true]; + case 'pre-line': + return [true, false]; + } + } + return [false, false]; + } + + /** + * Gets attributes for the specified character. + * @param character A string containing a single character. + * @returns An integer representing the attributes of the character. + * 0: Character should be ignored. + * 1: Character is collapsable whitespace. + * 2: Character should be added to the content. + * 3: Character should be added to the content and is a newline. + */ + static getCharacterAttributes(character, preserveNewlines, preserveWhitespace) { + switch (character.charCodeAt(0)) { + case 0x09: // Tab ('\t') + case 0x0c: // Form feed ('\f') + case 0x0d: // Carriage return ('\r') + case 0x20: // Space (' ') + return preserveWhitespace ? 2 : 1; + case 0x0a: // Line feed ('\n') + return preserveNewlines ? 3 : 1; + case 0x200c: // Zero-width non-joiner ('\u200c') + return 0; + default: // Other + return 2; + } + } + + /** + * Checks whether a given style is visible or not. + * This function does not check style.display === 'none'. + * @param style An object implementing the CSSStyleDeclaration interface. + * @returns true if the style should result in an element being visible, otherwise false. + */ + static isStyleVisible(style) { + return !( + style.visibility === 'hidden' || + parseFloat(style.opacity) <= 0 || + parseFloat(style.fontSize) <= 0 || + ( + !DOMTextScanner.isStyleSelectable(style) && + ( + DOMTextScanner.isCSSColorTransparent(style.color) || + DOMTextScanner.isCSSColorTransparent(style.webkitTextFillColor) + ) + ) + ); + } + + /** + * Checks whether a given style is selectable or not. + * @param style An object implementing the CSSStyleDeclaration interface. + * @returns true if the style is selectable, otherwise false. + */ + static isStyleSelectable(style) { + return !( + style.userSelect === 'none' || + style.webkitUserSelect === 'none' || + style.MozUserSelect === 'none' || + style.msUserSelect === 'none' + ); + } + + /** + * Checks whether a CSS color is transparent or not. + * @param cssColor A CSS color string, expected to be encoded in rgb(a) form. + * @returns true if the color is transparent, otherwise false. + */ + static isCSSColorTransparent(cssColor) { + return ( + typeof cssColor === 'string' && + cssColor.startsWith('rgba(') && + /,\s*0.?0*\)$/.test(cssColor) + ); + } + + /** + * Checks whether a CSS display value will cause a layout change for text. + * @param cssDisplay A CSS string corresponding to the value of the display property. + * @returns true if the layout is changed by this value, otherwise false. + */ + static doesCSSDisplayChangeLayout(cssDisplay) { + let pos = cssDisplay.indexOf(' '); + if (pos >= 0) { + // Truncate to part + cssDisplay = cssDisplay.substring(0, pos); + } + + pos = cssDisplay.indexOf('-'); + if (pos >= 0) { + // Truncate to first part of kebab-case value + cssDisplay = cssDisplay.substring(0, pos); + } + + switch (cssDisplay) { + case 'block': + case 'flex': + case 'grid': + case 'list': // list-item + case 'table': // table, table-* + return true; + case 'ruby': // rubt-* + return (pos >= 0); + default: + return false; + } + } +} diff --git a/test/data/html/test-dom-text-scanner.html b/test/data/html/test-dom-text-scanner.html new file mode 100644 index 00000000..6b78570a --- /dev/null +++ b/test/data/html/test-dom-text-scanner.html @@ -0,0 +1,393 @@ + + + + + + Yomichan DOMTextScanner Tests + + + + + +

Yomichan DOMTextScanner Tests

+ + + Layout newlines expected due to entering and exiting display:block nodes. +
小ぢん
まり1
+
小ぢん
まり2
+
+ + + Layout newline expected due to sequential display:block elements. +
小ぢんまり1
小ぢんまり2
+
+ + + Layout newline expected due to sequential display:block elements separated by a newline. +
小ぢんまり1
+
小ぢんまり2
+
+ + + No newlines expected due to display:inline. +小ぢんまり1小ぢんまり2 + + + + No newlines expected due to white-space:normal. +小ぢんまり1 +小ぢんまり2 + + + + Newline expected due to white-space:pre. +
+小ぢんまり1
+小ぢんまり2
+
+
+ + + No newlines expected due to display:inline-block. Actual layout flow cannot be determined by DOM/CSS alone. +小ぢんまり1小ぢんまり2 + + + + Single newline expected due to display:block layout. +
小ぢんまり1
小ぢんまり2
+
+ + + Two newlines expected due to position:absolute causing a significant layout change. +
小ぢんまり1
小ぢんまり2
+
+ + + Two newlines expected due to position:fixed causing a significant layout change. +
小ぢんまり1
小ぢんまり2
+
+ + + Two newlines expected due to position:sticky being able to cause a significant layout change. +
小ぢんまり1
小ぢんまり2
+
+ + + Scanning text starting in an <rt> element. Should start scanning at the start of the <ruby> tag instead. +
()ぢんまり1
+
+ + + Skip <script> content. +
小ぢんまり1
+
+ + + Skip <style> content. +
小ぢんまり1
+
+ + + Skip <textarea> content. +
小ぢんまり1
+
+ + + Skip <input> content. +
小ぢんまり1
+
+ + + Skip <button> content. +
小ぢんまり1
+
+ + + Skip content with font-size:0. +
小ぢんcontentまり1
+
+ + + Skip content with opacity:0. +
小ぢんcontentまり1
+
+ + + Skip content with visibility:hidden. +
小ぢんcontentまり1
+
+ + + Skip content with display:none. +
小ぢんcontentまり1
+
+ + + Don't skip content with user-select:none. +
小ぢまり1
+
+ + + Skip content with user-select:none and a transparent color. +
小ぢんcontentまり1
+
+ + + \ No newline at end of file diff --git a/test/data/html/test-stylesheet.css b/test/data/html/test-stylesheet.css index f63d2481..2e9a2f52 100644 --- a/test/data/html/test-stylesheet.css +++ b/test/data/html/test-stylesheet.css @@ -28,7 +28,9 @@ a, a:visited { text-decoration: underline; } -.test { +.test, +y-test { + display: block; background-color: #ffffff; margin: 1em 0; padding: 0.5em; @@ -36,7 +38,8 @@ a, a:visited { border-radius: 4px; } -.test:before { +.test:before, +y-test:before { content: "Test " counter(test-id); display: block; counter-increment: test-id; @@ -45,7 +48,10 @@ a, a:visited { font-weight: bold; } -.description { +.description, +y-description { color: #444444; font-style: italic; + display: block; + padding-bottom: 0.5em; } diff --git a/test/test-dom-text-scanner.js b/test/test-dom-text-scanner.js new file mode 100644 index 00000000..41d6e307 --- /dev/null +++ b/test/test-dom-text-scanner.js @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * 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 . + */ + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const {JSDOM} = require('jsdom'); +const {VM} = require('./yomichan-vm'); + + +function createJSDOM(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + return new JSDOM(domSource); +} + +function querySelectorTextNode(element, selector) { + let textIndex = -1; + const match = /::text$|::nth-text\((\d+)\)$/.exec(selector); + if (match !== null) { + textIndex = (match[1] ? parseInt(match[1], 10) - 1 : 0); + selector = selector.substring(0, selector.length - match[0].length); + } + const result = element.querySelector(selector); + if (textIndex < 0) { + return result; + } + for (let n = result.firstChild; n !== null; n = n.nextSibling) { + if (n.nodeType === n.constructor.TEXT_NODE) { + if (textIndex === 0) { + return n; + } + --textIndex; + } + } + return null; +} + + +function getComputedFontSizeInPixels(window, getComputedStyle, element) { + for (; element !== null; element = element.parentNode) { + if (element.nodeType === window.Node.ELEMENT_NODE) { + const fontSize = getComputedStyle(element).fontSize; + if (fontSize.endsWith('px')) { + const value = parseFloat(fontSize.substring(0, fontSize.length - 2)); + return value; + } + } + } + const defaultFontSize = 14; + return defaultFontSize; +} + +function createAbsoluteGetComputedStyle(window) { + // Wrapper to convert em units to px units + const getComputedStyleOld = window.getComputedStyle.bind(window); + return (element, ...args) => { + const style = getComputedStyleOld(element, ...args); + return new Proxy(style, { + get: (target, property) => { + let result = target[property]; + if (typeof result === 'string') { + result = result.replace(/([-+]?\d(?:\.\d)?(?:[eE][-+]?\d+)?)em/g, (g0, g1) => { + const fontSize = getComputedFontSizeInPixels(window, getComputedStyleOld, element); + return `${parseFloat(g1) * fontSize}px`; + }); + } + return result; + } + }); + }; +} + + +async function testDomTextScanner(dom, {DOMTextScanner}) { + const document = dom.window.document; + for (const testElement of document.querySelectorAll('y-test')) { + let testData = JSON.parse(testElement.dataset.testData); + if (!Array.isArray(testData)) { + testData = [testData]; + } + for (const testDataItem of testData) { + let { + node, + offset, + length, + forcePreserveWhitespace, + generateLayoutContent, + reversible, + expected: { + node: expectedNode, + offset: expectedOffset, + content: expectedContent + } + } = testDataItem; + + node = querySelectorTextNode(testElement, node); + expectedNode = querySelectorTextNode(testElement, expectedNode); + + // Standard test + { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length); + + const {node: actualNode1, offset: actualOffset1, content: actualContent1} = scanner; + assert.strictEqual(actualContent1, expectedContent); + assert.strictEqual(actualOffset1, expectedOffset); + assert.strictEqual(actualNode1, expectedNode); + } + + // Substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length - i); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent.substring(0, expectedContent.length - i)); + } + + if (reversible === false) { continue; } + + // Reversed test + { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-length); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent); + } + + // Reversed substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-(length - i)); + + const {content: actualContent} = scanner; + assert.strictEqual(actualContent, expectedContent.substring(i)); + } + } + } +} + + +async function testDocument1() { + const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-dom-text-scanner.html')); + const window = dom.window; + try { + const {document, Node, Range} = window; + + window.getComputedStyle = createAbsoluteGetComputedStyle(window); + + const vm = new VM({document, window, Range, Node}); + vm.execute('fg/js/dom-text-scanner.js'); + const DOMTextScanner = vm.get('DOMTextScanner'); + + await testDomTextScanner(dom, {DOMTextScanner}); + } finally { + window.close(); + } +} + + +async function main() { + await testDocument1(); +} + + +if (require.main === module) { main(); } -- cgit v1.2.3 From 77b744e675f8abf17ff5e8433f4f1717e0c9ffb5 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 3 May 2020 04:39:24 +0300 Subject: Modifier key profile condition (#487) * update Frontend options on modifier change * add modifier key profile condition * use select element for modifier condition value * support "is" and "is not" modifier key conditions * use plural * remove dead null check it's never null in that function * pass element on rather than assigning to this * rename event * remove Firefox OS key to Meta detection * hide Meta from dropdown on Firefox * move input type --- .eslintrc.json | 1 + ext/bg/js/profile-conditions.js | 66 +++++++++++++++++ ext/bg/js/search.js | 3 +- ext/bg/js/settings/conditions-ui.js | 139 ++++++++++++++++++++++++++++++++---- ext/bg/settings.html | 5 +- ext/fg/js/frontend.js | 27 ++++++- ext/mixed/js/core.js | 6 ++ ext/mixed/js/display.js | 7 +- ext/mixed/js/dom.js | 14 ++++ ext/mixed/js/text-scanner.js | 3 + 10 files changed, 248 insertions(+), 23 deletions(-) (limited to 'ext/fg/js') diff --git a/.eslintrc.json b/.eslintrc.json index a2de6671..3186a491 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -97,6 +97,7 @@ "parseUrl": "readonly", "areSetsEqual": "readonly", "getSetIntersection": "readonly", + "getSetDifference": "readonly", "EventDispatcher": "readonly", "EventListenerCollection": "readonly", "EXTENSION_IS_BROWSER_EDGE": "readonly" diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js index a0710bd1..c0f5d3f5 100644 --- a/ext/bg/js/profile-conditions.js +++ b/ext/bg/js/profile-conditions.js @@ -36,6 +36,24 @@ function _profileConditionTestDomainList(url, domainList) { return false; } +const _profileModifierKeys = [ + {optionValue: 'alt', name: 'Alt'}, + {optionValue: 'ctrl', name: 'Ctrl'}, + {optionValue: 'shift', name: 'Shift'} +]; + +if (!hasOwn(window, 'netscape')) { + _profileModifierKeys.push({optionValue: 'meta', name: 'Meta'}); +} + +const _profileModifierValueToName = new Map( + _profileModifierKeys.map(({optionValue, name}) => [optionValue, name]) +); + +const _profileModifierNameToValue = new Map( + _profileModifierKeys.map(({optionValue, name}) => [name, optionValue]) +); + const profileConditionsDescriptor = { popupLevel: { name: 'Popup Level', @@ -100,5 +118,53 @@ const profileConditionsDescriptor = { test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url)) } } + }, + modifierKeys: { + name: 'Modifier Keys', + description: 'Use profile depending on the active modifier keys.', + values: _profileModifierKeys, + defaultOperator: 'are', + operators: { + are: { + name: 'are', + placeholder: 'Press one or more modifier keys here', + defaultValue: '', + type: 'keyMulti', + transform: (optionValue) => optionValue + .split(' + ') + .filter((v) => v.length > 0) + .map((v) => _profileModifierNameToValue.get(v)), + transformReverse: (transformedOptionValue) => transformedOptionValue + .map((v) => _profileModifierValueToName.get(v)) + .join(' + '), + test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue)) + }, + areNot: { + name: 'are not', + placeholder: 'Press one or more modifier keys here', + defaultValue: '', + type: 'keyMulti', + transform: (optionValue) => optionValue + .split(' + ') + .filter((v) => v.length > 0) + .map((v) => _profileModifierNameToValue.get(v)), + transformReverse: (transformedOptionValue) => transformedOptionValue + .map((v) => _profileModifierValueToName.get(v)) + .join(' + '), + test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue)) + }, + include: { + name: 'include', + type: 'select', + defaultValue: 'alt', + test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue) + }, + notInclude: { + name: 'don\'t include', + type: 'select', + defaultValue: 'alt', + test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue) + } + } } }; diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js index b7d2eed8..47d495e6 100644 --- a/ext/bg/js/search.js +++ b/ext/bg/js/search.js @@ -17,6 +17,7 @@ /* global * ClipboardMonitor + * DOM * Display * QueryParser * apiClipboardGet @@ -178,7 +179,7 @@ class DisplaySearch extends Display { } onKeyDown(e) { - const key = Display.getKeyFromEvent(e); + const key = DOM.getKeyFromEvent(e); const ignoreKeys = this._onKeyDownIgnoreKeys; const activeModifierMap = new Map([ diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js index 84498b42..5b356101 100644 --- a/ext/bg/js/settings/conditions-ui.js +++ b/ext/bg/js/settings/conditions-ui.js @@ -16,6 +16,7 @@ */ /* global + * DOM * conditionsNormalizeOptionValue */ @@ -177,7 +178,8 @@ ConditionsUI.Condition = class Condition { this.parent = parent; this.condition = condition; this.container = ConditionsUI.instantiateTemplate('#condition-template').appendTo(parent.container); - this.input = this.container.find('input'); + this.input = this.container.find('.condition-input'); + this.inputInner = null; this.typeSelect = this.container.find('.condition-type'); this.operatorSelect = this.container.find('.condition-operator'); this.removeButton = this.container.find('.condition-remove'); @@ -186,14 +188,13 @@ ConditionsUI.Condition = class Condition { this.updateOperators(); this.updateInput(); - this.input.on('change', this.onInputChanged.bind(this)); this.typeSelect.on('change', this.onConditionTypeChanged.bind(this)); this.operatorSelect.on('change', this.onConditionOperatorChanged.bind(this)); this.removeButton.on('click', this.onRemoveClicked.bind(this)); } cleanup() { - this.input.off('change'); + this.inputInner.off('change'); this.typeSelect.off('change'); this.operatorSelect.off('change'); this.removeButton.off('click'); @@ -236,21 +237,48 @@ ConditionsUI.Condition = class Condition { updateInput() { const conditionDescriptors = this.parent.parent.conditionDescriptors; const {type, operator} = this.condition; - const props = new Map([ - ['placeholder', ''], - ['type', 'text'] - ]); const objects = []; + let inputType = null; if (hasOwn(conditionDescriptors, type)) { const conditionDescriptor = conditionDescriptors[type]; objects.push(conditionDescriptor); + if (hasOwn(conditionDescriptor, 'type')) { + inputType = conditionDescriptor.type; + } if (hasOwn(conditionDescriptor.operators, operator)) { const operatorDescriptor = conditionDescriptor.operators[operator]; objects.push(operatorDescriptor); + if (hasOwn(operatorDescriptor, 'type')) { + inputType = operatorDescriptor.type; + } } } + this.input.empty(); + if (inputType === 'select') { + this.inputInner = this.createSelectElement(objects); + } else if (inputType === 'keyMulti') { + this.inputInner = this.createInputKeyMultiElement(objects); + } else { + this.inputInner = this.createInputElement(objects); + } + this.inputInner.appendTo(this.input); + this.inputInner.on('change', this.onInputChanged.bind(this)); + + const {valid} = this.validateValue(this.condition.value); + this.inputInner.toggleClass('is-invalid', !valid); + this.inputInner.val(this.condition.value); + } + + createInputElement(objects) { + const inputInner = ConditionsUI.instantiateTemplate('#condition-input-text-template'); + + const props = new Map([ + ['placeholder', ''], + ['type', 'text'] + ]); + for (const object of objects) { if (hasOwn(object, 'placeholder')) { props.set('placeholder', object.placeholder); @@ -266,12 +294,95 @@ ConditionsUI.Condition = class Condition { } for (const [prop, value] of props.entries()) { - this.input.prop(prop, value); + inputInner.prop(prop, value); } - const {valid} = this.validateValue(this.condition.value); - this.input.toggleClass('is-invalid', !valid); - this.input.val(this.condition.value); + return inputInner; + } + + createInputKeyMultiElement(objects) { + const inputInner = this.createInputElement(objects); + + inputInner.prop('readonly', true); + + let values = []; + for (const object of objects) { + if (hasOwn(object, 'values')) { + values = object.values; + } + } + + const pressedKeyIndices = new Set(); + + const onKeyDown = ({originalEvent}) => { + const pressedKeyEventName = DOM.getKeyFromEvent(originalEvent); + if (pressedKeyEventName === 'Escape' || pressedKeyEventName === 'Backspace') { + pressedKeyIndices.clear(); + inputInner.val(''); + inputInner.change(); + return; + } + + const pressedModifiers = DOM.getActiveModifiers(originalEvent); + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey + // https://askubuntu.com/questions/567731/why-is-shift-alt-being-mapped-to-meta + // It works with mouse events on some platforms, so try to determine if metaKey is pressed + // hack; only works when Shift and Alt are not pressed + const isMetaKeyChrome = ( + pressedKeyEventName === 'Meta' && + getSetDifference(new Set(['shift', 'alt']), pressedModifiers).size !== 0 + ); + if (isMetaKeyChrome) { + pressedModifiers.add('meta'); + } + + for (const modifier of pressedModifiers) { + const foundIndex = values.findIndex(({optionValue}) => optionValue === modifier); + if (foundIndex !== -1) { + pressedKeyIndices.add(foundIndex); + } + } + + const inputValue = [...pressedKeyIndices].map((i) => values[i].name).join(' + '); + inputInner.val(inputValue); + inputInner.change(); + }; + + inputInner.on('keydown', onKeyDown); + + return inputInner; + } + + createSelectElement(objects) { + const inputInner = ConditionsUI.instantiateTemplate('#condition-input-select-template'); + + const data = new Map([ + ['values', []], + ['defaultValue', null] + ]); + + for (const object of objects) { + if (hasOwn(object, 'values')) { + data.set('values', object.values); + } + if (hasOwn(object, 'defaultValue')) { + data.set('defaultValue', object.defaultValue); + } + } + + for (const {optionValue, name} of data.get('values')) { + const option = ConditionsUI.instantiateTemplate('#condition-input-option-template'); + option.attr('value', optionValue); + option.text(name); + option.appendTo(inputInner); + } + + const defaultValue = data.get('defaultValue'); + if (defaultValue !== null) { + inputInner.val(defaultValue); + } + + return inputInner; } validateValue(value) { @@ -291,9 +402,9 @@ ConditionsUI.Condition = class Condition { } onInputChanged() { - const {valid, value} = this.validateValue(this.input.val()); - this.input.toggleClass('is-invalid', !valid); - this.input.val(value); + const {valid, value} = this.validateValue(this.inputInner.val()); + this.inputInner.toggleClass('is-invalid', !valid); + this.inputInner.val(value); this.condition.value = value; this.save(); } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index a0220e96..fc9221f8 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -117,7 +117,7 @@
-
+