1

I have a problem:

Every resource I looked for tells me this should work and it does in Chrome, Safari.. but not in Firefox.

System: macOS Catalina 10.15.7, Chrome 90.0.4430.212, Firefox 88.0.1, Edge 90.0.818.66

What I try to achieve / have achieved on Chrome: finding an iFrame, that is hosted on the same domain, and targeting some child nodes there and changing attributes, removing text inputs etc.

A simplified version of my JS code:

    // set as null first because the iFrame takes some time to load
    let iframeBody = null;

    // this function is only run once, while it should be run on every MutationObserver change
    function purge (iframe) {

        var timer = null;
        // only by querying that iFrame again Firefox picked up the purge function, I don't need that for Chrome 
        let iF = document.querySelector('#chatbot iframe').contentWindow.document.body;
        var inputField = iF.querySelector('div > div.frame-content > div.widget-position-right > div.chat > div.input-group');
        if(!inputField || typeof inputField === 'undefined') {
          if(timer !== null) {
            clearTimeout(timer);
          }
          timer = setTimeout(purge, 50);
          return;
        }
        // remove it
        inputField.remove();
    }

    // this one is never called in FF
    function cb (mutationList) {
        let chatDiv = iframeBody.querySelector('div > div.frame-content > div.widget-position-right > div.chat')
        if (chatDiv) {
            mutationList.some(item => {
                if (item.type === 'childList' && item.addedNodes && item.addedNodes[0] === chatDiv) {
                    purge();
                    return true;
                }
            })
        }

        let button = iframeBody.querySelector("div > div.frame-content > div.widget-position-right > #button")
    // make button invisible if found, restore if chat is closed
        if (button) {
            if (button.classList.contains('chat-open')) {
                button.style.opacity = 0;
                button.style.pointerEvents = "none";
                button.style.width = 0;
            }
            else {
                button.style.opacity = 1;
                button.style.pointerEvents = "auto";
                button.style.width = "auto";
            }
        }
    }

    function bind () {
        try {
            iframeBody = document.querySelector('#chatbot iframe').contentWindow.document.body;
        
            if (!iframeBody) {
                return setTimeout(bind, 500) // wait and retry because iFrame is not loaded immediately
            }
            if(iframeBody){
                console.log("iframeBody found");
            }

            const mutationObservable = new MutationObserver(cb)

            // actually don't need characterData, just an attempt to pick up the observed event
            const config = { characterData: true, attributes: true, childList: true, subtree: true};

            setTimeout(() => {
                // this is the initial call that works in FF (and Chrome/Safari naturally)
                purge();
                mutationObservable.observe(iframeBody, config);
            }, 500);
            
        } catch (err) {
            // console.log(err);
            // try to bind again, can take some time to load
            setTimeout(bind, 100)
        }
    }
    // start here
    bind();

})();

I verified that I can access the HTML nodes with querySelector in the Developer console and edit them just fine in all browsers, it is just the MutationObserver callback that is not picked up.

I tried to put write it like this

new MutationObserver((mutation) => {
    // cb function goes here
})

but to no avail. I have been sitting at this for a few hours now, just to get it to work, and I am tempted to just deactivate it all together for Firefox..

Any hints are very appreciated. If any additional resources are needed, please let me know.

Update: tried suggestion from comments with new frameElem.contentWindow.MutationObserver by calling MutationObserver like this without any change:

