Skip to main content

Drupal 7: Load content nodes into a modal overlay using AJAX

by jason

For one of our current projects (Aiko) we are building a site influenced by the Bloomberg L.P., company site, by Frog Design and the website, Pinterest. The design dictates that most of the site content is presented using a dropdown menu approach. Normally, a dropdown functions best when all of the content is already loaded on the page, and just relies on javascript to animate the display and hiding of each dropdown element.  For this design, it is pretty obvious that requiring all of the overlay content to load along with the initial page is excessive for both load times, as well as complicated for the CMS organization and structure.

AJAX to the rescue.  By loading each of the individual content nodes "on demand" we can cut the unnecessary burden of loading everything up front, and only load the content that is actually requested by the visitor.  Now the question becomes... how do we best do this in Drupal 7?

My solution involves the following approach:

  1. Add a class selector to all links that are meant to open in the overlay (class="overlay").
  2. Use jQuery to find all of these links in the document, and attach the click behavior of loading href into the desired overlay element via ajax, animate the effects, and return false to avoid firing the actual link to a separate page.
  3. Normally the link destination would load the entire page all over again within the overlay element, thus creating an infinite nesting problem (as well as breaking the design).  This is avoided by asking drupal to load these special "overlay" designated links in a different html.tpl.php and page.tpl.php so we can control which parts of the surrounding page are loaded into the overlay.  This is done using the theme's template.php preprocess functions and altering the href sent through ajax.
  4. We also want the url to correctly reflect which overlay page is currently open, and allow direct linking to a specific overlay.  This is handled with jQuery/javascript and dynamic updating of the page hashtag.  Additionally, when a page is first loaded an existing hash is parsed to determine what to display immediately.
  5. Lastly, each overlay may contain a slideshow of content.  If so, we also include this additional information in the hash to also allow for direct linking.

The following modules were used to implement this solution:
menu attributes: to add the class designation to selected menu links

CODE

mytheme/template.php
<?php
function mytheme_preprocess_page(&$vars) {
  if ( isset($_GET['overlay']) && $_GET['overlay'] == 'true' ) {
        $vars['theme_hook_suggestions'][] = 'page__overlay';
  }
}
function mytheme_preprocess_html(&$vars) {
  if ( isset($_GET['overlay']) && $_GET['overlay'] == 'true' ) {
        $vars['theme_hook_suggestions'][] = 'html__overlay';
  }
}
?>
mytheme/script.js
// JavaScript Document

jQuery(document).ready(function () {

  // set overlay-wrap to be hidden from view
  jQuery('#overlay-wrap').css('height', '0px');

  // find all overlay links and add overlay functionality
  jQuery('a.overlay').each( function() {
    jQuery(this).click( function() {
      var href=jQuery(this).attr('href');      // page url to load
      var name=jQuery(this).attr('name');      // new page name
      var rel =jQuery(this).attr('rel');       // new page group
      updateHash(name);
      loadPage(href);
      updateActive(href, rel);
      return false; // important to keep the page from loading in a new window
    });
  });

  // If hashtag exists, load the appropriate page and slide on page load.
  if(hash = parseHash()) {
    if(hash['page']) {
      var href = jQuery('a[name="'+hash['page']+'"]').attr('href');
      var rel = jQuery('a[name="'+hash['page']+'"]').attr('rel');
      var slide = hash['slide'];
      loadPage(href, rel, slide);
      updateActive(href, rel);
    }
  }

});

/* UTILITY FUNCTIONS FOR OVERLAY MANAGEMENT */

// function to update which link is active for styling purposes
function updateActive(href, rel) {
  // remove active from all links
  jQuery('a.active').removeClass('active');
  // add active to all links with same destination
  jQuery('a[href= "'+href+'"]').addClass('active');
  // add active to main level link regardless of destination
  jQuery('#main-menu a[name="'+rel+'"]').addClass('active');
}

// function to update the Hash after the content has changed
function updateHash(page, slide) {
  var list = 'page='+page;                     // add page hash
  if (slide) list += '&slide='+slide;          // add slide hash if exists
  window.location.hash = list;                 // update hash
}

