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

View File

@ -52,6 +52,7 @@
<script src="/fg/js/text-source-element.js"></script>
<script src="/fg/js/popup-factory.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="/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 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;
}
}

View File

@ -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);
let childFrameId = this._frameId;
const promises = [];
for (const frameId of ancestorFrameIds) {
promises.push(api.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId}));
childFrameId = frameId;
}
},
5000
);
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};
}
}

View File

@ -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');

View File

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

View File

@ -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'
]);