diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js index 4dc79943..487dda90 100644 --- a/ext/fg/js/popup-proxy-host.js +++ b/ext/fg/js/popup-proxy-host.js @@ -19,6 +19,7 @@ /* global * FrontendApiReceiver * Popup + * apiForward * apiFrameInformationGet */ @@ -48,6 +49,12 @@ class PopupProxyHost { ['clearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)], ['setContentScale', this._onApiSetContentScale.bind(this)] ])); + + this._windowMessageHandlers = new Map([ + ['getIframeOffset', ({offset, uniqueId}, e) => { return this._onGetIframeOffset(offset, uniqueId, e); }] + ]); + + window.addEventListener('message', this.onMessage.bind(this), false); } getOrCreatePopup(id=null, parentId=null, depth=null) { @@ -95,7 +102,7 @@ class PopupProxyHost { return popup; } - // Message handlers + // API message handlers async _onApiGetOrCreatePopup({id, parentId}) { const popup = this.getOrCreatePopup(id, parentId); @@ -152,6 +159,30 @@ class PopupProxyHost { return popup.setContentScale(scale); } + // Window message handlers + + onMessage(e) { + const {action, params} = e.data; + const handler = this._windowMessageHandlers.get(action); + if (typeof handler !== 'function') { return; } + handler(params, e); + } + + _onGetIframeOffset(offset, uniqueId, e) { + let sourceIframe = null; + for (const iframe of document.querySelectorAll('iframe:not(.yomichan-float)')) { + if (iframe.contentWindow !== e.source) { continue; } + sourceIframe = iframe; + break; + } + if (sourceIframe === null) { return; } + + const [forwardedX, forwardedY] = offset; + const {x, y} = sourceIframe.getBoundingClientRect(); + offset = [forwardedX + x, forwardedY + y]; + apiForward('iframeOffset', {offset, uniqueId}); + } + // Private functions _getPopup(id) { diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 997b1317..c1ee76ce 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -29,6 +29,12 @@ class PopupProxy { this._depth = depth; this._url = url; this._apiSender = new FrontendApiSender(); + + this._windowMessageHandlers = new Map([ + ['getIframeOffset', ({offset, uniqueId}, e) => { return this._onGetIframeOffset(offset, uniqueId, e); }] + ]); + + window.addEventListener('message', this.onMessage.bind(this), false); } // Public properties @@ -83,12 +89,19 @@ class PopupProxy { if (this._id === null) { return false; } + if (this._depth === 0) { + [x, y] = await PopupProxy._convertIframePointToRootPagePoint(x, y); + } return await this._invokeHostApi('containsPoint', {id: this._id, x, y}); } async showContent(elementRect, writingMode, type=null, details=null) { const id = await this._getPopupId(); - elementRect = PopupProxy._convertDOMRectToJson(elementRect); + let {x, y, width, height} = PopupProxy._convertDOMRectToJson(elementRect); + if (this._depth === 0) { + [x, y] = await PopupProxy._convertIframePointToRootPagePoint(x, y); + elementRect = {x, y, width, height}; + } return await this._invokeHostApi('showContent', {id, elementRect, writingMode, type, details}); } @@ -109,6 +122,31 @@ class PopupProxy { this._invokeHostApi('setContentScale', {id, scale}); } + // Window message handlers + + onMessage(e) { + const {action, params} = e.data; + const handler = this._windowMessageHandlers.get(action); + if (typeof handler !== 'function') { return; } + handler(params, e); + } + + _onGetIframeOffset(offset, uniqueId, e) { + let sourceIframe = null; + for (const iframe of document.querySelectorAll('iframe:not(.yomichan-float)')) { + if (iframe.contentWindow !== e.source) { continue; } + sourceIframe = iframe; + break; + } + if (sourceIframe === null) { return; } + + const [forwardedX, forwardedY] = offset; + const {x, y} = sourceIframe.getBoundingClientRect(); + offset = [forwardedX + x, forwardedY + y]; + window.parent.postMessage({action: 'getIframeOffset', params: {offset, uniqueId}}, '*'); + } + + // Private _getPopupId() { @@ -131,6 +169,35 @@ class PopupProxy { return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`); } + static async _convertIframePointToRootPagePoint(x, y) { + const uniqueId = yomichan.generateId(16); + + let frameOffsetResolve = null; + const frameOffsetPromise = new Promise((resolve) => (frameOffsetResolve = resolve)); + + const runtimeMessageCallback = ({action, params}, sender, callback) => { + if (action === 'iframeOffset' && isObject(params) && params.uniqueId === uniqueId) { + chrome.runtime.onMessage.removeListener(runtimeMessageCallback); + callback(); + frameOffsetResolve(params); + return false; + } + }; + chrome.runtime.onMessage.addListener(runtimeMessageCallback); + + window.parent.postMessage({ + action: 'getIframeOffset', + params: { + uniqueId, + offset: [x, y] + } + }, '*'); + + const {offset} = await frameOffsetPromise; + + return offset; + } + static _convertDOMRectToJson(domRect) { return { x: domRect.x,