// function to load the content via ajax and animate the overlay
function loadPage(href, rel, slide) {
  // fade out current content
  jQuery('#overlay').stop(true,true).fadeTo('fast', 0, function() {
    // determine if overlay is already open, if not animate it immediately
    if (!jQuery('#overlay-wrap').height()) {
      jQuery('#overlay-wrap').stop(true,true).animate({ height: 200 }, 'slow');
    }
/** 
*   NOTE: the additional '?overlay=true' is added to trigger the new 
*   templates in the template.php file
**/
    jQuery('#overlay').load( href+'?overlay=true', function () {
      if (slide) jQuery('#'+slide).click();  // cycle to correct slide
      // animate the height to fit the new content (within callback after load)
      jQuery('#overlay-wrap').stop(true,true).animate({ 
        height: jQuery('#overlay').height()+10 
      }, 'fast', function() { 
        jQuery('#overlay').stop(true,true).fadeTo('fast', 1.0); // fade in
      });
    });
  });
}

// function to extract data from the existing hash
function parseHash() {
  hash = window.location.hash.substring(1);   // load hash string
  if (!hash) return false;                    // return false if no hash
  var varlist= hash.split("&");               // break hash into variables
  for (i=0; i < varlist.length; i++) {        // cycle through variables
    var vars = varlist[i].split("=");         // split variable name from value
    varlist[vars[0]] = vars[1];               // assign variable value to array
                                              // indexed by variable name
  }
  return varlist;                             // return variable array
}

// function to close the overlay
function closePage() {
  jQuery('#overlay').stop(true,true).fadeTo('fast', 0, function() {
    jQuery('#overlay-wrap').stop(true,true).animate({
      height: 0
    }, 'fast', function() {
      jQuery('#overlay').html('');
      jQuery('a.active').removeClass('active');
      window.location.hash = '';
    });
  });
}
mytheme/html--overlay.tpl.php
<div id="overlay-close"></div>
<?php print $page; ?>
<script type="text/javascript">
  // close button
  jQuery('#overlay-close').click(function() { closePage() });
</script>
mytheme/page--overlay.tpl.php
<div class="overlay-content">
  <?php print render($page['content']); ?>
</div>
mytheme/page.tpl.php (add this code where you want the modal overlay to be located)
<div id="overlay-wrap"><div id="overlay"></div></div>

Comments

Thanks for sharing your solution.  I recently did something similar but with about twice as much code. FYI,  you can also avoid needing the Menu Attributes module by adding a preprocess function that adds the necessary classes to your menu items. 

I have been searching for a way to achieve this for some time, thank you for sharing!

Exactly what I was looking for Thx!

Jason, I would to thank you a million times. Thank you very much for sharing your idea!

Keep up that good work!

Thanks a lot for this !

I'm now trying to make this work generating my links within views.

I'm rewriting my links using "Output this field as a link", but couldn't find how to add a "name" attribute to make dynamic path to work.

Any idea ? Thanks again !

I can't get this to work. Should all files go in the root of the theme including script.js?

Thank You !

Echoing everyone else's sentiments: THANK YOU! Wonderfully executed.

One note: Perhaps I missed it somewhere in your explanation, but it took me a while to figure out that I had to add (something like) this to my theme's page.tpl.php in order to get the content to actually show up: 

<div id="overlay-wrap">     <div id="overlay" class="clearfix">&nbsp;</div> </div>

I see it's probably a good solution for my website. I created views gallery and would like to open node content when user click an image but it not clear for me how to do it in views. Is it possible?

So, I created a link in my views, now it works, thank you! Could you give me a clue what to include in hash in case I'd like to group my images in overlay like lightbox does? If it's possible, of course.

I was trying to accomplish something very similar and I ended up discovering this excellent approach to the matter.

One question related to the manipulations of the hastag for reflecting which overlay page is currently opened: Have you ever considered how the HTML5 History API would be inserted in your approach? 

Nice approach. I am however confused about which overlay you are aiming to use here (default Drupal overlay? Jquery UI?). After a quick try with your code, my ccontent is properly loaded through AJAX, but within the page, not in an overlay. What am i missing? 

