background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

In this article, I'll share some code that I use to gray out the screen during AJAX calls to prevent users from clicking on anything, and displays a visual cue to the user that their click request is still being processed.

In the following code, I created a hook_menu() implementation and 2 page callbacks; one to show a clickable link and the other to process the AJAX request.

<?php
/**
* Implements hook_menu()
*/
function helper_menu() {

 
$items = array();

 
// define the page callback to show the clickable link 
 
$items['shadow-test'] = array(
   
'title' => t('Shadow Test'),
   
'page callback' => '_helper_page_callback_shadow_test',
   
'access arguments' => array('access content'),
   
'type' => MENU_CALLBACK,
  );
 
 
// define the page callback to process the AJAX request
 
$items['shadow-test-ajax'] = array(
   
'title' => t('Shadow Test Ajax'),
   
'page callback' => '_helper_page_callback_shadow_test_ajax',
   
'access arguments' => array('access content'),
   
'type' => MENU_CALLBACK,
  );
 
  return
$items;

}

/**
* Implements the page callback to show the clickable link
*/
function _helper_page_callback_shadow_test() {

 
// include module css
 
drupal_add_css(drupal_get_path('module', 'helper') .'/helper.css');

 
// include module javascript
 
drupal_add_js(drupal_get_path('module', 'helper') .'/helper.js');

 
// generate shadow html
 
$shadow_html = "<div id='shadow' style='background: url(\"" . base_path() . drupal_get_path('module', 'helper') .'/shadow.png' . "\")'></div>";

 
// define javascript variables to be passed to the DOM
 
$js_vars = array(
   
'helper' => array(
     
'ajax_path' => base_path() . 'shadow-test-ajax',
     
'shadow_html' => $shadow_html,
    ),
  );

 
// pass variables to javascript
 
drupal_add_js($js_vars, 'setting');

 
// create a variable for page output
 
$output = "";
 
 
// create a clickable link
  // NOT: this link will be overwritten via jQuery
 
$output .= l(
   
t('Click Me'),
   
$_REQUEST['q'],
    array(
     
'attributes' => array(
       
'class' => 'ajax_clickable',
      ),
    )
  );
 
 
// return page output
 
return $output;

}

/**
* Implements page callback to process the AJAX request
*/
function _helper_page_callback_shadow_test_ajax() {

 
// wait 2 seconds
  // NOTE: this line is just used to demo the "shadow" effect
 
sleep(3);
 
 
// return a JSON value
 
$ret = new StdClass();
 
$ret->status = true;
  print
drupal_json($ret);

  die;

}
?>

After flushing my menu cache, and browsing to the first page callback, I see the following:

Shadow Off

I added some CSS to my module's CSS include file (helper.css), to help position the "shadow" full screen:

#shadow {
  z-index: 1000;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: none;
}

I created a 50% black transparent PNG image in PhotoShop:

50% black image

I added some jQuery to my module's javascript include file (helper.js), to add the click event to the link, and two functions to hide and show the shadow.

Drupal.behaviors.helper = function(context) {

  // find the link with the matching class, and add a click event
  $('.ajax_clickable').click(function(){
   
    // show shadow
    $(this).show_shadow();

    // make ajax call   
    $.getJSON(
      Drupal.settings.helper.ajax_path,
      function(data) {
       
        // when the ajax call is done, hide the shadow
        $(this).hide_shadow();
       
      }
    );

    // prevent the a tag from actually going anywhere
    return false;
 
  });
 
}

;(function($) {

  // defines the function to show the shadow
  $.fn.show_shadow = function() {
 
    // ensure the shadow does not already exist
    if ($('body #shadow').length == 0) {
     
      // add shadow html, and fade it in
      $('body').append(Drupal.settings.helper.shadow_html).find('#shadow').fadeIn();
         
    }
 
  }

  // defines the function to hide the shadow
  $.fn.hide_shadow = function() {
 
    // fade it out, and then remove it from the dom
    $('#shadow').fadeOut('slow', function() {
      $(this).remove();
    });
 
  }
 
})(jQuery);

