From 5c4614f585648c2b835efc1d369e78918bc4f5ff Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 17 Aug 2019 18:50:48 -0400 Subject: [PATCH 01/15] Add support for showing recursive popups --- ext/bg/background.html | 1 + ext/bg/js/backend-api-forwarder.js | 45 +++++++++++ ext/bg/js/backend.js | 2 + ext/fg/float.html | 6 ++ ext/fg/js/frontend-api-receiver.js | 62 ++++++++++++++ ext/fg/js/frontend-api-sender.js | 125 +++++++++++++++++++++++++++++ ext/fg/js/frontend.js | 34 ++++++-- ext/fg/js/popup-proxy-host.js | 118 +++++++++++++++++++++++++++ ext/fg/js/popup-proxy.js | 116 ++++++++++++++++++++++++++ ext/fg/js/popup.js | 45 ++++++++++- ext/fg/js/util.js | 6 +- ext/manifest.json | 2 + 12 files changed, 550 insertions(+), 12 deletions(-) create mode 100644 ext/bg/js/backend-api-forwarder.js create mode 100644 ext/fg/js/frontend-api-receiver.js create mode 100644 ext/fg/js/frontend-api-sender.js create mode 100644 ext/fg/js/popup-proxy-host.js create mode 100644 ext/fg/js/popup-proxy.js diff --git a/ext/bg/background.html b/ext/bg/background.html index 5978f10f..90a56024 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -15,6 +15,7 @@ + diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js new file mode 100644 index 00000000..979afd16 --- /dev/null +++ b/ext/bg/js/backend-api-forwarder.js @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +class BackendApiForwarder { + constructor() { + chrome.runtime.onConnect.addListener(this.onConnect.bind(this)); + } + + onConnect(port) { + if (port.name !== 'backend-api-forwarder') { return; } + + let tabId; + if (!( + port.sender && + port.sender.tab && + (typeof (tabId = port.sender.tab.id)) === 'number' + )) { + port.disconnect(); + return; + } + + const forwardPort = chrome.tabs.connect(tabId, {name: 'frontend-api-receiver'}); + + port.onMessage.addListener(message => forwardPort.postMessage(message)); + forwardPort.onMessage.addListener(message => port.postMessage(message)); + port.onDisconnect.addListener(() => forwardPort.disconnect()); + forwardPort.onDisconnect.addListener(() => port.disconnect()); + } +} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index d95cb82d..8f99c13a 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -22,6 +22,8 @@ class Backend { this.translator = new Translator(); this.anki = new AnkiNull(); this.options = null; + + this.apiForwarder = new BackendApiForwarder(); } async prepare() { diff --git a/ext/fg/float.html b/ext/fg/float.html index 0133e653..bd08296a 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -43,5 +43,11 @@ + + + + + + diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js new file mode 100644 index 00000000..f5d29f67 --- /dev/null +++ b/ext/fg/js/frontend-api-receiver.js @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +class FrontendApiReceiver { + constructor(source='', handlers={}) { + this.source = source; + this.handlers = handlers; + + chrome.runtime.onConnect.addListener(this.onConnect.bind(this)); + } + + onConnect(port) { + if (port.name !== 'frontend-api-receiver') { return; } + + port.onMessage.addListener(this.onMessage.bind(this, port)); + } + + onMessage(port, {id, action, params, target}) { + if ( + target !== this.source || + !this.handlers.hasOwnProperty(action) + ) { + return; + } + + this.sendAck(port, id); + + const handler = this.handlers[action]; + handler(params).then( + result => { + this.sendResult(port, id, {result}); + }, + e => { + const error = typeof e.toString === 'function' ? e.toString() : e; + this.sendResult(port, id, {error}); + }); + } + + sendAck(port, id) { + port.postMessage({type: 'ack', id}); + } + + sendResult(port, id, data) { + port.postMessage({type: 'result', id, data}); + } +} diff --git a/ext/fg/js/frontend-api-sender.js b/ext/fg/js/frontend-api-sender.js new file mode 100644 index 00000000..e2becb90 --- /dev/null +++ b/ext/fg/js/frontend-api-sender.js @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +class FrontendApiSender { + constructor() { + this.ackTimeout = 3000; // 3 seconds + this.responseTimeout = 10000; // 10 seconds + this.callbacks = {}; + this.disconnected = false; + this.nextId = 0; + + 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)); + } + + invoke(action, params, target) { + if (this.disconnected) { + return Promise.reject('Disconnected'); + } + + const id = `${this.nextId}`; + ++this.nextId; + + return new Promise((resolve, reject) => { + const info = {id, resolve, reject, ack: false, timer: null}; + this.callbacks[id] = info; + info.timer = setTimeout(() => this.onError(id, 'Timeout (ack)'), this.ackTimeout); + + this.port.postMessage({id, action, params, target}); + }); + } + + onMessage({type, id, data}) { + switch (type) { + case 'ack': + this.onAck(id); + break; + case 'result': + this.onResult(id, data); + break; + } + } + + onDisconnect() { + this.disconnected = true; + + const ids = Object.keys(this.callbacks); + for (const id of ids) { + this.onError(id, 'Disconnected'); + } + } + + onAck(id) { + if (!this.callbacks.hasOwnProperty(id)) { + console.warn(`ID ${id} not found`); + return; + } + + const info = this.callbacks[id]; + if (info.ack) { + console.warn(`Request ${id} already ack'd`); + return; + } + + info.ack = true; + clearTimeout(info.timer); + info.timer = setTimeout(() => this.onError(id, 'Timeout (response)'), this.responseTimeout); + } + + onResult(id, data) { + if (!this.callbacks.hasOwnProperty(id)) { + console.warn(`ID ${id} not found`); + return; + } + + const info = this.callbacks[id]; + if (!info.ack) { + console.warn(`Request ${id} not ack'd`); + return; + } + + delete this.callbacks[id]; + clearTimeout(info.timer); + info.timer = null; + + if (typeof data.error === 'string') { + info.reject(data.error); + } else { + info.resolve(data.result); + } + } + + onError(id, reason) { + if (!this.callbacks.hasOwnProperty(id)) { return; } + const info = this.callbacks[id]; + delete this.callbacks[id]; + info.timer = null; + info.reject(reason); + } + + static generateId(length) { + let id = ''; + for (let i = 0; i < length; ++i) { + id += Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + } + return id; + } +} diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 8a5c48d0..d378dd61 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -18,8 +18,8 @@ class Frontend { - constructor() { - this.popup = new Popup(); + constructor(popup) { + this.popup = popup; this.popupTimer = null; this.mouseDownLeft = false; this.mouseDownMiddle = false; @@ -36,6 +36,25 @@ class Frontend { this.scrollPrevent = false; } + static create() { + const floatUrl = chrome.extension.getURL('/fg/float.html'); + const currentUrl = location.href.replace(/[\?#][\w\W]*$/, ""); + const isNested = (currentUrl === floatUrl); + + let id = null; + if (isNested) { + const match = /[&?]id=([^&]*?)(?:&|$)/.exec(location.href); + if (match !== null) { + id = match[1]; + } + } + + const popup = isNested ? new PopupProxy(id) : PopupProxyHost.instance.createPopup(); + const frontend = new Frontend(popup); + frontend.prepare(); + return frontend; + } + async prepare() { try { this.options = await apiOptionsGet(); @@ -259,9 +278,8 @@ class Frontend { const handler = handlers[action]; if (handler) { handler(params); + callback(); } - - callback(); } onError(error) { @@ -281,7 +299,10 @@ class Frontend { } async searchAt(point, type) { - if (this.pendingLookup || this.popup.containsPoint(point)) { + if ( + this.pendingLookup || + (this.popup.containsPointIsAsync() ? await this.popup.containsPointAsync(point) : this.popup.containsPoint(point)) + ) { return; } @@ -482,5 +503,4 @@ class Frontend { } } -window.yomichan_frontend = new Frontend(); -window.yomichan_frontend.prepare(); +window.yomichan_frontend = Frontend.create(); diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js new file mode 100644 index 00000000..189481bc --- /dev/null +++ b/ext/fg/js/popup-proxy-host.js @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +class PopupProxyHost { + constructor() { + this.popups = {}; + this.nextId = 0; + this.apiReceiver = new FrontendApiReceiver('popup-proxy-host', { + createNestedPopup: ({parentId}) => this.createNestedPopup(parentId), + show: ({id, elementRect, options}) => this.show(id, elementRect, options), + showOrphaned: ({id, elementRect, options}) => this.show(id, elementRect, options), + hide: ({id}) => this.hide(id), + setVisible: ({id, visible}) => this.setVisible(id, visible), + containsPoint: ({id, point}) => this.containsPoint(id, point), + termsShow: ({id, elementRect, definitions, options, context}) => this.termsShow(id, elementRect, definitions, options, context), + kanjiShow: ({id, elementRect, definitions, options, context}) => this.kanjiShow(id, elementRect, definitions, options, context), + clearAutoPlayTimer: ({id}) => this.clearAutoPlayTimer(id) + }); + } + + createPopup(parentId) { + const parent = (typeof parentId === 'string' && this.popups.hasOwnProperty(parentId) ? this.popups[parentId] : null); + const id = `${this.nextId}`; + ++this.nextId; + const popup = new Popup(id); + if (parent !== null) { + popup.parent = parent; + parent.children.push(popup); + } + this.popups[id] = popup; + return popup; + } + + async createNestedPopup(parentId) { + return this.createPopup(parentId).id; + } + + getPopup(id) { + if (!this.popups.hasOwnProperty(id)) { + throw 'Invalid popup ID'; + } + + return this.popups[id]; + } + + jsonRectToDOMRect(popup, jsonRect) { + let x = jsonRect.x; + let y = jsonRect.y; + if (popup.parent !== null) { + const popupRect = popup.parent.container.getBoundingClientRect(); + x += popupRect.x; + y += popupRect.y; + } + return new DOMRect(x, y, jsonRect.width, jsonRect.height); + } + + async show(id, elementRect, options) { + const popup = this.getPopup(id); + elementRect = this.jsonRectToDOMRect(popup, elementRect); + return await popup.show(elementRect, options); + } + + async showOrphaned(id, elementRect, options) { + const popup = this.getPopup(id); + elementRect = this.jsonRectToDOMRect(popup, elementRect); + return await popup.showOrphaned(elementRect, options); + } + + async hide(id) { + const popup = this.getPopup(id); + return popup.hide(); + } + + async setVisible(id, visible) { + const popup = this.getPopup(id); + return popup.setVisible(visible); + } + + async containsPoint(id, point) { + const popup = this.getPopup(id); + return popup.containsPointIsAsync() ? await popup.containsPointAsync(point) : popup.containsPoint(point); + } + + async termsShow(id, elementRect, definitions, options, context) { + const popup = this.getPopup(id); + elementRect = this.jsonRectToDOMRect(popup, elementRect); + return await popup.termsShow(elementRect, definitions, options, context); + } + + async kanjiShow(id, elementRect, definitions, options, context) { + const popup = this.getPopup(id); + elementRect = this.jsonRectToDOMRect(popup, elementRect); + return await popup.kanjiShow(elementRect, definitions, options, context); + } + + async clearAutoPlayTimer(id) { + const popup = this.getPopup(id); + return popup.clearAutoPlayTimer(); + } +} + +PopupProxyHost.instance = new PopupProxyHost(); diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js new file mode 100644 index 00000000..3a15be7d --- /dev/null +++ b/ext/fg/js/popup-proxy.js @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +class PopupProxy { + constructor(parentId) { + this.parentId = parentId; + this.id = null; + this.idPromise = null; + this.parent = null; + this.children = []; + + this.container = null; + + this.apiSender = new FrontendApiSender(); + } + + getPopupId() { + if (this.idPromise === null) { + this.idPromise = this.getPopupIdAsync(); + } + return this.idPromise; + } + + async getPopupIdAsync() { + const id = await this.invokeHostApi('createNestedPopup', {parentId: this.parentId}); + this.id = id; + return id; + } + + async show(elementRect, options) { + const id = await this.getPopupId(); + elementRect = PopupProxy.DOMRectToJson(elementRect); + return await this.invokeHostApi('show', {id, elementRect, options}); + } + + async showOrphaned(elementRect, options) { + const id = await this.getPopupId(); + elementRect = PopupProxy.DOMRectToJson(elementRect); + return await this.invokeHostApi('showOrphaned', {id, elementRect, options}); + } + + async hide() { + if (this.id === null) { + return; + } + return await this.invokeHostApi('hide', {id: this.id}); + } + + async setVisible(visible) { + const id = await this.getPopupId(); + return await this.invokeHostApi('setVisible', {id, visible}); + } + + containsPoint() { + throw 'Non-async function not supported'; + } + + async containsPointAsync(point) { + if (this.id === null) { + return false; + } + return await this.invokeHostApi('containsPoint', {id: this.id, point}); + } + + containsPointIsAsync() { + return true; + } + + async termsShow(elementRect, definitions, options, context) { + const id = await this.getPopupId(); + elementRect = PopupProxy.DOMRectToJson(elementRect); + return await this.invokeHostApi('termsShow', {id, elementRect, definitions, options, context}); + } + + async kanjiShow(elementRect, definitions, options, context) { + const id = await this.getPopupId(); + elementRect = PopupProxy.DOMRectToJson(elementRect); + return await this.invokeHostApi('kanjiShow', {id, elementRect, definitions, options, context}); + } + + async clearAutoPlayTimer() { + if (this.id === null) { + return; + } + return await this.invokeHostApi('clearAutoPlayTimer', {id: this.id}); + } + + invokeHostApi(action, params={}) { + return this.apiSender.invoke(action, params, 'popup-proxy-host'); + } + + static DOMRectToJson(domRect) { + return { + x: domRect.x, + y: domRect.y, + width: domRect.width, + height: domRect.height + }; + } +} diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 86ce575d..f6b4f6d9 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -18,12 +18,15 @@ class Popup { - constructor() { + constructor(id) { + this.id = id; + this.parent = null; + this.children = []; this.container = document.createElement('iframe'); this.container.id = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); this.container.addEventListener('scroll', e => e.stopPropagation()); - this.container.setAttribute('src', chrome.extension.getURL('/fg/float.html')); + this.container.setAttribute('src', chrome.extension.getURL(`/fg/float.html?id=${id}`)); this.container.style.width = '0px'; this.container.style.height = '0px'; this.injected = null; @@ -77,6 +80,8 @@ class Popup { container.style.width = `${width}px`; container.style.height = `${height}px`; container.style.visibility = 'visible'; + + this.hideChildren(); } static getPositionForHorizontalText(elementRect, width, height, maxWidth, maxHeight, optionsGeneral) { @@ -178,8 +183,34 @@ class Popup { } hide() { - this.container.style.visibility = 'hidden'; + this.hideContainer(); this.container.blur(); + this.hideChildren(); + } + + hideChildren() { + if (this.children.length === 0) { + return; + } + + const targets = this.children.slice(0); + while (targets.length > 0) { + const target = targets.shift(); + if (target.isContainerHidden()) { continue; } + + target.hideContainer(); + for (const child of target.children) { + targets.push(child); + } + } + } + + hideContainer() { + this.container.style.visibility = 'hidden'; + } + + isContainerHidden() { + return (this.container.style.visibility === 'hidden'); } isVisible() { @@ -209,6 +240,14 @@ class Popup { return contained; } + async containsPointAsync(point) { + return containsPoint(point); + } + + containsPointIsAsync() { + return false; + } + async termsShow(elementRect, writingMode, definitions, options, context) { await this.show(elementRect, writingMode, options); this.invokeApi('termsShow', {definitions, options, context}); diff --git a/ext/fg/js/util.js b/ext/fg/js/util.js index 7518beb5..dc99274e 100644 --- a/ext/fg/js/util.js +++ b/ext/fg/js/util.js @@ -24,9 +24,10 @@ function utilAsync(func) { } function utilInvoke(action, params={}) { + const data = {action, params}; return new Promise((resolve, reject) => { try { - chrome.runtime.sendMessage({action, params}, (response) => { + chrome.runtime.sendMessage(data, (response) => { utilCheckLastError(chrome.runtime.lastError); if (response !== null && typeof response === 'object') { if (response.error) { @@ -35,7 +36,8 @@ function utilInvoke(action, params={}) { resolve(response.result); } } else { - reject(`Unexpected response of type ${typeof response}`); + const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; + reject(`${message} (${JSON.stringify(data)})`); } }); } catch (e) { diff --git a/ext/manifest.json b/ext/manifest.json index 62eed6ec..eed6e40a 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -21,7 +21,9 @@ "mixed/js/extension.js", "fg/js/api.js", "fg/js/document.js", + "fg/js/frontend-api-receiver.js", "fg/js/popup.js", + "fg/js/popup-proxy-host.js", "fg/js/source.js", "fg/js/util.js", "fg/js/frontend.js" From 1a9348ec27b903af47511da11306f884a82cf353 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 17 Aug 2019 19:32:58 -0400 Subject: [PATCH 02/15] Add option for maximum nested popup depth --- ext/bg/js/options.js | 3 ++- ext/bg/js/settings.js | 2 ++ ext/bg/settings.html | 5 ++++ ext/fg/float.html | 6 +---- ext/fg/js/popup-nested.js | 48 +++++++++++++++++++++++++++++++++++ ext/fg/js/popup-proxy-host.js | 3 ++- ext/fg/js/popup.js | 5 ++-- 7 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 ext/fg/js/popup-nested.js diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 7d993987..2197c72c 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -219,7 +219,8 @@ function optionsSetDefaults(options) { delay: 20, length: 10, modifier: 'shift', - deepDomScan: false + deepDomScan: false, + popupNestingMaxDepth: 0 }, dictionaries: {}, diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index f5d669b2..de1616f3 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -51,6 +51,7 @@ async function formRead() { optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10); optionsNew.scanning.length = parseInt($('#scan-length').val(), 10); optionsNew.scanning.modifier = $('#scan-modifier-key').val(); + optionsNew.scanning.popupNestingMaxDepth = parseInt($('#popup-nesting-max-depth').val(), 10); optionsNew.anki.enable = $('#anki-enable').prop('checked'); optionsNew.anki.tags = $('#card-tags').val().split(/[,; ]+/); @@ -192,6 +193,7 @@ async function onReady() { $('#scan-delay').val(options.scanning.delay); $('#scan-length').val(options.scanning.length); $('#scan-modifier-key').val(options.scanning.modifier); + $('#popup-nesting-max-depth').val(options.scanning.popupNestingMaxDepth); $('#dict-purge-link').click(utilAsync(onDictionaryPurge)); $('#dict-file').change(utilAsync(onDictionaryImport)); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index cc140023..a0d08ec5 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -215,6 +215,11 @@ + +
+ + +
diff --git a/ext/fg/float.html b/ext/fg/float.html index bd08296a..465db589 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -44,10 +44,6 @@ - - - - - + diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js new file mode 100644 index 00000000..7df4e4e2 --- /dev/null +++ b/ext/fg/js/popup-nested.js @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +async function popupNestedSetup() { + const options = await apiOptionsGet(); + const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth; + + let depth = null; + const match = /[&?]depth=([^&]*?)(?:&|$)/.exec(location.href); + if (match !== null) { + depth = parseInt(match[1], 10); + } + + if (!(typeof popupNestingMaxDepth === 'number' && typeof depth === 'number' && depth < popupNestingMaxDepth)) { + return; + } + + const scriptSrcs = [ + '/fg/js/frontend-api-sender.js', + '/fg/js/popup.js', + '/fg/js/popup-proxy.js', + '/fg/js/frontend.js' + ]; + for (const src of scriptSrcs) { + const script = document.createElement('script'); + script.async = false; + script.src = src; + document.body.appendChild(script); + } +} + +popupNestedSetup(); diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 189481bc..ba3db832 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -36,9 +36,10 @@ class PopupProxyHost { createPopup(parentId) { const parent = (typeof parentId === 'string' && this.popups.hasOwnProperty(parentId) ? this.popups[parentId] : null); + const depth = (parent !== null ? parent.depth + 1 : 0); const id = `${this.nextId}`; ++this.nextId; - const popup = new Popup(id); + const popup = new Popup(id, depth); if (parent !== null) { popup.parent = parent; parent.children.push(popup); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index f6b4f6d9..00dfeb89 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -18,15 +18,16 @@ class Popup { - constructor(id) { + constructor(id, depth) { this.id = id; + this.depth = depth; this.parent = null; this.children = []; this.container = document.createElement('iframe'); this.container.id = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); this.container.addEventListener('scroll', e => e.stopPropagation()); - this.container.setAttribute('src', chrome.extension.getURL(`/fg/float.html?id=${id}`)); + this.container.setAttribute('src', chrome.extension.getURL(`/fg/float.html?id=${id}&depth=${depth}`)); this.container.style.width = '0px'; this.container.style.height = '0px'; this.injected = null; From 42ec3e2a43dfd9ac0748ca7c364cef2b44f625a2 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 18 Aug 2019 16:48:18 -0400 Subject: [PATCH 03/15] Add support for popup on the search page --- ext/bg/js/options.js | 3 ++- ext/bg/js/search-frontend.js | 51 ++++++++++++++++++++++++++++++++++++ ext/bg/js/settings.js | 2 ++ ext/bg/search.html | 1 + ext/bg/settings.html | 4 +++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 ext/bg/js/search-frontend.js diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 2197c72c..1b2d5e1a 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -220,7 +220,8 @@ function optionsSetDefaults(options) { length: 10, modifier: 'shift', deepDomScan: false, - popupNestingMaxDepth: 0 + popupNestingMaxDepth: 0, + enableOnSearchPage: true }, dictionaries: {}, diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js new file mode 100644 index 00000000..77aa4052 --- /dev/null +++ b/ext/bg/js/search-frontend.js @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 Alex Yatskov + * Author: Alex Yatskov + * + * 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 . + */ + + +async function searchFrontendSetup() { + const options = await apiOptionsGet(); + if (!options.scanning.enableOnSearchPage) { return; } + + const scriptSrcs = [ + '/fg/js/api.js', + '/fg/js/frontend-api-receiver.js', + '/fg/js/popup.js', + '/fg/js/popup-proxy-host.js', + '/fg/js/util.js', + '/fg/js/frontend.js' + ]; + for (const src of scriptSrcs) { + 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); + } +} + +searchFrontendSetup(); diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index de1616f3..5472c7bd 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -48,6 +48,7 @@ async function formRead() { optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); optionsNew.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); + optionsNew.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10); optionsNew.scanning.length = parseInt($('#scan-length').val(), 10); optionsNew.scanning.modifier = $('#scan-modifier-key').val(); @@ -190,6 +191,7 @@ async function onReady() { $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); + $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); $('#scan-delay').val(options.scanning.delay); $('#scan-length').val(options.scanning.length); $('#scan-modifier-key').val(options.scanning.modifier); diff --git a/ext/bg/search.html b/ext/bg/search.html index 05c0daab..38c5a4e9 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -51,5 +51,6 @@ + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index a0d08ec5..0005efa6 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -192,6 +192,10 @@
+
+ +
+
From 53aad0bef68bf6930b684fda4a25e1a045cd800e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 18 Aug 2019 20:51:19 -0400 Subject: [PATCH 04/15] Fix messaging issues when iframes are present in the document --- ext/bg/js/api.js | 5 +++ ext/bg/js/backend.js | 4 +++ ext/bg/js/search-frontend.js | 2 +- ext/fg/js/api.js | 4 +++ ext/fg/js/frontend-api-receiver.js | 16 ++++----- ext/fg/js/frontend-api-sender.js | 8 +++-- ext/fg/js/frontend.js | 10 ++++-- ext/fg/js/popup-proxy-host.js | 21 ++++++++++-- ext/fg/js/popup-proxy.js | 9 +++-- ext/fg/js/popup.js | 55 +++++++++++++++++++----------- ext/manifest.json | 2 +- 11 files changed, 97 insertions(+), 39 deletions(-) diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js index 4b2bacd7..b8ef4362 100644 --- a/ext/bg/js/api.js +++ b/ext/bg/js/api.js @@ -205,3 +205,8 @@ function apiForward(action, params, sender) { chrome.tabs.sendMessage(tabId, {action, params}, (response) => resolve(response)); }); } + +function apiFrameInformationGet(sender) { + const frameId = sender.frameId; + return Promise.resolve({frameId}); +} diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 8f99c13a..39fd4288 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -127,6 +127,10 @@ class Backend { forward: ({action, params}) => { forward(apiForward(action, params, sender), callback); + }, + + frameInformationGet: () => { + forward(apiFrameInformationGet(sender), callback); } }; diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js index 77aa4052..840a1ea8 100644 --- a/ext/bg/js/search-frontend.js +++ b/ext/bg/js/search-frontend.js @@ -25,8 +25,8 @@ async function searchFrontendSetup() { '/fg/js/api.js', '/fg/js/frontend-api-receiver.js', '/fg/js/popup.js', - '/fg/js/popup-proxy-host.js', '/fg/js/util.js', + '/fg/js/popup-proxy-host.js', '/fg/js/frontend.js' ]; for (const src of scriptSrcs) { diff --git a/ext/fg/js/api.js b/ext/fg/js/api.js index 99ad307c..6bcb0dbb 100644 --- a/ext/fg/js/api.js +++ b/ext/fg/js/api.js @@ -64,3 +64,7 @@ function apiScreenshotGet(options) { function apiForward(action, params) { return utilInvoke('forward', {action, params}); } + +function apiFrameInformationGet() { + return utilInvoke('frameInformationGet'); +} diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js index f5d29f67..687e5c3c 100644 --- a/ext/fg/js/frontend-api-receiver.js +++ b/ext/fg/js/frontend-api-receiver.js @@ -31,7 +31,7 @@ class FrontendApiReceiver { port.onMessage.addListener(this.onMessage.bind(this, port)); } - onMessage(port, {id, action, params, target}) { + onMessage(port, {id, action, params, target, senderId}) { if ( target !== this.source || !this.handlers.hasOwnProperty(action) @@ -39,24 +39,24 @@ class FrontendApiReceiver { return; } - this.sendAck(port, id); + this.sendAck(port, id, senderId); const handler = this.handlers[action]; handler(params).then( result => { - this.sendResult(port, id, {result}); + this.sendResult(port, id, senderId, {result}); }, e => { const error = typeof e.toString === 'function' ? e.toString() : e; - this.sendResult(port, id, {error}); + this.sendResult(port, id, senderId, {error}); }); } - sendAck(port, id) { - port.postMessage({type: 'ack', id}); + sendAck(port, id, senderId) { + port.postMessage({type: 'ack', id, senderId}); } - sendResult(port, id, data) { - port.postMessage({type: 'result', id, 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 e2becb90..a1cb02c4 100644 --- a/ext/fg/js/frontend-api-sender.js +++ b/ext/fg/js/frontend-api-sender.js @@ -19,6 +19,7 @@ class FrontendApiSender { constructor() { + this.senderId = FrontendApiSender.generateId(16); this.ackTimeout = 3000; // 3 seconds this.responseTimeout = 10000; // 10 seconds this.callbacks = {}; @@ -43,11 +44,12 @@ class FrontendApiSender { this.callbacks[id] = info; info.timer = setTimeout(() => this.onError(id, 'Timeout (ack)'), this.ackTimeout); - this.port.postMessage({id, action, params, target}); + this.port.postMessage({id, action, params, target, senderId: this.senderId}); }); } - onMessage({type, id, data}) { + onMessage({type, id, data, senderId}) { + if (senderId !== this.senderId) { return; } switch (type) { case 'ack': this.onAck(id); @@ -69,7 +71,7 @@ class FrontendApiSender { onAck(id) { if (!this.callbacks.hasOwnProperty(id)) { - console.warn(`ID ${id} not found`); + console.warn(`ID ${id} not found for ack`); return; } diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index d378dd61..9c511d8a 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -42,14 +42,20 @@ class Frontend { const isNested = (currentUrl === floatUrl); let id = null; + let parentFrameId = null; if (isNested) { - const match = /[&?]id=([^&]*?)(?:&|$)/.exec(location.href); + let match = /[&?]id=([^&]*?)(?:&|$)/.exec(location.href); if (match !== null) { id = match[1]; } + + match = /[&?]parent=(\d+)(?:&|$)/.exec(location.href); + if (match !== null) { + parentFrameId = parseInt(match[1], 10); + } } - const popup = isNested ? new PopupProxy(id) : PopupProxyHost.instance.createPopup(); + const popup = isNested ? new PopupProxy(id, parentFrameId) : PopupProxyHost.instance.createPopup(null); const frontend = new Frontend(popup); frontend.prepare(); return frontend; diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index ba3db832..cdd1d02c 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -21,7 +21,22 @@ class PopupProxyHost { constructor() { this.popups = {}; this.nextId = 0; - this.apiReceiver = new FrontendApiReceiver('popup-proxy-host', { + this.apiReceiver = null; + this.frameIdPromise = null; + } + + static create() { + const popupProxyHost = new PopupProxyHost(); + popupProxyHost.prepare(); + return popupProxyHost; + } + + async prepare() { + this.frameIdPromise = apiFrameInformationGet(); + const {frameId} = await this.frameIdPromise; + if (typeof frameId !== 'number') { return; } + + this.apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${frameId}`, { createNestedPopup: ({parentId}) => this.createNestedPopup(parentId), show: ({id, elementRect, options}) => this.show(id, elementRect, options), showOrphaned: ({id, elementRect, options}) => this.show(id, elementRect, options), @@ -39,7 +54,7 @@ class PopupProxyHost { const depth = (parent !== null ? parent.depth + 1 : 0); const id = `${this.nextId}`; ++this.nextId; - const popup = new Popup(id, depth); + const popup = new Popup(id, depth, this.frameIdPromise); if (parent !== null) { popup.parent = parent; parent.children.push(popup); @@ -116,4 +131,4 @@ class PopupProxyHost { } } -PopupProxyHost.instance = new PopupProxyHost(); +PopupProxyHost.instance = PopupProxyHost.create(); diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 3a15be7d..bbf6a2cf 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -18,12 +18,14 @@ class PopupProxy { - constructor(parentId) { + constructor(parentId, parentFrameId) { this.parentId = parentId; + this.parentFrameId = parentFrameId; this.id = null; this.idPromise = null; this.parent = null; this.children = []; + this.depth = 0; this.container = null; @@ -102,7 +104,10 @@ class PopupProxy { } invokeHostApi(action, params={}) { - return this.apiSender.invoke(action, params, 'popup-proxy-host'); + if (typeof this.parentFrameId !== 'number') { + return Promise.reject('Invalid frame'); + } + return this.apiSender.invoke(action, params, `popup-proxy-host#${this.parentFrameId}`); } static DOMRectToJson(domRect) { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 00dfeb89..24b5684d 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -18,38 +18,55 @@ class Popup { - constructor(id, depth) { + constructor(id, depth, frameIdPromise) { this.id = id; this.depth = depth; + this.frameIdPromise = frameIdPromise; + this.frameId = null; this.parent = null; this.children = []; this.container = document.createElement('iframe'); this.container.id = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); this.container.addEventListener('scroll', e => e.stopPropagation()); - this.container.setAttribute('src', chrome.extension.getURL(`/fg/float.html?id=${id}&depth=${depth}`)); this.container.style.width = '0px'; this.container.style.height = '0px'; - this.injected = null; + this.injectPromise = null; + this.isInjected = false; } inject(options) { - if (!this.injected) { - this.injected = new Promise((resolve, reject) => { - this.container.addEventListener('load', () => { - this.invokeApi('setOptions', { - general: { - customPopupCss: options.general.customPopupCss - } - }); - resolve(); - }); - this.observeFullscreen(); - this.onFullscreenChanged(); - }); + if (this.injectPromise === null) { + this.injectPromise = this.createInjectPromise(options); + } + return this.injectPromise; + } + + async createInjectPromise(options) { + try { + const {frameId} = await this.frameIdPromise; + if (typeof frameId === 'number') { + this.frameId = frameId; + } + } catch (e) { + // NOP } - return this.injected; + return new Promise((resolve) => { + const parent = (typeof this.frameId === 'number' ? this.frameId : ''); + this.container.setAttribute('src', chrome.extension.getURL(`/fg/float.html?id=${this.id}&depth=${this.depth}&parent=${parent}`)); + this.container.addEventListener('load', () => { + this.invokeApi('setOptions', { + general: { + customPopupCss: options.general.customPopupCss + } + }); + resolve(); + }); + this.observeFullscreen(); + this.onFullscreenChanged(); + this.isInjected = true; + }); } async show(elementRect, writingMode, options) { @@ -215,7 +232,7 @@ class Popup { } isVisible() { - return this.injected && this.container.style.visibility !== 'hidden'; + return this.isInjected && this.container.style.visibility !== 'hidden'; } setVisible(visible) { @@ -260,7 +277,7 @@ class Popup { } clearAutoPlayTimer() { - if (this.injected) { + if (this.isInjected) { this.invokeApi('clearAutoPlayTimer'); } } diff --git a/ext/manifest.json b/ext/manifest.json index eed6e40a..06d13dd1 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -23,9 +23,9 @@ "fg/js/document.js", "fg/js/frontend-api-receiver.js", "fg/js/popup.js", - "fg/js/popup-proxy-host.js", "fg/js/source.js", "fg/js/util.js", + "fg/js/popup-proxy-host.js", "fg/js/frontend.js" ], "css": ["fg/css/client.css"], From 3491affcf13ef10b78714d4955da6ee9aeb11457 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 18 Aug 2019 21:49:55 -0400 Subject: [PATCH 05/15] Update nested initialization parameters passed via message rather than using the URL query string --- ext/fg/js/float.js | 4 ++++ ext/fg/js/frontend.js | 20 +++----------------- ext/fg/js/popup-nested.js | 19 ++++++++++--------- ext/fg/js/popup.js | 9 +++++++-- 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/ext/fg/js/float.js b/ext/fg/js/float.js index c0ec8a15..3c521714 100644 --- a/ext/fg/js/float.js +++ b/ext/fg/js/float.js @@ -72,6 +72,10 @@ class DisplayFloat extends Display { if (css) { this.setStyle(css); } + }, + + popupNestedInitialize: ({id, depth, parentFrameId}) => { + popupNestedInitialize(id, depth, parentFrameId); } }; diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 9c511d8a..3605dffd 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -37,23 +37,9 @@ class Frontend { } static create() { - const floatUrl = chrome.extension.getURL('/fg/float.html'); - const currentUrl = location.href.replace(/[\?#][\w\W]*$/, ""); - const isNested = (currentUrl === floatUrl); - - let id = null; - let parentFrameId = null; - if (isNested) { - let match = /[&?]id=([^&]*?)(?:&|$)/.exec(location.href); - if (match !== null) { - id = match[1]; - } - - match = /[&?]parent=(\d+)(?:&|$)/.exec(location.href); - if (match !== null) { - parentFrameId = parseInt(match[1], 10); - } - } + const initializationData = window.frontendInitializationData; + const isNested = (initializationData !== null && typeof initializationData === 'object'); + const {id, parentFrameId} = initializationData || {}; const popup = isNested ? new PopupProxy(id, parentFrameId) : PopupProxyHost.instance.createPopup(null); const frontend = new Frontend(popup); diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index 7df4e4e2..ad235cc6 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -17,20 +17,23 @@ */ -async function popupNestedSetup() { +let popupNestedInitialized = false; + +async function popupNestedInitialize(id, depth, parentFrameId) { + if (popupNestedInitialized) { + return; + } + popupNestedInitialized = true; + const options = await apiOptionsGet(); const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth; - let depth = null; - const match = /[&?]depth=([^&]*?)(?:&|$)/.exec(location.href); - if (match !== null) { - depth = parseInt(match[1], 10); - } - if (!(typeof popupNestingMaxDepth === 'number' && typeof depth === 'number' && depth < popupNestingMaxDepth)) { return; } + window.frontendInitializationData = {id, depth, parentFrameId}; + const scriptSrcs = [ '/fg/js/frontend-api-sender.js', '/fg/js/popup.js', @@ -44,5 +47,3 @@ async function popupNestedSetup() { document.body.appendChild(script); } } - -popupNestedSetup(); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 24b5684d..ab1dbbed 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -29,6 +29,7 @@ class Popup { this.container.id = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); this.container.addEventListener('scroll', e => e.stopPropagation()); + this.container.setAttribute('src', chrome.extension.getURL('/fg/float.html')); this.container.style.width = '0px'; this.container.style.height = '0px'; this.injectPromise = null; @@ -53,9 +54,13 @@ class Popup { } return new Promise((resolve) => { - const parent = (typeof this.frameId === 'number' ? this.frameId : ''); - this.container.setAttribute('src', chrome.extension.getURL(`/fg/float.html?id=${this.id}&depth=${this.depth}&parent=${parent}`)); + const parentFrameId = (typeof this.frameId === 'number' ? this.frameId : null); this.container.addEventListener('load', () => { + this.invokeApi('popupNestedInitialize', { + id: this.id, + depth: this.depth, + parentFrameId + }); this.invokeApi('setOptions', { general: { customPopupCss: options.general.customPopupCss From 71471d08e56f1a2431e67d0439724cc52a1ea73e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 25 Aug 2019 21:13:17 -0400 Subject: [PATCH 06/15] Add option to enable/disable scanning of source expressions in popups --- ext/bg/js/options.js | 1 + ext/bg/js/settings.js | 2 + ext/bg/settings.html | 4 ++ ext/fg/js/frontend.js | 86 +++++++++++++++++++++++++++++++++++---- ext/fg/js/popup-nested.js | 4 +- 5 files changed, 89 insertions(+), 8 deletions(-) diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 1b2d5e1a..df95aae9 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -221,6 +221,7 @@ function optionsSetDefaults(options) { modifier: 'shift', deepDomScan: false, popupNestingMaxDepth: 0, + enableOnPopupExpressions: false, enableOnSearchPage: true }, diff --git a/ext/bg/js/settings.js b/ext/bg/js/settings.js index 5472c7bd..83f4528c 100644 --- a/ext/bg/js/settings.js +++ b/ext/bg/js/settings.js @@ -48,6 +48,7 @@ async function formRead() { optionsNew.scanning.alphanumeric = $('#search-alphanumeric').prop('checked'); optionsNew.scanning.autoHideResults = $('#auto-hide-results').prop('checked'); optionsNew.scanning.deepDomScan = $('#deep-dom-scan').prop('checked'); + optionsNew.scanning.enableOnPopupExpressions = $('#enable-scanning-of-popup-expressions').prop('checked'); optionsNew.scanning.enableOnSearchPage = $('#enable-scanning-on-search-page').prop('checked'); optionsNew.scanning.delay = parseInt($('#scan-delay').val(), 10); optionsNew.scanning.length = parseInt($('#scan-length').val(), 10); @@ -191,6 +192,7 @@ async function onReady() { $('#search-alphanumeric').prop('checked', options.scanning.alphanumeric); $('#auto-hide-results').prop('checked', options.scanning.autoHideResults); $('#deep-dom-scan').prop('checked', options.scanning.deepDomScan); + $('#enable-scanning-of-popup-expressions').prop('checked', options.scanning.enableOnPopupExpressions); $('#enable-scanning-on-search-page').prop('checked', options.scanning.enableOnSearchPage); $('#scan-delay').val(options.scanning.delay); $('#scan-length').val(options.scanning.length); diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 0005efa6..85b7ee5f 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -192,6 +192,10 @@ +
+ +
+
diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 3605dffd..f59ea318 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -18,7 +18,7 @@ class Frontend { - constructor(popup) { + constructor(popup, ignoreNodes) { this.popup = popup; this.popupTimer = null; this.mouseDownLeft = false; @@ -26,6 +26,7 @@ class Frontend { this.textSourceLast = null; this.pendingLookup = false; this.options = null; + this.ignoreNodes = (Array.isArray(ignoreNodes) && ignoreNodes.length > 0 ? ignoreNodes.join(',') : null); this.primaryTouchIdentifier = null; this.contextMenuChecking = false; @@ -39,10 +40,10 @@ class Frontend { static create() { const initializationData = window.frontendInitializationData; const isNested = (initializationData !== null && typeof initializationData === 'object'); - const {id, parentFrameId} = initializationData || {}; + const {id, parentFrameId, ignoreNodes} = isNested ? initializationData : {}; const popup = isNested ? new PopupProxy(id, parentFrameId) : PopupProxyHost.instance.createPopup(null); - const frontend = new Frontend(popup); + const frontend = new Frontend(popup, ignoreNodes); frontend.prepare(); return frontend; } @@ -337,9 +338,14 @@ class Frontend { } async searchTerms(textSource, focus) { - textSource.setEndOffset(this.options.scanning.length); + this.setTextSourceScanLength(textSource, this.options.scanning.length); - const {definitions, length} = await apiTermsFind(textSource.text()); + const searchText = textSource.text(); + if (searchText.length === 0) { + return; + } + + const {definitions, length} = await apiTermsFind(searchText); if (definitions.length === 0) { return false; } @@ -365,9 +371,14 @@ class Frontend { } async searchKanji(textSource, focus) { - textSource.setEndOffset(1); + this.setTextSourceScanLength(textSource, 1); - const definitions = await apiKanjiFind(textSource.text()); + const searchText = textSource.text(); + if (searchText.length === 0) { + return; + } + + const definitions = await apiKanjiFind(searchText); if (definitions.length === 0) { return false; } @@ -493,6 +504,67 @@ class Frontend { } return false; } + + setTextSourceScanLength(textSource, length) { + textSource.setEndOffset(length); + if (this.ignoreNodes === null || !textSource.range) { + return; + } + + length = textSource.text().length; + while (textSource.range && length > 0) { + const nodes = Frontend.getNodesInRange(textSource.range); + if (Frontend.isValidScanningNodeList(nodes, this.ignoreNodes)) { + break; + } + --length; + textSource.setEndOffset(length); + } + } + + static getNodesInRange(range) { + const end = range.endContainer; + const nodes = []; + for (let node = range.startContainer; node !== null; node = Frontend.getNextNode(node)) { + nodes.push(node); + if (node === end) { break; } + } + return nodes; + } + + static getNextNode(node) { + let next = node.firstChild; + if (next === null) { + while (true) { + next = node.nextSibling; + if (next !== null) { break; } + + next = node.parentNode; + if (node === null) { break; } + + node = next; + } + } + return next; + } + + static isValidScanningNodeList(nodeList, selector) { + for (const node of nodeList) { + if (!Frontend.isValidScanningNode(node, selector)) { + return false; + } + } + return true; + } + + static isValidScanningNode(node, selector) { + for (; node !== null; node = node.parentNode) { + if (node.nodeType === Node.ELEMENT_NODE) { + return !node.matches(selector); + } + } + return true; + } } window.yomichan_frontend = Frontend.create(); diff --git a/ext/fg/js/popup-nested.js b/ext/fg/js/popup-nested.js index ad235cc6..e0376bb2 100644 --- a/ext/fg/js/popup-nested.js +++ b/ext/fg/js/popup-nested.js @@ -32,7 +32,9 @@ async function popupNestedInitialize(id, depth, parentFrameId) { return; } - window.frontendInitializationData = {id, depth, parentFrameId}; + const ignoreNodes = options.scanning.enableOnPopupExpressions ? [] : [ '.expression', '.expression *' ]; + + window.frontendInitializationData = {id, depth, parentFrameId, ignoreNodes}; const scriptSrcs = [ '/fg/js/frontend-api-sender.js', From 87ff5cb19bc78244faafcfcb54d8428b2ed978f6 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Wed, 28 Aug 2019 01:08:45 +0300 Subject: [PATCH 07/15] make containsPoint async everywhere --- ext/fg/js/frontend.js | 2 +- ext/fg/js/popup-proxy-host.js | 2 +- ext/fg/js/popup-proxy.js | 10 +--------- ext/fg/js/popup.js | 2 +- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index f59ea318..1dff6fb9 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -294,7 +294,7 @@ class Frontend { async searchAt(point, type) { if ( this.pendingLookup || - (this.popup.containsPointIsAsync() ? await this.popup.containsPointAsync(point) : this.popup.containsPoint(point)) + await this.popup.containsPoint(point) ) { return; } diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index cdd1d02c..1048f410 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -110,7 +110,7 @@ class PopupProxyHost { async containsPoint(id, point) { const popup = this.getPopup(id); - return popup.containsPointIsAsync() ? await popup.containsPointAsync(point) : popup.containsPoint(point); + return await popup.containsPoint(point); } async termsShow(id, elementRect, definitions, options, context) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index bbf6a2cf..112b998e 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -69,21 +69,13 @@ class PopupProxy { return await this.invokeHostApi('setVisible', {id, visible}); } - containsPoint() { - throw 'Non-async function not supported'; - } - - async containsPointAsync(point) { + async containsPoint(point) { if (this.id === null) { return false; } return await this.invokeHostApi('containsPoint', {id: this.id, point}); } - containsPointIsAsync() { - return true; - } - async termsShow(elementRect, definitions, options, context) { const id = await this.getPopupId(); elementRect = PopupProxy.DOMRectToJson(elementRect); diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index ab1dbbed..6b757472 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -248,7 +248,7 @@ class Popup { } } - containsPoint(point) { + async containsPoint(point) { if (!this.isVisible()) { return false; } From e4d302e786cdb052e5ab7311ca65abb0b49d56d0 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 31 Aug 2019 16:11:35 +0300 Subject: [PATCH 08/15] focus parent popup on hide instead of blurring --- ext/fg/js/popup.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 6b757472..c91764c4 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -207,7 +207,7 @@ class Popup { hide() { this.hideContainer(); - this.container.blur(); + this.focusParent(); this.hideChildren(); } @@ -248,6 +248,14 @@ class Popup { } } + focusParent() { + if (this.parent && this.parent.container) { + this.parent.container.focus(); + } else { + this.container.blur(); + } + } + async containsPoint(point) { if (!this.isVisible()) { return false; From 3d28a3a405273d8416ddc8976aaaff0c90ac057d Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 31 Aug 2019 16:35:25 +0300 Subject: [PATCH 09/15] prevent closing new child popup on parent mouseout --- ext/fg/js/frontend.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index 1dff6fb9..a31bdf79 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -56,6 +56,7 @@ class Frontend { window.addEventListener('mousedown', this.onMouseDown.bind(this)); window.addEventListener('mousemove', this.onMouseMove.bind(this)); window.addEventListener('mouseover', this.onMouseOver.bind(this)); + window.addEventListener('mouseout', this.onMouseOut.bind(this)); window.addEventListener('mouseup', this.onMouseUp.bind(this)); window.addEventListener('resize', this.onResize.bind(this)); @@ -149,6 +150,10 @@ class Frontend { } } + onMouseOut(e) { + this.popupTimerClear(); + } + onFrameMessage(e) { const handlers = { popupClose: () => { From 834f14e808526da958b26accd0d18056a7f4415c Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 31 Aug 2019 16:37:17 +0300 Subject: [PATCH 10/15] simplified if statement back to single line --- ext/fg/js/frontend.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index a31bdf79..eea77d2a 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -297,10 +297,7 @@ class Frontend { } async searchAt(point, type) { - if ( - this.pendingLookup || - await this.popup.containsPoint(point) - ) { + if (this.pendingLookup || await this.popup.containsPoint(point)) { return; } From 97be029deee4ec34ad2e90081af15d573dede0cd Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sat, 31 Aug 2019 18:01:38 +0300 Subject: [PATCH 11/15] focus the site when closing multi-level popups --- ext/fg/js/popup.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index c91764c4..61a5e4d0 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -206,9 +206,9 @@ class Popup { } hide() { + this.hideChildren(); this.hideContainer(); this.focusParent(); - this.hideChildren(); } hideChildren() { @@ -221,7 +221,7 @@ class Popup { const target = targets.shift(); if (target.isContainerHidden()) { continue; } - target.hideContainer(); + target.hide(); for (const child of target.children) { targets.push(child); } @@ -249,10 +249,9 @@ class Popup { } focusParent() { + this.container.blur(); if (this.parent && this.parent.container) { this.parent.container.focus(); - } else { - this.container.blur(); } } From 2df9a7f97794df46aac70a23bb2b62dbd2752a5a Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 1 Sep 2019 19:47:33 +0300 Subject: [PATCH 12/15] change Popup.children to Popup.child --- ext/fg/js/popup-proxy-host.js | 2 +- ext/fg/js/popup-proxy.js | 2 +- ext/fg/js/popup.js | 18 ++++-------------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 1048f410..fa61aeb4 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -57,7 +57,7 @@ class PopupProxyHost { const popup = new Popup(id, depth, this.frameIdPromise); if (parent !== null) { popup.parent = parent; - parent.children.push(popup); + parent.child = popup; } this.popups[id] = popup; return popup; diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 112b998e..f6295079 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -24,7 +24,7 @@ class PopupProxy { this.id = null; this.idPromise = null; this.parent = null; - this.children = []; + this.child = null; this.depth = 0; this.container = null; diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 61a5e4d0..3bc0b6f8 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -24,7 +24,7 @@ class Popup { this.frameIdPromise = frameIdPromise; this.frameId = null; this.parent = null; - this.children = []; + this.child = null; this.container = document.createElement('iframe'); this.container.id = 'yomichan-float'; this.container.addEventListener('mousedown', e => e.stopPropagation()); @@ -212,19 +212,9 @@ class Popup { } hideChildren() { - if (this.children.length === 0) { - return; - } - - const targets = this.children.slice(0); - while (targets.length > 0) { - const target = targets.shift(); - if (target.isContainerHidden()) { continue; } - - target.hide(); - for (const child of target.children) { - targets.push(child); - } + // recursively hides all children + if (this.child && !this.child.isContainerHidden()) { + this.child.hide(); } } From f4b81eff3054e4277e97d06290535bfecc9b9cc1 Mon Sep 17 00:00:00 2001 From: siikamiika Date: Sun, 1 Sep 2019 22:34:37 +0300 Subject: [PATCH 13/15] fix parent focus issues for Firefox and Chrome --- ext/fg/js/popup.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 3bc0b6f8..df7dc6b5 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -239,9 +239,15 @@ class Popup { } focusParent() { - this.container.blur(); if (this.parent && this.parent.container) { - this.parent.container.focus(); + // Chrome doesn't like focusing iframe without contentWindow. + this.parent.container.contentWindow.focus(); + } else { + // Firefox doesn't like focusing window without first blurring the iframe. + // this.container.contentWindow.blur() doesn't work on Firefox for some reason. + this.container.blur(); + // This is needed for Chrome. + window.focus(); } } From fad53324885c971a1ab1aeee3da5dd60dc5058df Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 1 Sep 2019 10:28:50 -0400 Subject: [PATCH 14/15] Move static DOM scanning functions into TextSourceRange --- ext/fg/js/frontend.js | 48 ++----------------------------------------- ext/fg/js/source.js | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/ext/fg/js/frontend.js b/ext/fg/js/frontend.js index eea77d2a..b70bf036 100644 --- a/ext/fg/js/frontend.js +++ b/ext/fg/js/frontend.js @@ -515,58 +515,14 @@ class Frontend { length = textSource.text().length; while (textSource.range && length > 0) { - const nodes = Frontend.getNodesInRange(textSource.range); - if (Frontend.isValidScanningNodeList(nodes, this.ignoreNodes)) { + const nodes = TextSourceRange.getNodesInRange(textSource.range); + if (!TextSourceRange.anyNodeMatchesSelector(nodes, this.ignoreNodes)) { break; } --length; textSource.setEndOffset(length); } } - - static getNodesInRange(range) { - const end = range.endContainer; - const nodes = []; - for (let node = range.startContainer; node !== null; node = Frontend.getNextNode(node)) { - nodes.push(node); - if (node === end) { break; } - } - return nodes; - } - - static getNextNode(node) { - let next = node.firstChild; - if (next === null) { - while (true) { - next = node.nextSibling; - if (next !== null) { break; } - - next = node.parentNode; - if (node === null) { break; } - - node = next; - } - } - return next; - } - - static isValidScanningNodeList(nodeList, selector) { - for (const node of nodeList) { - if (!Frontend.isValidScanningNode(node, selector)) { - return false; - } - } - return true; - } - - static isValidScanningNode(node, selector) { - for (; node !== null; node = node.parentNode) { - if (node.nodeType === Node.ELEMENT_NODE) { - return !node.matches(selector); - } - } - return true; - } } window.yomichan_frontend = Frontend.create(); diff --git a/ext/fg/js/source.js b/ext/fg/js/source.js index e724488d..385b5001 100644 --- a/ext/fg/js/source.js +++ b/ext/fg/js/source.js @@ -232,6 +232,50 @@ class TextSourceRange { const writingMode = style.writingMode; return typeof writingMode === 'string' ? writingMode : 'horizontal-tb'; } + + static getNodesInRange(range) { + const end = range.endContainer; + const nodes = []; + for (let node = range.startContainer; node !== null; node = TextSourceRange.getNextNode(node)) { + nodes.push(node); + if (node === end) { break; } + } + return nodes; + } + + static getNextNode(node) { + let next = node.firstChild; + if (next === null) { + while (true) { + next = node.nextSibling; + if (next !== null) { break; } + + next = node.parentNode; + if (node === null) { break; } + + node = next; + } + } + return next; + } + + static anyNodeMatchesSelector(nodeList, selector) { + for (const node of nodeList) { + if (TextSourceRange.nodeMatchesSelector(node, selector)) { + return true; + } + } + return false; + } + + static nodeMatchesSelector(node, selector) { + for (; node !== null; node = node.parentNode) { + if (node.nodeType === Node.ELEMENT_NODE) { + return node.matches(selector); + } + } + return false; + } } From 9028b55774f788f0b61acadb8d3ba85b2bfab34a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 3 Sep 2019 18:55:55 -0400 Subject: [PATCH 15/15] Fix nested popups closing when the mouse leaves the parent's rect --- ext/fg/js/popup.js | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index df7dc6b5..1b15977b 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -251,26 +251,13 @@ class Popup { } } - async containsPoint(point) { - if (!this.isVisible()) { - return false; + async containsPoint({x, y}) { + for (let popup = this; popup !== null && popup.isVisible(); popup = popup.child) { + const rect = popup.container.getBoundingClientRect(); + if (x >= rect.left && y >= rect.top && x < rect.right && y < rect.bottom) { + return true; + } } - - const rect = this.container.getBoundingClientRect(); - const contained = - point.x >= rect.left && - point.y >= rect.top && - point.x < rect.right && - point.y < rect.bottom; - - return contained; - } - - async containsPointAsync(point) { - return containsPoint(point); - } - - containsPointIsAsync() { return false; }