question 1.1.b long

<iframe>’s which display content from different domains have security measures in place to prevent all sorts of stuff. For example, you can’t have JavaScript access anything inside it. It can be very frustrating, for example, if you just want to do something normal and white-hat like adjust the height of the iframe to fit the content inside it. These security measures are in place to prevent all the black-hat kind of things you could do if you did have JavaScript access to the innards of an iframe.

I’ve literally tried to work on different solutions for this for years and always came up short. I recently came across a solution from Kazi Manzur Rashid (about two years old now) that looks pretty solid so I thought I’d try it out. The results are the closest I’ve been able to come yet:

View Demo

Warning: the demo kinda freaks out WebKit browsers like Safari and Chrome, see issues below.

To those who have come before…

To do this with an iframe with source content on the same domain, you can do this. Same-domain iframes aren’t subject to the same restrictions so it’s far easier.

Adam Fortuna explored some options using kind of a man-in-the-middle idea. This may have been inspired by a technique by John McKerrell.

The following technique doesn’t require the middle man thing though, which is why it’s closer to ideal.

Prereqs

This solution presupposes that you have control over both the hosting site and the source site. You’ll need to run JavaScript on both ends. So this isn’t going to work for an iframe of google.com.

The Big Idea

The work-around is using hash tags in the URL to relay information back and forth. This circumvents the security restrictions. It is unlikely that this will ever break, so it’s not really a “hack”. You can’t really do anything malicious with just a hash tag. In our case we’re just reading in that information and using it to do the resizing.

The HOST Domain

Actually has the iframe on it:

<iframe id="frame-one" scrolling="no" frameborder="0" src="http://digwp.com/examples/iFrameSource/source.html" onload="FrameManager.registerFrame(this)"></iframe>

The iframe has an onload event on it, which calls a function from the FrameManager class, which we’ll need to call in the <head>:

<script type="text/javascript" src="js/FrameManager.js"></script>

And here is the magical FrameManager class:

var FrameManager = {

    currentFrameId : '',
    currentFrameHeight : 0,
    lastFrameId : '',
    lastFrameHeight : 0,
    resizeTimerId : null,

    init: function() {
    
        if (FrameManager.resizeTimerId == null) {
        
            FrameManager.resizeTimerId = window.setInterval(FrameManager.resizeFrames, 500);
            
        }
        
    },

    resizeFrames: function() {
    
        FrameManager.retrieveFrameIdAndHeight();

        if ((FrameManager.currentFrameId != FrameManager.lastFrameId) || (FrameManager.currentFrameHeight != FrameManager.lastFrameHeight)) {
            
            var iframe = document.getElementById(FrameManager.currentFrameId.toString());

            if (iframe == null) return;

            iframe.style.height = FrameManager.currentFrameHeight.toString() + "px";

            FrameManager.lastFrameId = FrameManager.currentFrameId;
            FrameManager.lastFrameHeight = FrameManager.currentFrameHeight;
            window.location.hash = '';
            
        }
        
    },

    retrieveFrameIdAndHeight: function() {
    
        if (window.location.hash.length == 0) return;

        var hashValue = window.location.hash.substring(1);

        if ((hashValue == null) || (hashValue.length == 0)) return;

        var pairs = hashValue.split('&');

        if ((pairs != null) && (pairs.length > 0)) {
        
            for(var i = 0; i < pairs.length; i++) {
            
                var pair = pairs[i].split('=');

                if ((pair != null) && (pair.length > 0)) {
                
                    if (pair[0] == 'frameId') {
                    
                        if ((pair[1] != null) && (pair[1].length > 0)) {
                        
                            FrameManager.currentFrameId = pair[1];
                        }
                    } else if (pair[0] == 'height') {
                    
                        var height = parseInt(pair[1]);

                        if (!isNaN(height)) {
                        
                            FrameManager.currentFrameHeight = height;
                            FrameManager.currentFrameHeight += 15;
                            
                        }
                    }
                }
            }
        }
        
    },

    registerFrame: function(frame) {
    
        var currentLocation = location.href;
        var hashIndex = currentLocation.indexOf('#');

        if (hashIndex > -1) {
        
            currentLocation = currentLocation.substring(0, hashIndex);
        }

        frame.contentWindow.location = frame.src + '?frameId=' + frame.id + '#' + currentLocation;
        
    }
};

window.setTimeout(FrameManager.init, 300);

The SOURCE site

The source content could pretty much be anything, located on a different server. Perhaps:

