Refactor FrameOffsetForwarder (#1353)
* Add getChildFrameElement to FrameAncestryHandler * Add isRootFrame * Initialize _frameOffset to [0, 0] * Update FrameOffsetForwarder implementation * Update documentation
This commit is contained in:
parent
3e5b30ff76
commit
73e91b3b62
@ -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",
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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",
|
||||
|
@ -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'
|
||||
]);
|
||||
|
Loading…
Reference in New Issue
Block a user