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"],