yomichan/ext/fg/js/frame-offset-forwarder.js
toasted-nutbread a02891e52c
Update copyright year (#1194)
* Update copyright year

* Remove redundant Author info
2021-01-01 14:50:41 -05:00

178 lines
5.5 KiB
JavaScript

/*
* Copyright (C) 2020-2021 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* global
* 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)]
]);
}
prepare() {
if (this._isPrepared) { return; }
window.addEventListener('message', this._onMessage.bind(this), false);
this._isPrepared = true;
}
async getOffset() {
if (window === window.parent) {
return [0, 0];
}
const uniqueId = generateId(16);
const frameOffsetPromise = yomichan.getTemporaryListenerResult(
chrome.runtime.onMessage,
({action, params}, {resolve}) => {
if (action === 'frameOffset' && isObject(params) && params.uniqueId === uniqueId) {
resolve(params);
}
},
5000
);
this._getFrameOffsetParent([0, 0], uniqueId, this._frameId);
const {offset} = await frameOffsetPromise;
return offset;
}
// Private
_onMessage(event) {
const data = event.data;
if (data === null || typeof data !== 'object') { return; }
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});
}
}