thanks

Thank you so much, this is working like a charm. I have 1 issue though, for a specific case. I am opening a /node/add/content_type in a modal this way, but when someone uses the image upload it refreshes, jumps out of the overlay and into a blank page. It's still calling the tpl files and renders what I would want in the modal, it's just not staying in the modal. It is loading the url of /node/add/content_type?overlay=true

Any thoughts on that?

Thanks!

@Jason - I figured it out. It's as simple as adding <?php print $scripts; ?> to the html--overlay.tpl.php file. The Ajax functions for file uploads didn't exist, thus it broke itself. It works now.

Thanks for the help.

Hi,

This example was great. I applied it on my portfolio and added a loading animation to it, but there's a problem. After you load a few pages the page freezes, like the cache is getting out of hand on your browser or serverside. What way is there to basically flush out the content before the next round comes in?

Hey,

I just wanna thank you for the debugging stuff. It helped me find the root of this problem which was the GET method was pulling out the headers and all from the page, which meant that the preprocess_page HTML was not loading and I found out that it was because I had $variables, not $vars on my function. Fixed now and loading works as intended. Thank you!

To stop the page from scrolling to the top after closePage(), change:

window.location.hash = '';

to:

window.location.hash = 'none';

Hi Jason. thank you so much. your instruction was help me alot. so. there are some problems. i'm using drupal7x then, when i load a content page via your code, some jquery plugin did not load through( like CKEditor or my custom jquerys..)

when debug with chrome, i checked in header tag, and no jquery script tag has been loaded...

my english is not good very much, hope you can understand the problem i'm facing.

p/s: i have 4 months experience with drupal, and 2 weeks with jquery (start when i read your instruction :D to know how to use your code)

please help..

some things were wrong...the truth is ckeditor jquery plugin has not been loaded and my custom jquery to hide filter tip has been loaded but take no effect.

my custom jquery code:

<code>jQuery(document).ready(function () {

jQuery(".filter-wrapper").hide();

});</code>

p/s: i tested with node/add/content_type url

@jason: you can go into http://tantam.com.vn

i set new basic page link is load via overlay and article is normal load

check both of them and you will see poorly editor in overlay

all i do is copy all of your code then insert a line code in page.tpl.php: <?php drupal_add_js(path_to_theme().'/script.js','file');?>

hope you can solve this.

ps: the editor i'm using is wysiwyg and ckeditor 3.6.6.7568 in libraries

thank you so much.

so i discorvered if i disable html__overlay. it means i use only page--overlay template then ckeditor is loaded nearly perfect.

i read code carefully but don't know why.

and do u have any experience with ctools modal form?

if yes, the questions is: should i use it for create modal form?

i got some troubles when using ctools modal api, maybe.. it is troubles with ajax, json and drupal :(

Hi, It seemed a bit laggy, I have to wait a few seconds before modal opening. You can check it here: http://fabienlefrapper.me/cinelatin/programme (by clicking on a picture).

I have another problem, the url doesn't change properly, it displays 'page=undefined'. The content I load in the modal is a view. I'm a bit new to preprocess function and jquery, so I don't really know where does these issues come from.

Thanks ;)

By the way, thank you very much for this little tutorial, it helped me a lot !

Thank you for this tutorial it was very useful for me!

@Fabien you must edit the js code and change it in the the corresponding line for something like this:  var name=jQuery(this).attr('title');

Change 'name' with 'title', then edit your view and give the "title" the value you want. Read the previous comments!

 

@Kundu Thank you very much, I'm gonna try it right now. I read all the comments, but I didn't really understand how to figure out. 

So, it worked. But I still have some problems, @Kundu, do you have a link I can check to see how it works on your installation ? 

Mine seems really laggy. I think that it comes from the content I'm trying to load into the modal (some pictures and text), but maybe I can speed it up via the .js file ?

Thanks again

Thank you very much Jason for answering that quickly. My code is a bit dirty, I'm gonna clean it first. 

Then, I'll try to understand why the content is that long to load

what if I need to close modal clicking just outside modal, not close button