Merge pull request #460 from siikamiika/iframe-popup-edge-cases
Iframe popup edge cases
This commit is contained in:
commit
7a03ce0194
@ -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}}, '*');
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,4 +62,14 @@ class DOM {
|
|||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getFullscreenElement() {
|
||||||
|
return (
|
||||||
|
document.fullscreenElement ||
|
||||||
|
document.msFullscreenElement ||
|
||||||
|
document.mozFullScreenElement ||
|
||||||
|
document.webkitFullscreenElement ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,5 +77,22 @@ document.querySelector('#fullscreen-link1').addEventListener('click', () => togg
|
|||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="test">
|
||||||
|
<div class="description"><iframe> 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>
|
||||||
|
44
test/data/html/test-document3-frame1.html
Normal file
44
test/data/html/test-document3-frame1.html
Normal 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>
|
62
test/data/html/test-document3-frame2.html
Normal file
62
test/data/html/test-document3-frame2.html
Normal 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"><iframe> 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>
|
26
test/data/html/test-document3.html
Normal file
26
test/data/html/test-document3.html
Normal 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"><iframe> 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"><iframe> element containing an <iframe> 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>
|
Loading…
Reference in New Issue
Block a user