Refactor FrameOffsetForwarder (#1353)

* Add getChildFrameElement to FrameAncestryHandler

* Add isRootFrame

* Initialize _frameOffset to [0, 0]

* Update FrameOffsetForwarder implementation

* Update documentation
This commit is contained in:
toasted-nutbread 2021-02-08 17:52:56 -05:00 committed by GitHub
parent 3e5b30ff76
commit 73e91b3b62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 137 deletions

View File

@ -53,6 +53,7 @@
"fg/js/text-source-range.js", "fg/js/text-source-range.js",
"fg/js/text-source-element.js", "fg/js/text-source-element.js",
"fg/js/popup-factory.js", "fg/js/popup-factory.js",
"fg/js/frame-ancestry-handler.js",
"fg/js/frame-offset-forwarder.js", "fg/js/frame-offset-forwarder.js",
"fg/js/popup-proxy.js", "fg/js/popup-proxy.js",
"fg/js/popup-window.js", "fg/js/popup-window.js",

View File

@ -52,6 +52,7 @@
<script src="/fg/js/text-source-element.js"></script> <script src="/fg/js/text-source-element.js"></script>
<script src="/fg/js/popup-factory.js"></script> <script src="/fg/js/popup-factory.js"></script>
<script src="/fg/js/frontend.js"></script> <script src="/fg/js/frontend.js"></script>
<script src="/fg/js/frame-ancestry-handler.js"></script>
<script src="/fg/js/frame-offset-forwarder.js"></script> <script src="/fg/js/frame-offset-forwarder.js"></script>
<script src="/bg/js/settings/popup-preview-frame.js"></script> <script src="/bg/js/settings/popup-preview-frame.js"></script>

View File

@ -23,6 +23,7 @@
* This class is used to return the ancestor frame IDs for the current frame. * 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 * This is a workaround to using the `webNavigation.getAllFrames` API, which
* would require an additional permission that is otherwise unnecessary. * 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 { class FrameAncestryHandler {
/** /**
@ -54,6 +55,14 @@ class FrameAncestryHandler {
this._isPrepared = true; 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 * 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, * 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; 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 // Private
_getFrameAncestryInfo(timeout=5000) { _getFrameAncestryInfo(timeout=5000) {
@ -166,7 +195,7 @@ class FrameAncestryHandler {
} }
if (!this._childFrameMap.has(childFrameId)) { if (!this._childFrameMap.has(childFrameId)) {
this._childFrameMap.set(childFrameId, {window: source}); this._childFrameMap.set(childFrameId, {window: source, frameElement: void 0});
} }
if (more) { if (more) {
@ -192,4 +221,49 @@ class FrameAncestryHandler {
Math.floor(value) === value 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;
}
} }

View File

@ -16,162 +16,55 @@
*/ */
/* global /* global
* FrameAncestryHandler
* api * api
*/ */
class FrameOffsetForwarder { class FrameOffsetForwarder {
constructor(frameId) { constructor(frameId) {
this._frameId = frameId; this._frameId = frameId;
this._isPrepared = false; this._frameAncestryHandler = new FrameAncestryHandler(frameId);
this._cacheMaxSize = 1000;
this._frameCache = new Set();
this._unreachableContentWindowCache = new Set();
this._windowMessageHandlers = new Map([
['getFrameOffset', this._onMessageGetFrameOffset.bind(this)]
]);
} }
prepare() { prepare() {
if (this._isPrepared) { return; } this._frameAncestryHandler.prepare();
window.addEventListener('message', this._onMessage.bind(this), false); api.crossFrame.registerHandlers([
this._isPrepared = true; ['FrameOffsetForwarder.getChildFrameRect', {async: false, handler: this._onMessageGetChildFrameRect.bind(this)}]
]);
} }
async getOffset() { async getOffset() {
if (window === window.parent) { if (this._frameAncestryHandler.isRootFrame()) {
return [0, 0]; return [0, 0];
} }
const uniqueId = generateId(16); const ancestorFrameIds = await this._frameAncestryHandler.getFrameAncestryInfo();
const frameOffsetPromise = yomichan.getTemporaryListenerResult( let childFrameId = this._frameId;
chrome.runtime.onMessage, const promises = [];
({action, params}, {resolve}) => { for (const frameId of ancestorFrameIds) {
if (action === 'frameOffset' && isObject(params) && params.uniqueId === uniqueId) { promises.push(api.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId}));
resolve(params); childFrameId = frameId;
} }
},
5000
);
this._getFrameOffsetParent([0, 0], uniqueId, this._frameId); const results = await Promise.all(promises);
const {offset} = await frameOffsetPromise; let xOffset = 0;
return offset; let yOffset = 0;
for (const {x, y} of results) {
xOffset += x;
yOffset += y;
}
return [xOffset, yOffset];
} }
// Private // Private
_onMessage(event) { _onMessageGetChildFrameRect({frameId}) {
const data = event.data; const frameElement = this._frameAncestryHandler.getChildFrameElement(frameId);
if (data === null || typeof data !== 'object') { return; } if (frameElement === null) { return null; }
try { const {x, y, width, height} = frameElement.getBoundingClientRect();
const {action, params} = event.data; return {x, y, width, height};
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});
} }
} }

View File

@ -34,7 +34,7 @@ class PopupProxy extends EventDispatcher {
this._ownerFrameId = ownerFrameId; this._ownerFrameId = ownerFrameId;
this._frameOffsetForwarder = frameOffsetForwarder; this._frameOffsetForwarder = frameOffsetForwarder;
this._frameOffset = null; this._frameOffset = [0, 0];
this._frameOffsetPromise = null; this._frameOffsetPromise = null;
this._frameOffsetUpdatedAt = null; this._frameOffsetUpdatedAt = null;
this._frameOffsetExpireTimeout = 1000; this._frameOffsetExpireTimeout = 1000;
@ -194,7 +194,12 @@ class PopupProxy extends EventDispatcher {
async _updateFrameOffsetInner(now) { async _updateFrameOffsetInner(now) {
this._frameOffsetPromise = this._frameOffsetForwarder.getOffset(); this._frameOffsetPromise = this._frameOffsetForwarder.getOffset();
try { try {
const offset = await this._frameOffsetPromise; let offset = null;
try {
offset = await this._frameOffsetPromise;
} catch (e) {
// NOP
}
this._frameOffset = offset !== null ? offset : [0, 0]; this._frameOffset = offset !== null ? offset : [0, 0];
if (offset === null) { if (offset === null) {
this.trigger('offsetNotFound'); this.trigger('offsetNotFound');

View File

@ -52,6 +52,7 @@
"fg/js/text-source-range.js", "fg/js/text-source-range.js",
"fg/js/text-source-element.js", "fg/js/text-source-element.js",
"fg/js/popup-factory.js", "fg/js/popup-factory.js",
"fg/js/frame-ancestry-handler.js",
"fg/js/frame-offset-forwarder.js", "fg/js/frame-offset-forwarder.js",
"fg/js/popup-proxy.js", "fg/js/popup-proxy.js",
"fg/js/popup-window.js", "fg/js/popup-window.js",

View File

@ -1592,6 +1592,7 @@ class Display extends EventDispatcher {
'/fg/js/popup-proxy.js', '/fg/js/popup-proxy.js',
'/fg/js/popup-window.js', '/fg/js/popup-window.js',
'/fg/js/popup-factory.js', '/fg/js/popup-factory.js',
'/fg/js/frame-ancestry-handler.js',
'/fg/js/frame-offset-forwarder.js', '/fg/js/frame-offset-forwarder.js',
'/fg/js/frontend.js' '/fg/js/frontend.js'
]); ]);