How to check if iframe is visible from within the iframe
Asked Answered
O

3

8

I develop a widget that is expected to run on a partner's site as an iFrame. The iFrame is served up via a different domain than the parent's domain, so I cannot explicitly access their DOM.

The end-user can expand/collapse our widget using a button on the parent page. The parent page does not alert us when this happens or give us any way to tell the current status.

So I'm looking to see if there is some way around this by detecting from within our iFrame if the document is hidden. We use this information to determine if we need to send notifications to the end-user if our widget is hidden.

I've tried some things, like hookinh into the visibilitychange event, but it doesn't fire. I've also tried doing a hacky thing like getting the computed style of document.body but it doesn't seem to change based on whether the iFrame is hidden or not.

Offenseless answered 2/10, 2019 at 19:28 Comment(4)
Intersection observer is api that should tell you if an element is visible. If you hook body to it is should do the trick. Other than that, you can do in only in frames that are of same origin.Implore
well if you can have a script running on their page it is possible, without that, you are out of luck.Chronaxie
Thanks Akxe for the idea! I gave that a shot, but for some reason it was only firing once. However reading through the docs helped me realize that getBoundingClientRect() and checking if(height === 0) will do the trick for what I need.Offenseless
@Implore - Intersection observer is a good solution (the only one that worked for me so far), could you please submit an answer with a link to MDN documentation about it and a simple explanation on what it does ?Ischium
P
5

Firefox

document.hidden works correctly inside the iframe and will report if it has been hidden on the parent page.

Chrome et al

When the iframe is hidden, it sets both the documentElement and body height/width to zero. It's best to check both elements, to rule out someone doing something strange with CSS.

Safari

???? Still looking for an answer for this one.

Perfectionism answered 22/8 at 10:17 Comment(0)
K
1

Neither document.hidden nor height/width of the body or root element didn't work properly for me on the latest macOS. My best shot came with this piece of docs:

requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe>s, in order to improve performance and battery life.

It ended up like that:

let ts = new Date().valueOf();
let isVisible: boolean;
let handler: number;
const timeout = 500;

const checkVisibility = () => {
  const delay = new Date().valueOf() - ts;
  // if (delay > 50) {
  //   console.log(`${delay}ms since last check`);
  // }

  let newVisibility = new Date().valueOf() - ts < 100;

  if (newVisibility !== isVisible) {
    console.log("visible: " + newVisibility, new Date().valueOf() - ts);
    isVisible = newVisibility;
  }

  ts = new Date().valueOf();
  handler = requestAnimationFrame(checkVisibility);
};

setInterval(checkVisibility, timeout);

However, it only works in Chrome (perfectly though: both visibility: hidden and display: none are covered properly).

Firefox accidentally throws requestAnimationFrame calls even for the hidden iframe, although it quickly recognises its mistake. So, it can be fixed by stacking last few calls (say, three in a row should be ok), but it's an incredibly dirty trick and I doubt if it's reliable.

FF isn't sure

Safari don't give a damn about it at all. It works properly for the iframe in the inactive tab, but if the tab is in the foreground, iframe always acts the same way, whether it's visible or not. It has height and width, its children have them too, requestAnimationFrame is fired with the same frequency.

Feels like it can't be solved for Safari from the inside of the iframe.

Komara answered 25/8 at 16:7 Comment(4)
Interesting idea, but don’t think you have the implementation quite right. I would just use requestAnimationFrame, to set the last time it was called and then setInterval to check how long since it was last called. The only issue is if their is another long running task in the browser, you risk a false positive, but I guess that will auto correct pretty quickly.Perfectionism
Guess you're right, it's cleaner your way, however, I believe the result is the same. When it's visible, it fires itself recursively 60 times per minute or so. When it's not visible, it's fired only once a second because of setInterval.Komara
Minute one it fires 60 times a second, minute two, 120, minute 3, 180 and so on.Perfectionism
Yep, you're right, I missed the cleanup. But, unfortunately, it was not the issue: FF still fires two requestAnimationFrames in a row from time to time, Safari still doesn't give a damn.Komara
G
-1

For communicate between a parent page and an iFrame you can use PostMessage API

even they are on different domains. The parent page can send a message to the iFrame and the iFrame can listen for these messages.

// In the parent page
const iframe = document.getElementById('your-iframe-id');
iframe.contentWindow.postMessage('widget-expanded', '*');

// In the iFrame
window.addEventListener('message', (event) => {
    if (event.data === 'widget-expanded') {
        // Handle the widget being expanded
    }
});

Example:

in parent.html

<body>
    <button id="toggle-widget">Toggle Widget</button>
    <iframe id="widget-iframe" src="iframe.html" width="200" height="200"></iframe>

    <script>
        const iframe = document.getElementById('widget-iframe');
        const toggleButton = document.getElementById('toggle-widget');
        let isExpanded = false;

        toggleButton.addEventListener('click', () => {
            isExpanded = !isExpanded;
            const message = isExpanded ? 'widget-expanded' : 'widget-collapsed';
            iframe.contentWindow.postMessage(message, '*');
        });
    </script>
</body>

in iframe.html

<body>
    <div id="widget-content">Widget Content</div>

    <script>
        window.addEventListener('message', (event) => {
            if (event.origin !== 'http://your-parent-domain.com') {
                return; // Ignore messages from unknown origins
            }

            const widgetContent = document.getElementById('widget-content');
            if (event.data === 'widget-expanded') {
                widgetContent.style.display = 'block';
            } else if (event.data === 'widget-collapsed') {
                widgetContent.style.display = 'none';
            }
        });
    </script>
</body>
Gavel answered 27/8 at 16:44 Comment(1)
That's not the question being asked, and your code does not auto detect a hidden iframe on the parent page. It needs to work without the developer having to do any thing.Perfectionism

© 2022 - 2024 — McMap. All rights reserved.