Now, when I click on the link the page fades to black and the user cannot click on anything. After the 3 second delay (defined in the AJAX callback), the black shadow fades away.

Shadow On

In this tutorial I'll explain how to setup your Drupal site to be mobile friendly. Before you begin, it's helpful to consider the following: 1) which mobile devices to support; 2) using a different theme for mobile; 3) which hostnames will be used; 4) multi-site configuration options; and 5) any site alterations to simplify the mobile experience.

For my site, I decided on the following:
1) iPhones (to start)
2) Use a more clean theme for my mobile site: DevSeed's Singular
3) redirect mobile traffic to a new subdomain (mobile.ericlondon.com)
4) I was using sites/default for my blog, so hosting another hostname on the same filesystem was not an issue
5) I decided to remove some items in my primary navigation, and add some jQuery (accordion effect) to collapse node content on my front page.

I added the new DNS for my subdomain to point to the same IP address of my main site. In a standard Apache vhosts configuration, you can add a ServerAlias directive to ensure the mobile hostname is handled by the main site's vhost. For example:

<VirtualHost *:80>
  ServerName ericlondon.com
  ServerAlias mobile.ericlondon.com
  DocumentRoot /var/www/vhosts/ericlondon.com/httpdocs
</VirtualHost>

I installed and enabled the Singular theme in sites/all/theme/singular.

I added some mobile specific configurations in my settings.php file (sites/default/settings.php):

<?php
/**
* Mobile Theme Configuration
*/

// define mobile http host
define('MOBILE_HTTP_HOST', 'mobile.ericlondon.com');

// define mobile theme
define('MOBILE_THEME','singular');

// override custom theme for mobile site
if ($_SERVER['HTTP_HOST'] == MOBILE_HTTP_HOST) {
 
$GLOBALS['custom_theme'] = MOBILE_THEME;
}

// check for iPhone
$is_iphone = preg_match('/iphone/i', $_SERVER['HTTP_USER_AGENT']); 

// redirect to mobile theme
if ($is_iphone && $_SERVER['HTTP_HOST']!=MOBILE_HTTP_HOST) {
 
header('Location: http://' . MOBILE_HTTP_HOST . $_SERVER['REQUEST_URI']);
  die;
}
?>

Now, if someone visits my site using an iPhone, the user would be redirected to my specified mobile address, AND a new mobile theme would be used!

Mobile iPhone

In addition, I decided to make some alterations to my mobile theme to simplify the interface. I created a module and added a hook_preprocess_page() implementation:

<?php
function MYMODULE_preprocess_page(&$vars) {

 
// only process page variables if this is the mobile address
 
if ($_SERVER['HTTP_HOST'] != MOBILE_HTTP_HOST) {
    return;
  }

 
// define mobile javascript
  /*
  NOTE: this jQuery is very specific to my theme, and is just shown as an example.
  I looked into using jquery_ui's accordion library, but it would not work out of the box with the structure of my new mobile theme :(
  In an ideal situation, all jQuery would be put in separate include files
  */
 
$js = "
 
    // define node container
    var node_container = '.front #page #content';
     
    // define function to collapse node content
    function collapse_nodes() {     
      $(node_container + ' .node div').hide();
    }
 
    // define function to add click event
    function node_title_add_click() {
      $(node_container + ' .node h2.node-title a').click(function(){
        collapse_nodes();
        $('div', $(this).parent().parent()).show();
        return false;
      });
    }
   
    $(document).ready(function(){

      collapse_nodes();
      node_title_add_click();
     
    });
  "
;
 
drupal_add_js($js, 'inline');
 
 
// rebuild scripts variable
 
$vars['scripts'] = drupal_get_js();
 
 
// determine a list of hrefs to remove from primary navigation
 
$href_remove = array(
   
'drupal',
   
'tagadelic/chunk/1',
   
'recent-posts',
   
'rss.xml',
   
'logout',
   
'contact',
  );
 
 
// loop through primary links and remove as necessary
 
if (is_array($vars['primary_links'])) {
    foreach (
$vars['primary_links'] as $key => $value) {
      if (
in_array(strtolower($value['href']), $href_remove)) {
        unset(
$vars['primary_links'][$key]);
      }
    }
  }
 
}
?>

