aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsiikamiika <siikamiika@users.noreply.github.com>2020-04-18 23:39:21 +0300
committerGitHub <noreply@github.com>2020-04-18 23:39:21 +0300
commit7a03ce0194fafb0c2e49994dc6efd33d5fdb6a07 (patch)
tree68a6bc438180e9b3871cee4755a78b18ba3fa3e1
parent5b3d7fadc3534ba27eee971dac4ba5ee475c4e3d (diff)
parentd66ca93ce4d6a4c9814bac4cc508c24ff87b8f69 (diff)
Merge pull request #460 from siikamiika/iframe-popup-edge-cases
Iframe popup edge cases
-rw-r--r--ext/fg/js/frame-offset-forwarder.js70
-rw-r--r--ext/fg/js/frontend-initialize.js17
-rw-r--r--ext/fg/js/popup-proxy.js7
-rw-r--r--ext/fg/js/popup.js13
-rw-r--r--ext/manifest.json2
-rw-r--r--ext/mixed/js/dom.js10
-rw-r--r--test/data/html/test-document2.html19
-rw-r--r--test/data/html/test-document3-frame1.html44
-rw-r--r--test/data/html/test-document3-frame2.html62
-rw-r--r--test/data/html/test-document3.html26
10 files changed, 248 insertions, 22 deletions
diff --git a/ext/fg/js/frame-offset-forwarder.js b/ext/fg/js/frame-offset-forwarder.js
index c658c55a..1a2f3e1e 100644
--- a/ext/fg/js/frame-offset-forwarder.js
+++ b/ext/fg/js/frame-offset-forwarder.js
@@ -23,6 +23,10 @@ class FrameOffsetForwarder {
constructor() {
this._started = false;
+ this._cacheMaxSize = 1000;
+ this._frameCache = new Set();
+ this._unreachableContentWindowCache = new Set();
+
this._forwardFrameOffset = (
window !== window.parent ?
this._forwardFrameOffsetParent.bind(this) :
@@ -74,12 +78,12 @@ class FrameOffsetForwarder {
_onGetFrameOffset(offset, uniqueId, e) {
let sourceFrame = null;
- for (const frame of document.querySelectorAll('frame, iframe:not(.yomichan-float)')) {
- if (frame.contentWindow !== e.source) { continue; }
- sourceFrame = frame;
- break;
+ 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._forwardFrameOffsetOrigin(null, uniqueId);
return;
}
@@ -91,6 +95,64 @@ class FrameOffsetForwarder {
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) {
window.parent.postMessage({action: 'getFrameOffset', params: {offset, uniqueId}}, '*');
}
diff --git a/ext/fg/js/frontend-initialize.js b/ext/fg/js/frontend-initialize.js
index 2b942258..2df59e20 100644
--- a/ext/fg/js/frontend-initialize.js
+++ b/ext/fg/js/frontend-initialize.js
@@ -16,6 +16,7 @@
*/
/* global
+ * DOM
* FrameOffsetForwarder
* Frontend
* PopupProxy
@@ -24,7 +25,7 @@
* apiOptionsGet
*/
-async function createIframePopupProxy(url, frameOffsetForwarder) {
+async function createIframePopupProxy(url, frameOffsetForwarder, setDisabled) {
const rootPopupInformationPromise = yomichan.getTemporaryListenerResult(
chrome.runtime.onMessage,
({action, params}, {resolve}) => {
@@ -38,7 +39,7 @@ async function createIframePopupProxy(url, 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();
return popup;
@@ -78,6 +79,13 @@ async function main() {
let frontendPreparePromise = null;
let frameOffsetForwarder = null;
+ let iframePopupsInRootFrameAvailable = true;
+
+ const disableIframePopupsInRootFrame = () => {
+ iframePopupsInRootFrameAvailable = false;
+ applyOptions();
+ };
+
const applyOptions = async () => {
const optionsContext = {depth: isSearchPage ? 0 : depth, url};
const options = await apiOptionsGet(optionsContext);
@@ -88,8 +96,8 @@ async function main() {
}
let popup;
- if (isIframe && options.general.showIframePopupsInRootFrame) {
- popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder);
+ if (isIframe && options.general.showIframePopupsInRootFrame && DOM.getFullscreenElement() === null && iframePopupsInRootFrameAvailable) {
+ popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder, disableIframePopupsInRootFrame);
popups.iframe = popup;
} else if (proxy) {
popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url);
@@ -117,6 +125,7 @@ async function main() {
};
yomichan.on('optionsUpdated', applyOptions);
+ window.addEventListener('fullscreenchange', applyOptions, false);
await applyOptions();
}
diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js
index 82ad9a8f..3af83db2 100644
--- a/ext/fg/js/popup-proxy.js
+++ b/ext/fg/js/popup-proxy.js
@@ -20,7 +20,7 @@
*/
class PopupProxy {
- constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null) {
+ constructor(id, depth, parentId, parentFrameId, url, getFrameOffset=null, setDisabled=null) {
this._parentId = parentId;
this._parentFrameId = parentFrameId;
this._id = id;
@@ -28,6 +28,7 @@ class PopupProxy {
this._url = url;
this._apiSender = new FrontendApiSender();
this._getFrameOffset = getFrameOffset;
+ this._setDisabled = setDisabled;
this._frameOffset = null;
this._frameOffsetPromise = null;
@@ -142,6 +143,10 @@ class PopupProxy {
try {
const offset = await this._frameOffsetPromise;
this._frameOffset = offset !== null ? offset : [0, 0];
+ if (offset === null && this._setDisabled !== null) {
+ this._setDisabled();
+ return;
+ }
this._frameOffsetUpdatedAt = now;
} catch (e) {
logError(e);
diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js
index 99610e17..ae158263 100644
--- a/ext/fg/js/popup.js
+++ b/ext/fg/js/popup.js
@@ -16,6 +16,7 @@
*/
/* global
+ * DOM
* apiGetMessageToken
* apiInjectStylesheet
*/
@@ -271,7 +272,7 @@ class Popup {
}
_onFullscreenChanged() {
- const parent = (Popup._getFullscreenElement() || document.body || null);
+ const parent = (DOM.getFullscreenElement() || document.body || null);
if (parent !== null && this._container.parentNode !== parent) {
parent.appendChild(this._container);
}
@@ -365,16 +366,6 @@ class Popup {
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) {
const preferBelow = (optionsGeneral.popupHorizontalTextPosition === 'below');
const horizontalOffset = optionsGeneral.popupHorizontalOffset * offsetScale;
diff --git a/ext/manifest.json b/ext/manifest.json
index 452b642c..d383dab0 100644
--- a/ext/manifest.json
+++ b/ext/manifest.json
@@ -71,7 +71,7 @@
"applications": {
"gecko": {
"id": "alex@foosoft.net",
- "strict_min_version": "52.0"
+ "strict_min_version": "53.0"
}
}
}
diff --git a/ext/mixed/js/dom.js b/ext/mixed/js/dom.js
index 03acbb80..31ba33d6 100644
--- a/ext/mixed/js/dom.js
+++ b/ext/mixed/js/dom.js
@@ -62,4 +62,14 @@ class DOM {
default: return false;
}
}
+
+ static getFullscreenElement() {
+ return (
+ document.fullscreenElement ||
+ document.msFullscreenElement ||
+ document.mozFullScreenElement ||
+ document.webkitFullscreenElement ||
+ null
+ );
+ }
}
diff --git a/test/data/html/test-document2.html b/test/data/html/test-document2.html
index 3a22a5bf..b2046dfd 100644
--- a/test/data/html/test-document2.html
+++ b/test/data/html/test-document2.html
@@ -77,5 +77,22 @@ document.querySelector('#fullscreen-link1').addEventListener('click', () => togg
</script>
</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>
-</html> \ No newline at end of file
+</html>
diff --git a/test/data/html/test-document3-frame1.html b/test/data/html/test-document3-frame1.html
new file mode 100644
index 00000000..2ae906d2
--- /dev/null
+++ b/test/data/html/test-document3-frame1.html
@@ -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>
diff --git a/test/data/html/test-document3-frame2.html b/test/data/html/test-document3-frame2.html
new file mode 100644
index 00000000..c486e04b
--- /dev/null
+++ b/test/data/html/test-document3-frame2.html
@@ -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>
diff --git a/test/data/html/test-document3.html b/test/data/html/test-document3.html
new file mode 100644
index 00000000..3e7d5236
--- /dev/null
+++ b/test/data/html/test-document3.html
@@ -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>