iframe Auto Adjust Height as content changes
Asked Answered
H

5

27

I have an iframe as you can see on the following link;-

http://one2onecars.com

The iframe is the online booking in the centre of the screen. The problem I have is that although the height of the iframe is okay as the page loads, I need it to somehow auto adjust the height as the page content adjusts. For example, if I do a postcode search in the online booking it creates a dropdown menu and then makes the 'Next Step' button not viewable.

What I need to happen is that when the content of the online booking changes, the iframe auto adjusts to the new height of the iframe (dynamically) as it is not loading any other pages.

I have tried several different scripts using jquery to try resolving this issue, but they all only seem to auto adjust the height of the iframe when the page first loads and not as the contents of the iframe changes.

Is this even possible to do?

The code I have at the moment is with a set height at the moment:-

        <div id="main-online-booking">

            <iframe id="main-online-frame" class="booking-dimensions" src="http://www.marandy.com/one2oneob/login-guest.php" scrolling="no" frameborder="0"></iframe>

        </div>

#main-online-booking {
    height: 488px;
    border-bottom: 6px #939393 solid;
    border-left: 6px #939393 solid;
    border-right: 6px #939393 solid;
    z-index: 4;
    background-color: #fff;
}


.booking-dimensions {
    width: 620px;
    height: 488px;
}

If anybody can help me with this I would be much appreciated!

Hypodermic answered 15/1, 2013 at 8:59 Comment(6)
see this solution #11267074Shockley
why use Iframe in first place? ONly take a little bit of code to convert Iframe module to AJAXPogue
Because the online booking is hosted on our servers and not the customersHypodermic
if Iframe going on another domain can't use script between frame and main document due to security restrictionsPogue
different domain makes AJAX even more appealing..use jsonp to tarnsfer dataPogue
This is what I came up with: var height = 0; $('iframe').contents().filter(function() { if($(this).height() > height) height = $(this).height(); }); $('iframe').css('height',height +'px');Imponderabilia
M
32

Modern browsers have some new features that make this task easier than it use to be.

PostMessage

The postMessage API provides a simple method for comunicating between an iFrame and its parent.

To send a message to the parent page you call it as follows.

parent.postMessage('Hello parent','http://origin-domain.com');

In the other direction we can send the message to the iFrame with the following code.

var iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello my child', 'http://remote-domain.com:8080');

To recevie a message create an event listerner for the message event.

function receiveMessage(event)
{
  if (event.origin !== "http://remote-domain.com:8080")
    return;

  console.log(event.data);
}

