diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json index d221d994..981213c8 100644 --- a/dev/data/manifest-variants.json +++ b/dev/data/manifest-variants.json @@ -53,6 +53,7 @@ "fg/js/text-source-range.js", "fg/js/text-source-element.js", "fg/js/popup-factory.js", + "fg/js/frame-ancestry-handler.js", "fg/js/frame-offset-forwarder.js", "fg/js/popup-proxy.js", "fg/js/popup-window.js", diff --git a/ext/bg/popup-preview.html b/ext/bg/popup-preview.html index d0bf77d3..c1ef7ca4 100644 --- a/ext/bg/popup-preview.html +++ b/ext/bg/popup-preview.html @@ -52,6 +52,7 @@ + diff --git a/ext/fg/js/frame-ancestry-handler.js b/ext/fg/js/frame-ancestry-handler.js index 31ee956b..b1ed7114 100644 --- a/ext/fg/js/frame-ancestry-handler.js +++ b/ext/fg/js/frame-ancestry-handler.js @@ -23,6 +23,7 @@ * This class is used to return the ancestor frame IDs for the current frame. * This is a workaround to using the `webNavigation.getAllFrames` API, which * would require an additional permission that is otherwise unnecessary. + * It is also used to track the correlation between child frame elements and their IDs. */ class FrameAncestryHandler { /** @@ -54,6 +55,14 @@ class FrameAncestryHandler { this._isPrepared = true; } + /** + * Returns whether or not this frame is the root frame in the tab. + * @returns `true` if it is the root, otherwise `false`. + */ + isRootFrame() { + return (window === window.parent); + } + /** * Gets the frame ancestry information for the current frame. If the frame is the * root frame, an empty array is returned. Otherwise, an array of frame IDs is returned, @@ -68,6 +77,26 @@ class FrameAncestryHandler { return await this._getFrameAncestryInfoPromise; } + /** + * Gets the frame element of a child frame given a frame ID. + * For this function to work, the `getFrameAncestryInfo` function needs to have + * been invoked previously. + * @param frameId The frame ID of the child frame to get. + * @returns The element corresponding to the frame with ID `frameId`, otherwise `null`. + */ + getChildFrameElement(frameId) { + const frameInfo = this._childFrameMap.get(frameId); + if (typeof frameInfo === 'undefined') { return null; } + + let {frameElement} = frameInfo; + if (typeof frameElement === 'undefined') { + frameElement = this._findFrameElementWithContentWindow(frameInfo.window); + frameInfo.frameElement = frameElement; + } + + return frameElement; + } + // Private _getFrameAncestryInfo(timeout=5000) { @@ -166,7 +195,7 @@ class FrameAncestryHandler { } if (!this._childFrameMap.has(childFrameId)) { - this._childFrameMap.set(childFrameId, {window: source}); + this._childFrameMap.set(childFrameId, {window: source, frameElement: void 0}); } if (more) { @@ -192,4 +221,49 @@ class FrameAncestryHandler { Math.floor(value) === value ); } + + _findFrameElementWithContentWindow(contentWindow) { + // Check frameElement, for non-null same-origin frames + try { + const {frameElement} = contentWindow; + if (frameElement !== null) { return frameElement; } + } catch (e) { + // NOP + } + + // Check frames + const frameTypes = ['iframe', 'frame', 'embed']; + for (const frameType of frameTypes) { + for (const frame of document.getElementsByTagName(frameType)) { + if (frame.contentWindow === contentWindow) { + return frame; + } + } + } + + // Check for shadow roots + const rootElements = [document.documentElement]; + while (rootElements.length > 0) { + const rootElement = rootElements.shift(); + const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT); + while (walker.nextNode()) { + const element = walker.currentNode; + + if (element.contentWindow === contentWindow) { + return element; + } + + const shadowRoot = ( + element.shadowRoot || + element.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions + ); + if (shadowRoot) { + rootElements.push(shadowRoot); + } + } + } + + // Not found + return null; + } } diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js index b5d5424c..0a0b4a18 100644 --- a/ext/fg/js/frame-offset-forwarder.js +++ b/ext/fg/js/frame-offset-forwarder.js @@ -16,162 +16,55 @@ */ /* global + * FrameAncestryHandler * api */ class FrameOffsetForwarder { constructor(frameId) { this._frameId = frameId; - this._isPrepared = false; - this._cacheMaxSize = 1000; - this._frameCache = new Set(); - this._unreachableContentWindowCache = new Set(); - this._windowMessageHandlers = new Map([ - ['getFrameOffset', this._onMessageGetFrameOffset.bind(this)] - ]); + this._frameAncestryHandler = new FrameAncestryHandler(frameId); } prepare() { - if (this._isPrepared) { return; } - window.addEventListener('message', this._onMessage.bind(this), false); - this._isPrepared = true; + this._frameAncestryHandler.prepare(); + api.crossFrame.registerHandlers([ + ['FrameOffsetForwarder.getChildFrameRect', {async: false, handler: this._onMessageGetChildFrameRect.bind(this)}] + ]); } async getOffset() { - if (window === window.parent) { + if (this._frameAncestryHandler.isRootFrame()) { return [0, 0]; } - const uniqueId = generateId(16); + const ancestorFrameIds = await this._frameAncestryHandler.getFrameAncestryInfo(); - const frameOffsetPromise = yomichan.getTemporaryListenerResult( - chrome.runtime.onMessage, - ({action, params}, {resolve}) => { - if (action === 'frameOffset' && isObject(params) && params.uniqueId === uniqueId) { - resolve(params); - } - }, - 5000 - ); + let childFrameId = this._frameId; + const promises = []; + for (const frameId of ancestorFrameIds) { + promises.push(api.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId})); + childFrameId = frameId; + } - this._getFrameOffsetParent([0, 0], uniqueId, this._frameId); + const results = await Promise.all(promises); - const {offset} = await frameOffsetPromise; - return offset; + let xOffset = 0; + let yOffset = 0; + for (const {x, y} of results) { + xOffset += x; + yOffset += y; + } + return [xOffset, yOffset]; } // Private - _onMessage(event) { - const data = event.data; - if (data === null || typeof data !== 'object') { return; } + _onMessageGetChildFrameRect({frameId}) { + const frameElement = this._frameAncestryHandler.getChildFrameElement(frameId); + if (frameElement === null) { return null; } - try { - const {action, params} = event.data; - const handler = this._windowMessageHandlers.get(action); - if (typeof handler !== 'function') { return; } - handler(params, event); - } catch (e) { - // NOP - } - } - - _onMessageGetFrameOffset({offset, uniqueId, frameId}, e) { - let sourceFrame = null; - if (!this._unreachableContentWindowCache.has(e.source)) { - sourceFrame = this._findFrameWithContentWindow(e.source); - } - if (sourceFrame === null) { - // closed shadow root etc. - this._addToCache(this._unreachableContentWindowCache, e.source); - this._replyFrameOffset(null, uniqueId, frameId); - return; - } - - const [forwardedX, forwardedY] = offset; - const {x, y} = sourceFrame.getBoundingClientRect(); - offset = [forwardedX + x, forwardedY + y]; - - if (window === window.parent) { - this._replyFrameOffset(offset, uniqueId, frameId); - } else { - this._getFrameOffsetParent(offset, uniqueId, frameId); - } - } - - _findFrameWithContentWindow(contentWindow) { - const ELEMENT_NODE = Node.ELEMENT_NODE; - for (const elements of this._getFrameElementSources()) { - while (elements.length > 0) { - const element = elements.shift(); - if (element.contentWindow === contentWindow) { - this._addToCache(this._frameCache, element); - return element; - } - - const shadowRoot = ( - element.shadowRoot || - element.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions - ); - if (shadowRoot) { - for (const child of shadowRoot.children) { - if (child.nodeType === ELEMENT_NODE) { - elements.push(child); - } - } - } - - for (const child of element.children) { - if (child.nodeType === ELEMENT_NODE) { - elements.push(child); - } - } - } - } - - return null; - } - - *_getFrameElementSources() { - const frameCache = []; - for (const frame of this._frameCache) { - // removed from DOM - if (!frame.isConnected) { - this._frameCache.delete(frame); - continue; - } - frameCache.push(frame); - } - yield frameCache; - // will contain duplicates, but frame elements are cheap to handle - yield [...document.querySelectorAll('frame,iframe')]; - yield [document.documentElement]; - } - - _addToCache(cache, value) { - let freeSlots = this._cacheMaxSize - cache.size; - if (freeSlots <= 0) { - for (const cachedValue of cache) { - cache.delete(cachedValue); - ++freeSlots; - if (freeSlots > 0) { break; } - } - } - cache.add(value); - } - - _getFrameOffsetParent(offset, uniqueId, frameId) { - window.parent.postMessage({ - action: 'getFrameOffset', - params: { - offset, - uniqueId, - frameId - } - }, '*'); - } - - _replyFrameOffset(offset, uniqueId, frameId) { - api.sendMessageToFrame(frameId, 'frameOffset', {offset, uniqueId}); + const {x, y, width, height} = frameElement.getBoundingClientRect(); + return {x, y, width, height}; } } diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 6d6c7fb9..bb037705 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -34,7 +34,7 @@ class PopupProxy extends EventDispatcher { this._ownerFrameId = ownerFrameId; this._frameOffsetForwarder = frameOffsetForwarder; - this._frameOffset = null; + this._frameOffset = [0, 0]; this._frameOffsetPromise = null; this._frameOffsetUpdatedAt = null; this._frameOffsetExpireTimeout = 1000; @@ -194,7 +194,12 @@ class PopupProxy extends EventDispatcher { async _updateFrameOffsetInner(now) { this._frameOffsetPromise = this._frameOffsetForwarder.getOffset(); try { - const offset = await this._frameOffsetPromise; + let offset = null; + try { + offset = await this._frameOffsetPromise; + } catch (e) { + // NOP + } this._frameOffset = offset !== null ? offset : [0, 0]; if (offset === null) { this.trigger('offsetNotFound'); diff --git a/ext/manifest.json b/ext/manifest.json index 54df2a89..238fabe3 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -52,6 +52,7 @@ "fg/js/text-source-range.js", "fg/js/text-source-element.js", "fg/js/popup-factory.js", + "fg/js/frame-ancestry-handler.js", "fg/js/frame-offset-forwarder.js", "fg/js/popup-proxy.js", "fg/js/popup-window.js", diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index c1044872..99ccfa85 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -1592,6 +1592,7 @@ class Display extends EventDispatcher { '/fg/js/popup-proxy.js', '/fg/js/popup-window.js', '/fg/js/popup-factory.js', + '/fg/js/frame-ancestry-handler.js', '/fg/js/frame-offset-forwarder.js', '/fg/js/frontend.js' ]);