aboutsummaryrefslogtreecommitdiff
path: root/ext/fg/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2019-08-17 18:50:48 -0400
committertoasted-nutbread <toasted-nutbread@users.noreply.github.com>2019-09-02 19:31:42 -0400
commit5c4614f585648c2b835efc1d369e78918bc4f5ff (patch)
tree50f7aee35187192b7104752d953c18d52bcfba6c /ext/fg/js
parent4ac55da7dd5354e6c3495f04583352d0d863b7b6 (diff)
Add support for showing recursive popups
Diffstat (limited to 'ext/fg/js')
-rw-r--r--ext/fg/js/frontend-api-receiver.js62
-rw-r--r--ext/fg/js/frontend-api-sender.js125
-rw-r--r--ext/fg/js/frontend.js34
-rw-r--r--ext/fg/js/popup-proxy-host.js118
-rw-r--r--ext/fg/js/popup-proxy.js116
-rw-r--r--ext/fg/js/popup.js45
-rw-r--r--ext/fg/js/util.js6
7 files changed, 494 insertions, 12 deletions
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 <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+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 <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+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 <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+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 <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+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) {