if ('addEventListener' in window){
    window.addEventListener('message', receiveMessage, false);
} else if ('attachEvent' in window){ //IE
    window.attachEvent('onmessage', receiveMessage);

These examples uses the origin property to limit where the message is sent to and to check where it came from. It is possible to specify * to allow sending to any domain and you may in some cases you may want to accept messages from any domain. However, if you do this you need to consider the security implications and implement your own checks on the incoming message to ensure it contains what your expecting. In this case the iframe can post its height to '*', as we might have more than one parent domain. However, it's a good idea to check incoming messages are from the iFrame.

function isMessageFromIFrame(event,iframe){
    var
        origin  = event.origin,
        src     = iframe.src;

    if ((''+origin !== 'null') && (origin !== src.substr(0,origin.length))) {
        throw new Error(
            'Unexpect message received from: ' + origin +
            ' for ' + iframe.id + '. Message was: ' + event.data  
        );
    }

    return true;
}

MutationObserver

The other advance in more modern broswers is MutationObserver which allows you to watch for changes in the DOM; so it is now possible to detect changes that could affect the size of the iFrame without having to constantly poll with setInterval.

function createMutationObserver(){
    var
        target = document.querySelector('body'),

        config = {
            attributes            : true,
            attributeOldValue     : false,
            characterData         : true,
            characterDataOldValue : false,
            childList             : true,
            subtree               : true
        },

        observer = new MutationObserver(function(mutations) {
            parent.postMessage('[iframeResize]'+document.body.offsetHeight,'*');
        });

    log('Setup MutationObserver');
    observer.observe(target, config);
}

var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

if (MutationObserver){
    createMutationObserver();
}

Working out an accurate height

Getting an accurate height for the iFrame is not as simple as it should be, as you have a choice of six different properties that you can check and none of them give a constantly right answer. The best solution I've come up with is this function that works so long as you don't use CSS to overflow the body tag.

function getIFrameHeight(){
    function getComputedBodyStyle(prop) {
        const style = document.defaultView.getComputedStyle(
            document.body, null);
        return parseInt(style.getPropertyValue(prop), 10);
    }

    return document.body.offsetHeight +
        getComputedBodyStyle('marginTop') +
        getComputedBodyStyle('marginBottom');
}

This is the IE9 version, for the much long IE8 version see this answer.

If you do overflow the body and you can't fix your code to stop this, then using either the offsetHeight or scrollHeight properties of document.documentElement are your best options. Both have pros and cons and it is best just to test both and see which works for you.

Other issues

Other things to consider include: having more than one iFrame on the page, CSS :Checkbox and :Hover events causing page resize, avoiding the use of height auto in the iFrames' body and html tags, and lastly the window being resized.

IFrame Resizer Library

I've wrapped all this up in a simple dependancy free library, that also provides some extra functions and page size detection algorithms not discussed here.

https://github.com/davidjbradshaw/iframe-resizer

This works in all modern browsers.

Mcgee answered 10/4, 2014 at 15:8 Comment(7)
In the function getComputedBodyStyle you define a parameter prop, however in the function body it is not used. Typo somewhere?Devitt
Old comment, but fwiw there's a missing call to getPropertyValue(prop). So, the way to get the computed style property is: document.defaultView.getComputedStyle(document.body, null).getPropertyValue(prop).Torrential
Holy crap, thank you for the resizer library. That was completely simple to set up and works perfectly.Prosciutto
This might be a better way to get height, document.body.getBoundingClientRect().heightLyophilize
@Lyophilize Interesting ideaMcgee
I tested the method @Lyophilize posted, and it seems to work interchangeably, but one tricky aspect is that neither method will get a valid height for an iframe that's not currently visible. The embedding code that Discourse serves, for example, uses this method to auto-size its iframe to fit the embedded discussion. Therefore, if the discussion embed is in a tabbed interface, it fails to correctly size itself when it's not the currently-selected tab on load. We ended up having to just forcibly delete and re-initialize the Discourse iframe when switching to its tab.Vazquez
Tabs have always been the biggest challenge. It’s the main reason the iframe-resizer API has a manual method in it to force a resize.Mcgee
T
31

setInterval

The only (corrected due to advances in browser tech, see David Bradshaw's answer) backwards compatible way to achieve this with an iframe is to use setInterval and keep an eye on the iframe's content yourself. When it changes its height, you update the size of the iframe. There is no such event you can listen out for that will make it easy unfortunately.

A basic example, this will only work if the iframe content that has changed in size is part of the main page flow. If the elements are floated or positioned then you will have to target them specifically to look for height changes.

jQuery(function($){
  var lastHeight = 0, curHeight = 0, $frame = $('iframe:eq(0)');
  setInterval(function(){
    curHeight = $frame.contents().find('body').height();
    if ( curHeight != lastHeight ) {
      $frame.css('height', (lastHeight = curHeight) + 'px' );
    }
  },500);
});

Obviously depending on what you want you can modify the perspective of this code so that it works from the iframe, on itself, rather than expecting to be part of the main page.

cross-domain issue

The problem you will find is that due to browser security it wont let you access the content of the iframe if it is on a different host to the main page, so there isn't actually anything you can do unless you have a way of adding any script to the html that appears in the iframe.

ajax

Some others are suggesting trying to use the third-party service via AJAX, unless the service supports this method it will be very unlikely you'll be able to get it to work -- especially if it is a booking service that will most likely need to operate over https/ssl.

As it appears you have full control over the iframe content, you have full options open to you, AJAX with JSONP would be an option. However, one word of warning. If your booking system is multistepped you need to make sure you have a well designed UI -- and possibly some history/fragment management code -- if you are to go down the AJAX route. All because you can never tell when a user will decide to navigate forward or back in their browser (which an iframe would automatically handle, within reason). A well designed UI can detract users from doing this.

cross-domain communication

If you have control of both sides (which it sounds like you do) you also have the cross domain communication option using window.postMessage - see here for more information https://developer.mozilla.org/en-US/docs/DOM/window.postMessage

Thalassa answered 15/1, 2013 at 9:17 Comment(7)
last statement about AJAX is absurd. OP is providing the service and there is absolutely no reason they can't provide a jsonp API to accomplish same thingPogue
any sensitive data can simply be managed on a different page... main page doesn't need to perform everything. But for basic search UI on main page would be idealPogue
@Pogue hardly absurd, where in the question does it mention the OP is providing the iframe content? I admit I missed the later comment, but the question doesn't mention it, and if dealing with a third-party what I state is perfectly true (especially as I constantly mention third-party). However, good point, I shall modify my answer accordingly. I have no idea what you are on about with your second comment though...Thalassa
suggestion about ajax has nothing to do with third party. That was an assumption on your part. For OP situation removing Iframe makes more sense than keeping it IMO. Comment about sensitive data was in regard to your https comments...more than one way to skin a cat. At point where any sensitive data would need to be input, open another page that doesn't have UI interference issues when using iframePogue
@DavidBradshaw ~ Others have rejected your edit because you should post such involved additional detail as a new answer, especially if it has quite different content. If you do, I'd quite happily up vote your additions myself as they are clear and up-to-date (at least with modern browsers).Thalassa
@pebbl, ok have written a new longer answer.Mcgee
@DavidBradshaw ~ nicely put together +1 from me.Thalassa
C
7

I wrote this script and it's working perfectly for me. Feel free to use it!

function ResizeIframeFromParent(id) {
    if (jQuery('#'+id).length > 0) {
        var window = document.getElementById(id).contentWindow;
        var prevheight = jQuery('#'+id).attr('height');
        var newheight = Math.max( window.document.body.scrollHeight, window.document.body.offsetHeight, window.document.documentElement.clientHeight, window.document.documentElement.scrollHeight, window.document.documentElement.offsetHeight );
        if (newheight != prevheight && newheight > 0) {
            jQuery('#'+id).attr('height', newheight);
            console.log("Adjusting iframe height for "+id+": " +prevheight+"px => "+newheight+"px");
        }
    }
}

You can call the function inside a loop:

<script>
jQuery(document).ready(function() {
    // Try to change the iframe size every 2 seconds
    setInterval(function() {
        ResizeIframeFromParent('iframeid');
    }, 2000);
});
</script>
Convoy answered 11/10, 2013 at 6:52 Comment(0)
N
2

ResizeObserver allows your code to remain encapsulated inside the iframe and decoupled from the outer scope (i.e., versus postMessage), and it is ligher weight than the general-purpose MutationObserver.

Simple example below (or good example at Mozilla):

const myElement = document.getElementById('my-element');
const resizeObserver = new ResizeObserver((entries) => {
  const dims = myElement.getBoundingClientRect(); // or see Mozilla for `entries` example
  console.log(`new height (${dims.height}) and width (${dims.width})`);
});
resizeObserver.observe(myElement);
Neuter answered 14/6, 2022 at 23:23 Comment(0)
E
0

use this script:

$(document).ready(function () {
  // Set specific variable to represent all iframe tags.
  var iFrames = document.getElementsByTagName('iframe');

  // Resize heights.
  function iResize() {
    // Iterate through all iframes in the page.
    for (var i = 0, j = iFrames.length; i < j; i++) {
    // Set inline style to equal the body height of the iframed content.
      iFrames[i].style.height = iFrames[i].contentWindow.document.body.offsetHeight + 'px';
    }
  }

 // Check if browser is Safari or Opera.
 if ($.browser.safari || $.browser.opera) {
    // Start timer when loaded.
    $('iframe').load(function () {
    setTimeout(iResize, 0);
    });

   // Safari and Opera need a kick-start.
   for (var i = 0, j = iFrames.length; i < j; i++) {
     var iSource = iFrames[i].src;
     iFrames[i].src = '';
     iFrames[i].src = iSource;
   }
 } else {
    // For other good browsers.
    $('iframe').load(function () {
    // Set inline style to equal the body height of the iframed content.
    this.style.height = this.contentWindow.document.body.offsetHeight + 'px';
    });
  }
});

Note : use it on webserver.

Epigraph answered 15/1, 2013 at 9:13 Comment(3)
note that will only work on same domain. Appears OP wants Iframe on other domainsPogue
so this will work on an iframe on the same domain? if thats the case I'll have to get the client to transfer the domain to usHypodermic
@Hypodermic This code will only work onload, as it isn't using any polling method, as far as I can see.Thalassa

© 2022 - 2024 — McMap. All rights reserved.