Search code examples
javascriptdomiframegoogle-chrome-extensionshadow-dom

How to capture iframe nested within Shadow DOM's "#shadow-root (open)" element?


  • I need to inject an element inside an <iframe>, after a <button> within this iframe is clicked.
  • This <iframe>exists within a page (e. g. somewebpage.com) that is not under my control.
  • The <iframe>'s content belongs to the same domain as the main document's body/DOM structure.
  • To achieve this I need to use a Content Script file from a Chrome Extension as per the below V3 manifest's "content_scripts" configuration:
"content_scripts": [
        {
            "type": "module",                // I added this items "type", "run_at", "all_frames"
            "run_at": "document_end",        // and "match_origin_as_fallback" hoping they  
            "all_frames": true,              // would help.
            "match_origin_as_fallback": true, 
            "js": [
                "foreground.js"               // File containing the pure JS code
            ],
            "matches": [
                "https://somewebpage.com/*"   // The page where I want to inject the element
            ]

The extension's content script "foreground.js" file contains the code that should detect the iframe, listen to the click event, and then inject the element within the iframe after click is detected.

After the page is fully loaded, when I inspect the elements on this page, it basically looks like this:

<body>
    <macroponent>
        #shadow-root (open) <!-- Shadow DOM element. I believe this is what is my main obstacle -->
        <div>
            <sn-canvas-appshell-root>
                <sn-canvas-appshell-layout>
                    <sn-polaris-layout>
                        <iframe id="myIframe"> <!-- to access button I need to find the iframe 1st -->
                            <button id="myButton">TAKE</button> <!-- I need to get that click event -->
                        </iframe>
                    </sn-polaris-layout>
            </sn-canvas-appshell-root>
            </sn-canvas-appshell-root>
        </div>
    </macroponent> 
</body>

I have tried a lot of different approaches and none were able to find the <iframe>. The error messages on the console are returning the getElementById as null and getElementsByTagName as undefined.

As a standard method I tried this with some variations like window.onload:

document.onreadystatechange = () => {
    if (document.readyState === 'complete') {
        let iframe = document.getElementsByTagName('iframe')[0];
        iframe.contentDocument.getElementById("myButton").addEventListener("click", function () {
            alert("Hurray!");
            // Injected element code here
        });
    }

I have also tried some more advanced solutions like this:

function searchFrame(id) {                                     // id = the id of the wanted (i)frame
    var result = null,                                         // Stores the result
        search = function (iframes) {                          // Recursively called function
            var n;                                             // General loop counter
            for (n = 0; n < iframes.length; n++) {             // Iterate through all passed windows in (i)frames
                if (iframes[n].frameElement.id === id) {       // Check the id of the (i)frame
                    result = iframes[n];                       // If found the wanted id, store the window to result
                }
                if (!result && iframes[n].frames.length > 0) { // Check if result not found and current window has (i)frames
                    search(iframes[n].frames);                 // Call search again, pass the windows in current window
                }
            }
        };
    search(window.top.frames);                                  // Start searching from the topmost window
    return result;                                              // Returns the wanted window if found, null otherwise
}

Source:
Get element value inside iframe which is nested inside Frame in javascript?

In my opinion, this issue persists perhaps because the <iframe> is a child to a "Shadow DOM" element, which makes the method of finding the <iframe> different from the standard.

After realizing that probably the issue is that the <iframe> is a child to a "Shadow DOM" element, I tried this approach from another question:

this.windows = this.shadowRoot.getElementsByTagName('app-window')

Source:
GetElementById from within Shadow DOM

Or even this one:
(although I suspect it might be a bit overkill for my use case)

customElements.define("my-component", class extends HTMLElement {
  constructor() {
    super().attachShadow({mode:"open"}).innerHTML = `<slot></slot>`;
  }
})

const shadowDive = (
  el,
  selector,
  match = (el, root) => {
    console.warn('match', el, root);
  },
  root = el.shadowRoot || el
) => {
  root.querySelector(selector) && match(root.querySelector(selector), root);
  [...root.querySelectorAll("*")].map(el => shadowDive(el, selector, match));
}
shadowDive(document.body, "content"); // note optional parameters

Source:
How to select element tag from shadow root

And again I am getting the same null or undefined errors. But I think that perhaps I am on the right track now that I know that the <iframe> is nested within a "Shadow DOM" element.

Perhaps there is a way to bypass or respect the "Shadow DOM" and get to that <iframe> but to be honest I am out of ideas.

Thank you


Solution

    1. A shadowRoot doesn't have getElementsByTagName.
    2. ShadowDOM frames aren't exposed in the global window or frames (it's the same thing BTW)
    3. customElements.define doesn't work inside content scripts (due to "world isolation").

    You can use querySelector + contentDocument:

    const el = document.querySelector('macroponent').shadowRoot.querySelector('iframe');
    const elInner = el.contentDocument.querySelector('div');
    

    Note that you don't need all_frames or match_origin_as_fallback here because the iframe is same-origin, hence it's directly accessible from the main document's content script as shown above.

    One pitfall is that modern sites generate the contents of the page dynamically, often depending on a network response, which can happen long time after all your content scripts ran. In this case you can use MutationObserver to detect it or simply re-check periodically inside setInterval.