Merge pull request #460 from siikamiika/iframe-popup-edge-cases

Iframe popup edge cases
This commit is contained in:
siikamiika 2020-04-18 23:39:21 +03:00 committed by GitHub
commit 7a03ce0194
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 248 additions and 22 deletions

View File

@ -23,6 +23,10 @@ class FrameOffsetForwarder {
constructor() { constructor() {
this._started = false; this._started = false;
this._cacheMaxSize = 1000;
this._frameCache = new Set();
this._unreachableContentWindowCache = new Set();
this._forwardFrameOffset = ( this._forwardFrameOffset = (
window !== window.parent ? window !== window.parent ?
this._forwardFrameOffsetParent.bind(this) : this._forwardFrameOffsetParent.bind(this) :
@ -74,12 +78,12 @@ class FrameOffsetForwarder {
_onGetFrameOffset(offset, uniqueId, e) { _onGetFrameOffset(offset, uniqueId, e) {
let sourceFrame = null; let sourceFrame = null;
for (const frame of document.querySelectorAll('frame, iframe:not(.yomichan-float)')) { if (!this._unreachableContentWindowCache.has(e.source)) {
if (frame.contentWindow !== e.source) { continue; } sourceFrame = this._findFrameWithContentWindow(e.source);
sourceFrame = frame;
break;
} }
if (sourceFrame === null) { if (sourceFrame === null) {
// closed shadow root etc.
this._addToCache(this._unreachableContentWindowCache, e.source);
this._forwardFrameOffsetOrigin(null, uniqueId); this._forwardFrameOffsetOrigin(null, uniqueId);
return; return;
} }
@ -91,6 +95,64 @@ class FrameOffsetForwarder {
this._forwardFrameOffset(offset, uniqueId); this._forwardFrameOffset(offset, uniqueId);
} }
_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;
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:not(.yomichan-float)')];
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);
}
_forwardFrameOffsetParent(offset, uniqueId) { _forwardFrameOffsetParent(offset, uniqueId) {
window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*'); window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*');
} }

View File

@ -16,6 +16,7 @@
*/ */
/* global /* global
* DOM
* FrameOffsetForwarder * FrameOffsetForwarder
* Frontend * Frontend
* PopupProxy * PopupProxy
@ -24,7 +25,7 @@
* apiOptionsGet * apiOptionsGet
*/ */
async function createIframePopupProxy(url, frameOffsetForwarder) { async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) {
const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( const rootPopupInformationPromise = yomichan.getTemporaryListenerResult(
chrome.runtime.onMessage, chrome.runtime.onMessage,
({action, params}, {resolve}) => { ({action, params}, {resolve}) => {
@ -38,7 +39,7 @@ async function createIframePopupProxy(url, frameOffsetForwarder) {
const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder);
const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset); const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset, setDisabled);
await popup.prepare(); await popup.prepare();
return popup; return popup;
@ -78,6 +79,13 @@ async function main() {
let frontendPreparePromise = null; let frontendPreparePromise = null;
let frameOffsetForwarder = null; let frameOffsetForwarder = null;
let iframePopupsInRootFrameAvailable = true;
const disableIframePopupsInRootFrame = () => {
iframePopupsInRootFrameAvailable = false;
applyOptions();
};
const applyOptions = async () => { const applyOptions = async () => {
const optionsContext = {depth: isSearchPage ? 0 : depth, url}; const optionsContext = {depth: isSearchPage ? 0 : depth, url};
const options = await apiOptionsGet(optionsContext); const options = await apiOptionsGet(optionsContext);
@ -88,8 +96,8 @@ async function main() {
} }
let popup; let popup;
if (isIframe && options.general.showIframePopupsInRootFrame) { if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) {
popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder); popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame);
popups.iframe = popup; popups.iframe = popup;
} else if (proxy) { } else if (proxy) {
popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url); popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url);
@ -117,6 +125,7 @@ async function main() {
}; };
yomichan.on('optionsUpdated', applyOptions); yomichan.on('optionsUpdated', applyOptions);
window.addEventListener('fullscreenchange', applyOptions, false);
await applyOptions(); await applyOptions();
} }

View File

@ -20,7 +20,7 @@
*/ */
class PopupProxy { class PopupProxy {
constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null) { constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null, setDisabled=null) {
this._parentId = parentId; this._parentId = parentId;
this._parentFrameId = parentFrameId; this._parentFrameId = parentFrameId;
this._id = id; this._id = id;
@ -28,6 +28,7 @@ class PopupProxy {
this._url = url; this._url = url;
this._apiSender = new FrontendApiSender(); this._apiSender = new FrontendApiSender();
this._getFrameOffset = getFrameOffset; this._getFrameOffset = getFrameOffset;
this._setDisabled = setDisabled;
this._frameOffset = null; this._frameOffset = null;
this._frameOffsetPromise = null; this._frameOffsetPromise = null;
@ -142,6 +143,10 @@ class PopupProxy {
try { try {
const offset = await this._frameOffsetPromise; const offset = await this._frameOffsetPromise;
this._frameOffset = offset !== null ? offset : [0, 0]; this._frameOffset = offset !== null ? offset : [0, 0];
if (offset === null && this._setDisabled !== null) {
this._setDisabled();
return;
}
this._frameOffsetUpdatedAt = now; this._frameOffsetUpdatedAt = now;
} catch (e) { } catch (e) {
logError(e); logError(e);

View File

@ -16,6 +16,7 @@
*/ */
/* global /* global
* DOM
* apiGetMessageToken * apiGetMessageToken
* apiInjectStylesheet * apiInjectStylesheet
*/ */
@ -271,7 +272,7 @@ class Popup {
} }
_onFullscreenChanged() { _onFullscreenChanged() {
const parent = (Popup._getFullscreenElement() || document.body || null); const parent = (DOM.getFullscreenElement() || document.body || null);
if (parent !== null && this._container.parentNode !== parent) { if (parent !== null && this._container.parentNode !== parent) {
parent.appendChild(this._container); parent.appendChild(this._container);
} }
@ -365,16 +366,6 @@ class Popup {
contentWindow.postMessage({action, params, token}, this._targetOrigin); contentWindow.postMessage({action, params, token}, this._targetOrigin);
} }
static _getFullscreenElement() {
return (
document.fullscreenElement ||
document.msFullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
null
);
}
static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) { static _getPositionForHorizontalText(elementRect, width, height, viewport, offsetScale, optionsGeneral) {
const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below'); const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below');
const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale; const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale;

View File

@ -71,7 +71,7 @@
"applications": { "applications": {
"gecko": { "gecko": {
"id": "alex@foosoft.net", "id": "alex@foosoft.net",
"strict_min_version": "52.0" "strict_min_version": "53.0"
} }
} }
} }

View File

@ -62,4 +62,14 @@ class DOM {
default: return false; default: return false;
} }
} }
static getFullscreenElement() {
return (
document.fullscreenElement ||
document.msFullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
null
);
}
} }

View File

@ -77,5 +77,22 @@ document.querySelector('#fullscreen-link1').addEventListener('click', () => togg
</script> </script>
</div> </div>
<div class="test">
<div class="description">&lt;iframe&gt; element inside of an open shadow DOM.</div>
<div id="shadow-iframe-container-open"></div>
<template id="shadow-iframe-container-open-content-template">
<iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe>
</template>
<script>
(() => {
const shadowIframeContainer = document.querySelector('#shadow-iframe-container-open');
const shadow = shadowIframeContainer.attachShadow({mode: 'open'});
const template = document.querySelector('#shadow-iframe-container-open-content-template').content;
const content = document.importNode(template, true);
shadow.appendChild(content);
})();
</script>
</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Manual Performance Tests</title>
<link rel="stylesheet" href="test-stylesheet.css" />
</head>
<body><div class="content">
<div class="description">Add elements</div>
<div>
<a href="#" id="add-elements-1000">1000</a>
<a href="#" id="add-elements-10000">10000</a>
<a href="#" id="add-elements-100000">100000</a>
<a href="#" id="add-elements-1000000">1000000</a>
<script>
document.querySelector('#add-elements-1000').addEventListener('click', () => addElements(1000), false);
document.querySelector('#add-elements-10000').addEventListener('click', () => addElements(10000), false);
document.querySelector('#add-elements-100000').addEventListener('click', () => addElements(100000), false);
document.querySelector('#add-elements-1000000').addEventListener('click', () => addElements(1000000), false);
let counter = 0;
function addElements(amount) {
const container = document.querySelector('#container');
for (let i = 0; i < amount; i++) {
const element = document.createElement('div');
element.textContent = 'ありがとう';
container.appendChild(element);
}
counter += amount;
document.querySelector('#counter').textContent = counter;
}
</script>
</div>
<div id="counter"></div>
<div id="container"></div>
</div></body>
</html>

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Manual Performance Tests</title>
<link rel="stylesheet" href="test-stylesheet.css" />
</head>
<body><div class="content">
<div class="description">&lt;iframe&gt; element inside of an open shadow DOM.</div>
<div id="shadow-iframe-container-open"></div>
<template id="shadow-iframe-container-open-content-template">
<iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 50px; border: 1px solid #d8d8d8;"></iframe>
</template>
<script>
(() => {
const shadowIframeContainer = document.querySelector('#shadow-iframe-container-open');
const shadow = shadowIframeContainer.attachShadow({mode: 'open'});
const template = document.querySelector('#shadow-iframe-container-open-content-template').content;
const content = document.importNode(template, true);
shadow.appendChild(content);
})();
</script>
<div class="description">Add elements</div>
<div>
<a href="#" id="add-elements-1000">1000</a>
<a href="#" id="add-elements-10000">10000</a>
<a href="#" id="add-elements-100000">100000</a>
<a href="#" id="add-elements-1000000">1000000</a>
</div>
<div id="counter"></div>
<div id="container"></div>
<script>
(() => {
document.querySelector('#add-elements-1000').addEventListener('click', () => addElements(1000), false);
document.querySelector('#add-elements-10000').addEventListener('click', () => addElements(10000), false);
document.querySelector('#add-elements-100000').addEventListener('click', () => addElements(100000), false);
document.querySelector('#add-elements-1000000').addEventListener('click', () => addElements(1000000), false);
let counter = 0;
function addElements(amount) {
const container = document.querySelector('#container');
for (let i = 0; i < amount; i++) {
const element = document.createElement('div');
element.textContent = 'ありがとう';
container.appendChild(element);
}
counter += amount;
document.querySelector('#counter').textContent = counter;
}
})();
</script>
</div></body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Yomichan Manual Performance Tests</title>
<link rel="icon" type="image/gif" href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw==" />
<link rel="stylesheet" href="test-stylesheet.css" />
</head>
<body>
<h1>Yomichan Manual Performance Tests</h1>
<p class="description">Testing Yomichan performance with artificially demanding cases in a real browser</p>
<div class="test">
<div class="description">&lt;iframe&gt; element.</div>
<iframe src="test-document3-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe>
</div>
<div class="test">
<div class="description">&lt;iframe&gt; element containing an &lt;iframe&gt; element inside of an open shadow DOM.</div>
<iframe src="test-document3-frame2.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe>
</div>
</body>
</html>