Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cross Domain IFrame element.scrollIntoView() Safari Issue

I have ran into an issue that is plagued all over google but none of the provided solutions work correctly. I assume the majority of these solutions do not account for cross domain.

I have a website which I would like to instruct users to embed into their site with a (full page) IFrame. The problem is on some versions of Safari only. Elements within the IFrame can not scroll themselves into view.

enter image description here

I notice that if I do a same domain test the IFrame can scroll itself using window.parent.scrollTo(0,element.top). This works, but not cross domain. Another odd thing is that no other browser requires the window.parent method to scroll the IFrame, only Safari. All other browsers can use element.scrollIntoView() from within the IFrame. Note that I already use the JavaScript workaround to please Safari with cross-protocol IFrames.

Another issue I've only seen on Safari Mobile inside IFrame is that Bootstrap Modals appear out of view at the top of the IFrame when scrolled down. Although, I'm sure if we can correctly set the scroll position we should be able to set the modal position as well.

Here's what I've tried;

 1. window.frames['IFrameName'].document.
    getElementById("elmId").scrollIntoView();
  1. Offset trick
  2. Velocity.js

My last resort here (I think) is to use postMessage from within my IFrame to notify the parent domain to set the scroll position of the frame.

It seems to me that this issue has been around for an awful long time. Is there a better approach than this?

like image 234
clamchoda Avatar asked Mar 04 '23 13:03

clamchoda


2 Answers

This ended up being a lot more research than code. What was going on was - I had code that resized the IFrame based on the content.

In all other browsers this works fine and eliminates the scroll bars. Turns out that Safari automatically sizes the Iframe leaving scrolls of it's own. In my application there are zero static pages. This leaves me with the issue of not being able to use the scrolling=no fix described in the link.

After discovering exactly what was going on I took a different approach to fixing elm.scrollIntoView(). The code is more comments then anything but the important parts are;

  1. Detecting when to apply the Iframe fix with RequiresIframeScrollFix
  2. Using elm.getBoundingClientRect().top to get our scroll position from within the Iframe.
  3. Communicating to the parent to scroll with window.parent.postMessage
  4. Receiving the message in the parent with window.addEventListener('message',...)

Here's what it looks like.

Iframe Site

Our Iframe site currently scrolls it's elements into view like this elm.scrollIntoView(); We've changed that to the following.

if (RequiresIframeScrollFix())
     window.parent.postMessage(elm.getBoundingClientRect().top, "*"); // Tell IFrame parent to do the scrolling. If this is not a test environment, replace "*" with the parent domain.
 else
     elm.scrollIntoView(); // If not scroll into view as usual.

Optional: fix for bootstrap modal positioning in IOS IFrames using elm.getBoundingClientRect().top.

$('#modalId').css('top', elm.getBoundingClientRect().top); // This fixes modal not in view on Safari Iframes.

RequiresIframeScrollFix() is mostly made up of some well document code laying around SO to determine if we're in an Iframe on the IPad or IPhone.

// Used to help identify problematic userAgents.
var debugNavigator = false;

// Detects an issue on mobile where the Parent is an iframe which cannot have it's scroll bars removed.
// Presumably not a bug as safari will autosize it's iframes: https://salomvary.com/iframe-resize-ios-safari.html
// Can use "scrolling=no" fix instead if the parent knows the initial size of your iframe.
function RequiresIframeScrollFix() {
    try {
        // Debug navigator Agent
        if (debugNavigator)
            alert(navigator.userAgent);

        // We know this issue happens inside an IFrame on;
        // Safari iPhone
        // Safari iPad
        // Safari Desktop Works fine.

        // Check for safari
        var is_safari = navigator.userAgent.indexOf("Safari") > -1;
        // Chrome has Safari in the user agent so we need to filter (https://stackoverflow.com/a/7768006/1502448)
        var is_chrome = navigator.userAgent.indexOf('Chrome') > -1;
        if ((is_chrome) && (is_safari)) { is_safari = false; }

        // If we need to narrow this down even further we can use a more robust browser detection (https://stackoverflow.com/questions/5916900)
        // Problematic browsers can be adjusted here.
        if (is_safari && inIframe() && (
                navigator.userAgent.match(/iPad/i) ||
                navigator.userAgent.match(/iPhone/i)
                ))
            return true;
        else
            return false;

    } catch (e) {
        alert(e.message);
    }
}

// (https://stackoverflow.com/questions/326069/)
function inIframe() {
    try {
        return window.self !== window.top;
    } catch (e) {
        return true;
    }
}

Parent Site

Our parent site holds the IFrame that was automatically sized by Safari Mobile. Therefore the parent site now has scroll bars of it's own, and not the IFrame. We set our listener up inside the parent site to scroll itself when it receives a message from the IFramed site.

// Safari Mobile Iframe Cross Domain Scroll Fix.
window.onload = function () {
     // Calback function to process our postMessages.
     function receiveMessage(e) {
          try {
               // Set the scroll position from our postMessage data.
               // Non-Test pages should uncomment the line below.
               //if (e.origin.includes("your-iframe-domain.com"))
               window.scrollTo(0, e.data);
           }
           catch (err) {
           }
      }

      // Setup and event to receives messages form our iframe (or window)
      window.addEventListener('message', receiveMessage);
}

Hopefully this helps someone else dissect Safari Iframe problems on mobile. Also, let me know if I've overlooked a better solution.

like image 76
clamchoda Avatar answered Mar 15 '23 15:03

clamchoda


The other option is iframeResizer library. There are two methods you can use from within iframePage: scrollTo and scrollToOffset, which do pretty much the same what you've described - they communicate via messages. It solved this problem for us in Safari.

Inside the parent page, when setting up resizing for the iframe, you have to assign a callback function to its onScroll event:

iframeNode.iframeResize({ 
  ...,
  onScroll: ({x,y}) => callback
}

And inside iframe page:

if('parentIFrame' in window){
  window.parentIFrame.scrollTo(0, someNode.offsetTop);
}
like image 43
Mikołaj Bernecki Avatar answered Mar 15 '23 16:03

Mikołaj Bernecki