diff options
| author | siikamiika <siikamiika@users.noreply.github.com> | 2020-04-18 23:39:21 +0300 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-04-18 23:39:21 +0300 | 
| commit | 7a03ce0194fafb0c2e49994dc6efd33d5fdb6a07 (patch) | |
| tree | 68a6bc438180e9b3871cee4755a78b18ba3fa3e1 | |
| parent | 5b3d7fadc3534ba27eee971dac4ba5ee475c4e3d (diff) | |
| parent | d66ca93ce4d6a4c9814bac4cc508c24ff87b8f69 (diff) | |
Merge pull request #460 from siikamiika/iframe-popup-edge-cases
Iframe popup edge cases
| -rw-r--r-- | ext/fg/js/frame-offset-forwarder.js | 70 | ||||
| -rw-r--r-- | ext/fg/js/frontend-initialize.js | 17 | ||||
| -rw-r--r-- | ext/fg/js/popup-proxy.js | 7 | ||||
| -rw-r--r-- | ext/fg/js/popup.js | 13 | ||||
| -rw-r--r-- | ext/manifest.json | 2 | ||||
| -rw-r--r-- | ext/mixed/js/dom.js | 10 | ||||
| -rw-r--r-- | test/data/html/test-document2.html | 19 | ||||
| -rw-r--r-- | test/data/html/test-document3-frame1.html | 44 | ||||
| -rw-r--r-- | test/data/html/test-document3-frame2.html | 62 | ||||
| -rw-r--r-- | test/data/html/test-document3.html | 26 | 
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"><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> -</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"><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> 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"><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> |