const iframeElmWindow = document.querySelector('#chatbot iframe').contentWindow;
const mutationObservable = new iframeElmWindow.MutationObserver(cb);
hreimer
  • 785
  • 8
  • 31
  • 2
    Can you create a [mre] of your code, a StackSnippet if possible. – Teemu May 24 '21 at 10:14
  • Try on firefox [this example](http://jsbin.com/ivamoh/53/edit?html,js,output) does it work? – Greg-- May 24 '21 at 10:30
  • 1) Try `new frameElem.contentWindow.MutationObserver`. 2) There's no cross-browser guarantee that div is present in a separate mutation's first added node, it may be inside some other element. – wOxxOm May 24 '21 at 10:37
  • @wOxxOm 1) I tried it but saw no change, for 2) what do you mean by that? I had it previously defined just with div.className or the #buttonId, without the parent element > child element syntax. – hreimer May 25 '21 at 02:47
  • anyways, I will provide an example as @Teemu asked – hreimer May 25 '21 at 02:47
  • @Greg-- was your example complete? on Enter both Chrome and FF added li elements with numbers – hreimer May 25 '21 at 02:52
  • How is loaded your iframe? Are you sure the `iframeBody` Firefox gets is the one of the loaded document and not of say, `about:blank`? – Kaiido May 25 '21 at 06:03
  • Did you try example in firefox? does it work for you? – Greg-- May 25 '21 at 09:41

1 Answers1

0

The problem is certainly that you caught the initial about:blank's Document's <body> in your iframeBody variable.

Here is an outsourced1 minimal example that shows that your code may catch that initial Document and not the one that gets loaded later on.

// removed a lot of unrelated stuff from OP,
// to keep only the core of the issue
function bind() {
  const iframeBody = frame.contentWindow.document.body;
  if( !iframeBody ) {
    return setTimeout( bind, 500 );
  }
  if( iframeBody ) {
    console.log( "iframeBody found" );
    console.log( "location:", iframeBody.ownerDocument.location.href );
                // logs "about:blank"
}
bind();

So the one <body> you are observing is the one of that temporary initial about:blank Document. And since this Document will get replaced in a few ms by the loaded Document, no new Mutation will ever happen there.

The easiest to workaround that is to use the <iframe>'s load event, which will fire only when the loaded Document actually has loaded.
This way, you are sure you won't catch the initial about:blank Document, and you don't need your setTimeout( bind ) hack anymore: if ever you can't access the iframe's contentDocument in this event, it's because both context are not same-origin, and trying again won't fix it.

So all in all, to start a MutationObserver targeting your <iframe>'s body you'd do:

frame.onload = function( evt ) {
  const iframeBody = frame.contentDocument?.body;
  if( !iframeBody ) {
    // this means we are in a cross-origin document...
    return;
  }
  const observer = new MutationObserver( cb );
  observer.observe( iframeBody, { childList: true, subtree: true } );
};

Once again as an outsourced example1.

1. We need to outsource these examples because StackSnippet's null-origined iframes won't allow our script to access the content of an iframe, no matter how it's being served.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • It's probably worth mentioning that this does not work in Chrome/Chromium-based browsers as iframes trigger exactly no events. https://stackoverflow.com/a/6245998/15388164 – rschristian Jan 28 '22 at 20:43
  • @rschristian this Q/A is about iframe used to download a file, i.e where the server sends a Content-Disposition: attachment header. In such a case there is no document that gets loaded, and nothong to attach a MutationObserver on. "Normal" document loads do fire events even in Chrome. – Kaiido Jan 29 '22 at 01:03
  • That might be what the issue is focused on, but that behavior affects the issue being referenced here. Chrome won't ever fire the on load event. – rschristian Jan 29 '22 at 05:00
  • @rschristian Yes it does, [the fiddle](https://jsfiddle.net/r5fqa8sn/) works on Chrome, it does fire the load event. Once again it will not fire the load event only when the iframe doesn't represent a document. – Kaiido Jan 29 '22 at 07:43
  • I can assure you it doesn't fire the event. It's not hard to test. Firefox will fire the event and eventually result in a ready document, but this does not ever happen in Chrome. – rschristian Jan 29 '22 at 20:19
  • @rschristian but... the fiddle works for you too right? If it didn't fire a load event, then the MutationObserver would never even be created, and the console would never show a single message. I mean, you are right it's not hard to test, did you test it? Is this even simpler example more clear for you https://jsfiddle.net/ktovgnce/ ? – Kaiido Jan 30 '22 at 01:53