<body>

    <div id="content">
        Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce in tortor sit amet sem luctus ornare. Nam sed augue id erat commodo gravida. Nulla in pede. Nunc sed elit non pede aliquam eleifend. Cras varius. Sed non lorem eget ipsum accumsan suscipit. Donec bibendum enim. Phasellus a ligula. Fusce turpis diam, ultricies at, ullamcorper a, consectetuer et, mauris. Pellentesque neque felis, scelerisque non, vestibulum at, luctus quis, velit. Quisque sit amet mi sed sem facilisis ornare. In leo ante, hendrerit nec, lobortis eget, feugiat ac, orci.
    </div>
    
</body>

The most important thing we do on the source site is run some JavaScript to “publish” the height of itself. In my demo, I’m also throwing some jQuery in there to do some font-size animation so that the source content grows taller and shorter.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js?ver=1.3.2"></script>
<script type="text/javascript" src="frame.js"></script>
<script type="text/javascript">

    window.onload = function(event) {
        window.setInterval(publishHeight, 300);
    }
    
    $(function() {
    
        var $content = $("#content");
    
        function toggleFontSize() {
        
            if ($content.css("font-size") == "22px") {
                $("#content").animate({
                    fontSize: "15px"
                });
            } else {
                $("#content").animate({
                    fontSize: "22px"
                });
            }
        
        }
        
        var int = setInterval(toggleFontSize, 5000);
    
    });
</script>

So we are calling publishHeight() every 300 milliseconds. Here is that function, and it’s rag-tag gang of fellow supporting functions from the frame.js file.

function publishHeight() {

    if (window.location.hash.length == 0) return;

    var frameId = getFrameId();

    if (frameId == '') return;

    var actualHeight = getBodyHeight();
    var currentHeight = getViewPortHeight();

    if  (Math.abs(actualHeight - currentHeight) > 15) {
        var hostUrl = window.location.hash.substring(1);

        hostUrl += "#";
        hostUrl += 'frameId=' + frameId;
        hostUrl += '&';
        hostUrl += 'height=' + actualHeight.toString();

        window.top.location = hostUrl;
    }
}

function getFrameId() {

    var qs = parseQueryString(window.location.href);
    var frameId = qs["frameId"];

    var hashIndex = frameId.indexOf('#');

    if (hashIndex > -1) {
        frameId = frameId.substring(0, hashIndex);
    }

    return frameId;
    
}

function getBodyHeight() {

    var height,
        scrollHeight,
        offsetHeight;

    if (document.height) {
    
        height = document.height;
        
    } else if (document.body) {
    
        if (document.body.scrollHeight) {
            height = scrollHeight = document.body.scrollHeight;
        }
        
        if (document.body.offsetHeight) {
            height = offsetHeight = document.body.offsetHeight;
        }
        
        if (scrollHeight && offsetHeight) {
            height = Math.max(scrollHeight, offsetHeight);
        }
    }

    return height;
}

function getViewPortHeight() {

    var height = 0;

    if (window.innerHeight) {
        height = window.innerHeight - 18;
    } else if ((document.documentElement) && (document.documentElement.clientHeight)) {
        height = document.documentElement.clientHeight;
    } else if ((document.body) && (document.body.clientHeight)) {
        height = document.body.clientHeight;
    }

    return height;
    
}

function parseQueryString(url) {

    url = new String(url);
    
    var queryStringValues = new Object(),
        querystring = url.substring((url.indexOf('?') + 1), url.length),
        querystringSplit = querystring.split('&');

    for (i = 0; i < querystringSplit.length; i++) {
        var pair = querystringSplit[i].split('='),
            name = pair[0],
            value = pair[1];

        queryStringValues[name] = value;
    }

    return queryStringValues;
    
}

Issues

  • Refresh happy in WebKit. Apparently it used to be Firefox that got all refresh happy, but apparently with the latest version it is flip flopped. Watch out of visiting the demo in Safari or Chrome, it’s a little choppy. If anyone has any ideas here, this is probably the biggest problem.
  • Messes up back button. Because of all the hash tags flying around on the host page, it may screw up the back button functionality on that page.
  • Intervals, intervals, intervals. There are a lot of intervals flying around here which are nearly always hacky-red-flags. The quicker the intervals, the smoother but more resource intensive. The slower, the more choppy but easier. Either way, sucky.
  • Limit of information sent via hash. If you were thinking about using this technique to send other information, because it happens via URL, you are limited by the amount of information that can pass. Presumably the same as a GET request… around 1k.
[end]