Send us some feedback

use this form to send us an email.
we’d love to hear from you!

By submitting this form, you accept the Mollom privacy policy.
[-] hide

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

January 19, 2012 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>

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. 

@Ken: Thanks for the suggestion.  In my case, the overlay links are not on all menu links, so there needs to be more selective control than using a preprocess function would allow.  If you are going for all menu links, then you don't even need a preprocess function as you can make the selector just hit every link within the menu structure.

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?

Sorry for the delayed response.  I hope you've got your problems sorted out by now, but if not here are some answers:

@jm: the name attribute is just a placeholder to differentiate this link from the other overlay links and to describe the page in the url in a friendly way.  You could probably make use of the "title" attribute to do the same thing.  Just swap out all uses of the name attribute with the title attribute.

@Peter: This will depend on your theme and how you have implemented it.  The template.php file is always in the theme root folder (as far as I have seen).  The various "tpl.php" files can either be in the theme root or optionally in a folder called "templates".  Place them wherever your theme keeps the other files of that type.  The javascript file can be located pretty much anywhere as long as it is added to your theme in the ".info" file.  Normally the "scripts.js" file is located in the theme root or a subfolder named "js", but since you manually add it with the callout in the theme.info file, you can really decide wherever makes sense for your theme.

Add new comment

Comment Form

  • Allowed HTML tags: <a> <em> <u> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <span> <div> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.