By added the above module code and jQuery, I removed some items from my primary navigation and added an accordion-like interface for the front page:

Mobile theme

NOTE: If you're using a Mac, the iPhone Simulator application (which comes with Xcode + iPhone SDK) is a great way to development and test mobile configurations.In this tutorial I'll explain how to setup your Drupal site to be mobile friendly. Before you begin, it's helpful to consider the following: 1) which mobile devices to support; 2) using a different theme for mobile; 3) which hostnames will be used; 4) multi-site configuration options; and 5) any site alterations to simplify the mobile experience.

For my site, I decided on the following:
1) iPhones (to start)
2) Use a more clean theme for my mobile site: DevSeed's Singular
3) redirect mobile traffic to a new subdomain (mobile.ericlondon.com)
4) I was using sites/ericlondon.com for my blog, so hosting another hostname on the same filesystem was not an issue
5) I decided to remove some items in my primary navigation, and add some jQuery (accordion effect) to collapse node content on my front page.

I added the new DNS for my subdomain to point to the same IP address of my main site. In a standard Apache vhosts configuration, you can add a ServerAlias directive to ensure the mobile hostname is handled by the main site's vhost. For example:

<VirtualHost *:80>
  ServerName ericlondon.com
  ServerAlias mobile.ericlondon.com
  DocumentRoot /var/www/vhosts/ericlondon.com/httpdocs
</VirtualHost>

I installed and enabled the Singular theme in sites/all/theme/singular.

I added some mobile specific configurations in my settings.php file (sites/ericlondon.com/settings.php):

<?php
/**
* Mobile Theme Configuration
*/

// define mobile http host
define('MOBILE_HTTP_HOST', 'mobile.ericlondon.com');

// define mobile theme
define('MOBILE_THEME','singular');

// override custom theme for mobile site
if ($_SERVER['HTTP_HOST'] == MOBILE_HTTP_HOST) {
 
$GLOBALS['custom_theme'] = MOBILE_THEME;
}

// check for iPhone
$is_iphone = preg_match('/iphone/i', $_SERVER['HTTP_USER_AGENT']); 

// redirect to mobile theme
if ($is_iphone && $_SERVER['HTTP_HOST']!=MOBILE_HTTP_HOST) {
 
header('Location: http://' . MOBILE_HTTP_HOST);
  die;
}
?>

Now, if someone visits my site using an iPhone, the user would be redirected to my specified mobile address, AND a new mobile theme would be used!

Mobile iPhone

In addition, I decided to make some alterations to my mobile theme to simplify the interface. I created a module and added a hook_preprocess_page() implementation:

<?php
function MYMODULE_preprocess_page(&$vars) {

 
// only process page variables if this is the mobile address
 
if ($_SERVER['HTTP_HOST'] != MOBILE_HTTP_HOST) {
    return;
  }

 
// define mobile javascript
  /*
  NOTE: this jQuery is very specific to my theme, and is just shown as an example.
  I looked into using jquery_ui's accordion library, but it would not work out of the box with the structure of my new mobile theme :(
  In an ideal situation, all jQuery would be put in separate include files
  */
 
$js = "
 
    // define node container
    var node_container = '.front #page #content';
     
    // define function to collapse node content
    function collapse_nodes() {     
      $(node_container + ' .node div').hide();
    }
 
    // define function to add click event
    function node_title_add_click() {
      $(node_container + ' .node h2.node-title a').click(function(){
        collapse_nodes();
        $('div', $(this).parent().parent()).show();
        return false;
      });
    }
   
    $(document).ready(function(){

      collapse_nodes();
      node_title_add_click();
     
    });
  "
;
 
drupal_add_js($js, 'inline');
 
 
// rebuild scripts variable
 
$vars['scripts'] = drupal_get_js();
 
 
// determine a list of hrefs to remove from primary navigation
 
$href_remove = array(
   
'drupal',
   
'tagadelic/chunk/1',
   
'recent-posts',
   
'rss.xml',
   
'logout',
   
'contact',
  );
 
 
// loop through primary links and remove as necessary
 
if (is_array($vars['primary_links'])) {
    foreach (
$vars['primary_links'] as $key => $value) {
      if (
in_array(strtolower($value['href']), $href_remove)) {
        unset(
$vars['primary_links'][$key]);
      }
    }
  }
 
}
?>

