FrameWarp - jQuery plugin for displaying pages in a neat overlay

Demo Download

While working on an exciting new web app, I found that I needed a way to show certain pages in an overlay window. This comes handy if you want to reuse something like a sharing or a settings page in different screens of your app. Instead of hacking together something that barely got the job done, I decided to take the time, do it properly and share you with you.

Of course, there is the option of using one of the numerous lightbox plugins to do this, but the plugin we will be creating in this tutorial has a lot of advantages over a generic lightbox script:

  • Lightweight - it is created specifically for showing pages, not images;
  • No UI, so the page feels like a dialog window;
  • The page can close itself, and can also send messages to the parent window;
  • Can optionally use a cache for faster subsequent page loads;
  • Uses a neat CSS animation with a JavaScript fallback.

Great! Now let's get started.

The Idea

When a link or button is clicked, our plugin, dubbed FrameWarp, will detect the coordinates of that element, and trigger a CSS animation of an expanding polygon moving to the center of the window. The plugin will then load an Iframe pointing to the URL we want to show. If the page is from the same origin as the current site, FrameWarp will also add two useful methods to the iframe - one for hiding it, and another one for sending a message to the parent.

We will be using the jQuery++ collection of tools for jQuery, which converts the library's animate() method to use CSS3 transitions on browsers that support them. This makes constructing complex CSS animations quite easy.

The Animation

As they say, a fiddle is worth 1000 words. So here is the animation in action (hit the Result tab):

The trick here is that we are animating the border properties of the element and the width, while the height remains 0. The left and right borders are set to transparent in the CSS of the plugin. Alternatively, you could do it with 3D CSS transforms, but it wouldn't work in older browsers.

The Plugin

Now to write the plugin. We are going to wrap our code in an anonymous function so that it is isolated from the rest of the page. In effect all the variables and helper functions you can see below are private and accessible only to our plugin.

assets/framewarp/framewarp.js

(function($){

    // Private varialble deffinitions

    var body = $('body'),
        win = $(window),
        popup, popupBG;

    var frameCache = {};
    var frameCacheDiv = $('<div class="frameCacheDiv">').appendTo('body');
    var currentIframe;

    $.fn.frameWarp = function(settings){

        // The main code of the plugin will go here

    };

    // Helper Functions

    function hide(){

        // Here we will remove the popup and dark background from the page

    }

    function setUpAPI(iframe, settings){

        // In this function, we will make two API methods available to the frame,
        // if it the page is from the same domain.
    }

    function sameOrigin(url){

        // Here we will determine whether the page is from the same domain
    }

    function getOrigin(url){

        // A helper function for generating an origin string
        // of the type: https://www.google.com
        // This includes the protocol and host.
    }

})(jQuery);

The plugin creates a div with a frameCacheDiv class name. It is going to hold the iframes we are adding to the page. Two more divs are added to the page by the plugin - .popup and .popupBG, which we will discuss in a moment. Now let's inspect the helper functions.

function hide(){

    if(currentIframe){
        currentIframe.hide();
        currentIframe = null;
    }

    popupBG.remove();
    popup.remove();
}

function setUpAPI(iframe, settings){

    if(sameOrigin(settings.url)){

        // Exposing a minimal API to the iframe
        iframe[0].contentWindow.frameWarp = {
            hide: hide,
            sendMessage:function(param){
                return settings.onMessage(param);
            }
        };
    }
}

function sameOrigin(url){

    // Compare whether the url belongs to the
    // local site or is remote

    return (getOrigin(url) == getOrigin(location.href));
}

function getOrigin(url){

    // Using an anchor element to
    // parse the URL

    var a = document.createElement('a');
    a.href = url;

    return a.protocol+'//'+a.hostname;
}

Browsers implement a security feature called "same origin policy" that limits a web site from accessing the DOM of another. For this reason, we have a helper function that compares the URL of the iframe with the address of the current page. Only when both the domain and the protocol match, will the plugin attempt to access the DOM of the iframe and add the API methods for sending messages and hiding.

Now we are ready to write the actual frameWarp plugin!