By added the above module code and jQuery, I removed some items from my primary navigation and added an accordion-like interface for the front page:

Mobile theme

NOTE: If you're using a Mac, the iPhone Simulator application (which comes with Xcode + iPhone SDK) is a great way to development and test mobile configurations.

For some time now I've wanted to write a blog entry about using AHAH to create dynamically generated form elements. After a recent conversation at work regarding usability, I now had a real world example to create: how to use tiered taxonomy to dynamically generate a form. This code snippet will show you how to create a form that creates child select dropdowns based on the parent taxonomy term the user selects.

First I established a multi-tier taxonomy called "AHAH":

For this example I created a menu callback to display my initial form:

<?php
function helper_menu() {
 
 
$items = array();
 
 
$items['ahah-form'] = array(
   
'title' => 'AHAH Form',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('_helper_callback_ahah_form'),
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
  return
$items;
 
}
?>

I then defined the page callback to show the initial form:

<?php
function _helper_callback_ahah_form() {

 
// define an array to contain form elements
 
$form = array();
 
 
// define the top level vid
 
$vid = 2;
 
 
// fetch a tree of taxonomy elements
 
$tree = taxonomy_get_tree($vid, 0, -1, 1);
 
 
// loop though taxonomy and collect elements
 
$options = array();
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// create the first select dropdown input
 
$form['select_1'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 1'),
   
'#size' => 5,
   
'#multiple' => false,
   
'#ahah' => array(
     
'event' => 'change',
     
'path' => 'ahah-form-callback',
     
'wrapper' => 'wrapper-1',
     
'method' => 'replace',
    ),
  );
 
 
// pass the top level vid in the form
 
$form['ahah_vid'] = array(
   
'#type' => 'hidden',
   
'#value' => $vid,
  );
 
 
// create an empty form element to contain the second taxonomy dropdown
 
$form['wrapper_1'] = array(
   
'#prefix' => '<div id="wrapper-1">',
   
'#suffix' => '</div>',
   
'#value' => '&nbsp;',
  );
 
 
// add a form submit button
 
$form['submit'] = array(
   
'#value' => 'Submit',
   
'#type' => 'submit'
 
);
 
  return
$form;
 
}
?>

The above form callback produces the following:

Next, I defined a callback to handle the AHAH page request:

<?php
// new menu item:
function helper_menu() {
 
 
// ...
 
 
$items['ahah-form-callback'] = array(
   
'title' => 'AHAH Form Callback',
   
'page callback' => '_helper_callback_ahah_form_callback',
   
'type' => MENU_CALLBACK,
   
'access callback' => 'user_access',
   
'access arguments' => array('access content'),
  );
 
 
// ...

 
return $items;
 
}

// and, here's the AHAH callback used to create the new form elements:
function _helper_callback_ahah_form_callback() {
 
 
// define a string variable to contain callback output
 
$output = "";
 
 
// pull the top level vid from the $_POST data
 
$vid = $_POST['ahah_vid'];
 
 
// pull the selected dropdown from the $_PODT data
 
$parentVid = $_POST['select_1'];
 
 
// loop through the taxonomy tree and fetch child taxonomies
 
$options = array();
 
$tree = taxonomy_get_tree($vid, $parentVid, -1, 1);  
  foreach (
$tree as $key => $value) {
   
$options[$value->tid] = $value->name;
  }
 
 
// define the second tier select dropdown element
 
$form['select_2'] = array(
   
'#type' => 'select',
   
'#options' => $options,
   
'#title' => t('Select 2'),
   
'#size' => 5,
   
'#multiple' => false,
  );
 
 
// rebuild form object and output new form elements
 
$output .= ahah_render($form, 'select_2');
 
 
// render form output as JSON
 
print drupal_to_js(array('data' => $output, 'status' => true));
 
 
// exit to avoid rendering the theme layer
 
exit();
 
}

// Lastly, here's a help function pulled from Nick Lewis's blog to alter the form
// see: http://www.nicklewis.org/node/967
// NOTE: based on poll module, see: poll_choice_js() function in poll.module
function ahah_render($fields, $name) {
 
$form_state = array('submitted' => FALSE);
 
$form_build_id = $_POST['form_build_id'];
 
// Add the new element to the stored form. Without adding the element to the
  // form, Drupal is not aware of this new elements existence and will not
  // process it. We retreive the cached form, add the element, and resave.
 
$form = form_get_cache($form_build_id, $form_state);
 
$form[$name] = $fields;
 
form_set_cache($form_build_id, $form, $form_state);
 
$form += array(
   
'#post' => $_POST,
   
'#programmed' => FALSE,
  );
 
// Rebuild the form.
 
$form = form_builder($_POST['form_id'], $form, $form_state);

 
// Render the new output.
 
$new_form = $form[$name];
  return
drupal_render($new_form);
}
?>

The above code allows the user to select an option from the top level tier of taxonomy and the AHAH callback will generate the a select dropdown of the child taxonomies as shown below:

On form submission, you'll see that the options the user selected as stored in $form_state['values']['select_1'] and $form_state['values']['select_2']

This article will show you how to establish parent and child relationships between nodes. To start, you'll need to install and enable the CCK module, and node reference (which comes with CCK). Next, you'll need to create two node types (admin/content/types/add). For my example, I created two node types: Organization (parent) and Department (child), both consisting of a title and body (of course, you can add as many fields as you'd like).

Next, you'll want to add a new field to the parent node (organization) by editing the content type (admin/content/node-type/organization) and clicking on the "Manage fields" tab. Enter the information to add a new reference field on this screen. Add a label (ex: "Department Reference"), enter a field name (ex: field_ref_department), choose "Node reference" as the field type, and choose "Autocomplete text field" as the form element as shown below.

node reference add field 1

On the next screen you can choose the field options. For this example, I changed "Number of values" to "Unlimited" (to allow a one to many relationship), selected "Department" in the list of content types that can be referenced, and saved these settings.

node reference add field 2

Now, if you create a new parent node (Organization: node/add/organization), you'll have the option of adding pre-existing child (Department) nodes. As you start typing the title of the child nodes in the node reference auto-complete field, it will automatically populate the field with nodes that match. Of course this will only work if you have already created child nodes. At this point adding relationships is a two step process.

node reference add parent node 1

To improve usability, you can install the Popups API (http://drupal.org/project/popups) and Add and Reference (http://drupal.org/project/popups_reference) modules which allow you to add child nodes without leaving the node/add/PARENT-NODE screen. After installing and configuring these modules, you'll be able to add child nodes directly from the node/add/PARENT-NODE screen by clicking on the "Add new: Add Child Node link", which spawns a lightbox window to add a child node.

node reference add parent node 2

Here is a screenshot of a parent node view after adding child relationships:

node reference parent node view

Eric.London's picture

If someone clicks on a taxonomy term they land on a page showing all the content tagged with that term. The title on those pages is simply the taxonomy term. I thought it would be more usable if I changed the verbiage of the title to let the user know what they are viewing. This code snippet shows how you can change the title for the taxonomy term landing pages:

<?php
function MYTHEME_preprocess_page(&$variables) {
  if (
arg(0)=='taxonomy' && arg(1)=='term') {
   
$variables['title'] = "Content tagged with: " . $variables['title'];
  }
}
?>

Syndicate content