$.fn.frameWarp = function(settings){

    // Supplying default settings

    settings = $.extend({
        cache: true,
        url: '',
        width:600,
        height:500,
        closeOnBackgroundClick: true,
        onMessage:function(){},
        onShow:function(){}
    }, settings);

    this.on('click',function(e){

        e.preventDefault();

        var elem = $(this),
            offset = elem.offset();

        // The center of the button
        var buttonCenter = {
            x: offset.left - win.scrollLeft() + elem.outerWidth()/2,
            y: offset.top - win.scrollTop() + elem.outerHeight()/2
        };

        // The center of the window
        var windowCenter = {
            x: win.width()/2,
            y: win.height()/2
        };

        // If no URL is specified, use the href attribute.
        // This is useful for progressively enhancing links.

        if(!settings.url && elem.attr('href')){
            settings.url = elem.attr('href');
        }

        // The dark background

        popupBG = $('<div>',{'class':'popupBG'}).appendTo(body);

        popupBG.click(function(){

            if(settings.closeOnBackgroundClick){
                hide();
            }

        }).animate({    // jQuery++ CSS3 animation
            'opacity':1
        },400);

        // The popup

        popup = $('<div>').addClass('popup').css({
            width   : 0,
            height  : 0,
            top     : buttonCenter.y,
            left    : buttonCenter.x - 35
        });

        // Append it to the page, and trigger a CSS3 animation
        popup.appendTo(body).animate({
            'width'                 : settings.width,
            'top'                   : windowCenter.y - settings.height/2,
            'left'                  : windowCenter.x - settings.width/2,
            'border-top-width'      : settings.height,
            'border-right-width'    : 0,
            'border-left-width'     : 0
        },200,function(){

            popup.addClass('loading').css({
                'width': settings.width,
                'height': settings.height
            });

            var iframe;

            // If this iframe already exists in the cache
            if(settings.cache && settings.url in frameCache){
                iframe = frameCache[settings.url].show();
            }
            else{

                iframe = $('<iframe>',{
                    'src' : settings.url,
                    'css' : {
                        'width' : settings.width,
                        'height' : settings.height,
                    }
                });

                // If the cache is enabled, add the frame to it
                if(settings.cache){
                    frameCache[settings.url] = iframe;
                    iframe.data('cached',true);
                    settings.onShow();
                }
                else{

                    // remove non-cached iframes
                    frameCacheDiv.find('iframe').each(function(){
                        var f = $(this);
                        if(!f.data('cached')){
                            f.remove();
                        }
                    });
                }

                iframe.ready(function(){
                    frameCacheDiv.append(iframe);
                    setUpAPI(iframe, settings);
                    settings.onShow();
                });
            }

            currentIframe = iframe;

        });

    });

    return this;
};

As I mentioned in the opening section, we are using jQuery++ to enhance jQuery's animate() function to support CSS3 animations. This way we don't have to write tons of CSS, and we also achieve full backwards compatibility, as the new animate() method will fall back to the old if the browser has not support for CSS animations.

Once the first animation is complete, we add the loading class to the .popup div. The new class adds an animated preloader gif to the popup and a soft box-shadow, as you can see by inspecting assets/framewarp/framewarp.css.

framewarp-jquery-plugin.jpg

Using the plugin

To use the plugin, include assets/framewarp/framewarp.css to the head of your page, and assets/framewarp/framewarp.js after your copy of the jQuery library.

After this, all that is left is to initialize the plugin. As an example, here is the code that drives our demo page:

assets/js/script.s

$(function(){

    // If no url property is passed, the
    // href attribute will be used

    $('#b1').frameWarp();

    $('#b2').frameWarp({
        onMessage: function(msg){
            $('#messages').append('Message Received: '+ msg+'
');
        }
    });

    // Cache is enabled by default
    $('#b3').frameWarp({
        url : 'http://www.cnn.com/'
    });

    // Disable caching
    $('#b4').frameWarp({
        url : 'http://www.cnn.com/',
        cache:false
    });
});

Done!

With this the plugin is complete! You can use it to enhance your web app and reuse certain parts of it without writing extra code. I would love to hear your suggestions or thoughts in the comment section below.

Bootstrap Studio

The revolutionary web design tool for creating responsive websites and apps.

Learn more

Related Articles