Drupal 7

Building a responsive website with Display Suite

I've been excitied about Display Suite recently, so this blog entry is just a cursory overview. For more info on Display Suite - watch the screencasts. Also, I think most people are familiar with responsive web design so I just touch basics. More info on that over at A List Apart though.

Display Suite

The nice thing about Display Suite is it takes preprocess logic out of template.php and makes it drag & drop configurable in the administration area. Things like;

  • Hiding the page title for certain pages
  • Displaying blocks related to a node on their pages
  • Rearranging node fields into column layouts without touching any templates
  • Adding view modes without using info_alter hook
  • Adding custom entity and preprocess fields

These are all configurable through the Display Suite admin.

Also, 0 templates. With Display Suite & View Modes - you can build pretty complex layouts without touching any templates and writing very little code. Technically, you could use a theme with 0 template files (drupal core templates), just a .info file and a css file.

Field-based layouts

Customizing layouts has become quite a bit easier in Drupal 7 with field-based layouts. Content fields like the body, title, display date etc. can be output in different regions. Display Suite, like panels allows you to configure region-based layouts for any content types. So for example the blog could be configured to use a layout like the grid / image at the top of this post instead of the traditional vertical layout.

Under Structure -> Layout -> Display Suite and 'Manage Display' you can select a new region-based layout for any content type. Display Suite comes with some built in layouts but you can create your own.

Building custom Display Suite layouts is easy with drush. On mac osx, drush is easily installed using homebrew.

I develop / test sites on my desktop using MAMP in a folder called "development" so I 'cd' into it with the command line.

drush help ds-build

This command shows syntax for creating custom display suite layouts.

drush ds-build "lasso layout" --regions="Left, Nested Top, Nested Center, Nested Bottom, Right" --css="lasso_layout.css"

This builds a folder called "lasso_layout" that includes a template file called "lasso-layout.tpl.php" which contains my new regions. I've also included a css file, "lasso_layout.css". Inside lasso-layout.tpl.php I've wrapped the three nested regions in a div, "wrap-nested" which is the hypothetical lasso. This custom layout generated by drush includes the barebones regions and .inc settings file that are needed but can be further configured just like a custom panels layout (they are the same and use ctools) -- explained here on Drupal.org.

This is html / php included in the layout tpl file ("lasso-layout.tpl.php");

<div class="<?php print $classes; ?> clearfix">
 
  <?php if (isset($title_suffix['contextual_links'])): ?>
    <?php print render($title_suffix['contextual_links']); ?>
  <?php endif; ?>
 
  <?php if ($left): ?>
    <div class="ds-left<?php print $left_classes; ?>">
      <?php print $left; ?>
    </div>
  <?php endif; ?>
 
 <div class="wrap-nested">
  <?php if ($nested_top): ?>
    <div class="ds-nested-top<?php print $nested_top_classes; ?>">
      <?php print $nested_top; ?>
    </div>
  <?php endif; ?>
 
  <?php if ($nested_center): ?>
    <div class="ds-nested-center<?php print $nested_center_classes; ?>">
      <?php print $nested_center; ?>
    </div>
  <?php endif; ?>
 
  <?php if ($nested_bottom): ?>
    <div class="ds-nested-bottom<?php print $nested_bottom_classes; ?>">
      <?php print $nested_bottom; ?>
    </div>
  <?php endif; ?>
 </div><!-- /.wrap-nested -->
 
  <?php if ($right): ?>
    <div class="ds-right<?php print $right_classes; ?>">
      <?php print $right; ?>
    </div>
  <?php endif; ?>
 
</div>

That's it. Customize the html if you need but don't add grid classes yet, that comes next and can be done in the administration through display suite. Move the layout into your theme folder and flush the cache.

mkdir ds_layouts
mv lasso_layout ds_layouts
mv ds_layouts sites/all/themes/mytheme
drush cc all

You can then move fields into the new "lasso layout".

Responsive layout

Display Suite layout features include the ability to add html classes to your fields for css styling. This makes it prime for including grid columns into your layout. You first need to include all of your grid classes then they will be available as point and click for the layout fields.

In the administration under Structure -> Display Suite -> Layout you can configure the layout for any content types under 'Manage Display'. At the bottom is a tab option for, 'Extra classes for regions'. In 'Manage region styles' you can include all your grid classes

Responsive grid

Gridpak responsive layout generator. Building a responsive grid is fairly easy these days as outlined in Ethan Marcotte's book. His formula is as so;

  • Fluid grid formula: target/content = result
  • Fluid images: img{max-width:100%;}
  • Media queries: @media screen and (min-width: 320px) etc.

But if you don't want to go to all that trouble of building your own grid from scratch there are some nice generators out there that can help you along your way. One such nice one is gridpak. You can configure your number of columns, gutter width's and breakpoints. Then just download a zip file with all your grid classes and pop them into Display Suite.

View Modes

You can't talk about Display Suite without talking about view modes. View Modes are a powerful new feature in Drupal 7. Traditionally you have two view modes, a "teaser" view mode and a "full" view mode. The teaser obviously displays a shorter version of the full view mode and links to the full version -- which shows all the content. Now you can create your own view modes. "Lasso layout" can be just one view mode for the blog, another view mode could be a different custom region-based layout.

Through Display Suite you can create new view modes under Structure -> Content Types -> Manage Display -> Manage View Modes.

Adding custom view modes in a module without using Display Suite need to implement hook_entity_info_alter(), example;

/**
 * Implements hook_entity_info_alter()
 */
 function mymodule_entity_info_alter(&info) {
    info['node']['view modes'] += array(
       'Slideshow' => array(
           'label' => t('Slideshow'),
           'custom settings' => TRUE,
        ),
    );
}

Example, one view mode could be a slideshow. Just show an image field in that particular view mode and wrap it with some slideshow widget or jquery.

More great info on view modes in this excellent screencast.

Display View Modes not View fields

Views is so powerful you can build any layout with it by putting together view fields, filtering by content types, sorting etc.. It's drawback is too many template files when you need to customize and include php.

Display view modes not fields and cut down on templates and php.

Custom Fields

For extra logic, create custom fields. Structure -> Display Suite -> Fields.

<?php print $entity->type; ?>

Print the article type. $entity is the object for either the node, taxonomy term or user.

I'm still playing around with View Modes and Display Suite so maybe I'll blog more on them later. But there's more real awesome features under the "Extra's" area of Display Suite. Including "region to block", display blocks semantically by node id. Configure the field templates (remove html). Integration with panels and more.

Finally though -- you can write less preprocess functions and customize fewer template files.

Media Queries and mobile first

I was talking about responsive web design...

I won't go into this much since it's been done and is best explained in the short but concise A Book Apart and Luke's blog. The best way to build media queries is using the mobile first method. This entails building the site for how it will look in a mobile browser first then using media quieries for the breakpoints as the layout expands for tablet's and desktop's. In this case, media queries use the 'min-width' syntax instead of the 'max-width'.

 @media screen and (min-width: 320px) {
 
}

Meta Tags

Don't forget meta tags so mobile browsers stop natively using a 980pixel layout and instead use your responsive layout. These can go inside html.tpl.php.

  <meta name="MobileOptimized" content="width">
  <meta name="HandheldFriendly" content="true">
  <meta name="viewport" content="width=device-width">
  <meta http-equiv="cleartype" content="on">

Helpful modules

Some modules I've found helpful for building responsive layouts include;

 

∗ Permalink

Contextual blocks

Sometimes you want to display content from a node in a separate region, like a sidebar. A nice way of doing that is to add an additional field to your content type. Call it something like, "sidebar body". Hide it's display under "manage display". Then create a new view block that uses a 'contextual filter' of type 'Content: Nid'. Check off "provide a default value" and select "Content ID from URL" in the dropdown. Add your 'sidebar body' field into the view fields then put your block in the sidebar. The contents of that field will now display in the block on the page where the rest of the node content is displayed.

What about the title

Instead of hard setting the title for that block, make it dynamic. Add an additional field to your content type called, "sidebar body title". Set the title for that block to your new field through hook_block_view_alter(). Here's an example from a project I did recently,

/**
 * Implementation of hook_block_view_alter()
 */
function awesometheme_block_view_alter(&$data, $block) {
 
   //We grab the field 'sidebarbody title' (in 'basic_page' content type) from the node and set it equal to the title
   //of the block that spits out 'sidebar body'
    if (isset($block->info)) {
         if ( $block->info == 'View: Basic Node Content Revisions: Sidebar' ) {
               if ($node = menu_get_object()) {
                      if (isset($node->field_sidebar_title[$node->language]['0']['value'])) {
                            $block->title = $node->field_sidebar_title[$node->language]['0']['value'];
                       }
                 }
            }
      }
}

Using the devel module (include kpr($data) to look inside data, kpr($block) to look inside block) I looked for my block and something unique about it. In this case I grabbed the contents in block->info because it was unique about the block. menu_get_object() is another way of looking at the node instead of using node_load(), the node is often stored in the menu_router on node pages. Looking at the contents of the node (using kpr($node) inside the menu_get_object() statement), I set the sidebar_title field equal to the title of the block.

*update: This can be also done through Display Suite region to block functionality, described in the seventh screencast - link on the module page.

 

∗ Permalink

Ctools modalframe for node creation forms

Expanding on my calendar and module code here, this is what I used to open a calendar event creation form into a ctools modalframe popup.  The content type is, "public_event". ctools_modal_text_button() creates the button that loads the modalframe, which I've added to specific block content on my site through hook_block_view_alter.

<?php
 
/**
 * @file
 * Create Event module file
 *
 * This module allows anonymous users to create calendar events.
 */
 
/**
 * Implementation of hook_menu
 */
function mymodule_menu() {
  $items = array();
  $items['create/%ctools_js/public-event'] = array(
      'title' => 'Submit an event',
      'page callback' => 'modal_content',
      'page arguments' => array(1),
      'access callback' => TRUE,
      'type' => MENU_CALLBACK,
  );
 
  return $items;
}
 
 
/**
 * Function for creating ctools text button
 */
function mymodule_link() {
 
  global $user;
 
  ctools_include('ajax');
  ctools_include('modal');
  ctools_modal_add_js();
 
  $output = ctools_modal_text_button(t('Submit an event'), 'create/nojs/public-event', 'public-event');
 
  return $output;
}
 
 
/**
 * Implementation of hook_block_view_alter
 * Just adding ctools text button to a block on the calendar page
 */
function mymodule_block_view_alter(&$data, $block) {
  if (($block->delta == 'submit_event') && ($block->context == 'event_calendar')) {
      $string = $data['content'];
      $data['content'] = array();
      $data['content'] = array(
         '#type' => 'markup',
         '#markup' => $string.'<div class="modal">'.mymodule_link().'</div>',
         );
   }
}
 
/**
 * Callback function for loading events node/add form into a ctools modal window
 */
function modal_content($js = FALSE) {
 
  global $user;
 
  ctools_include('node.pages', 'node', '');
  ctools_include('modal');
  ctools_include('ajax');
 
  $type = 'public_event';
  $node = (object) array(
    'uid' => $user->uid,
    'name' => (isset($user->name) ? $user->name : ''),
    'type' => $type,
    'language' => LANGUAGE_NONE);
 
  if (!$js) {
    return drupal_get_form($type . '_node_form', $node);
  }
 
  $form_state = array(
    'title' => t('Submit an event'),
    'ajax' => TRUE,
  );
 
  $form_state['build_info']['args'] = array($node);
  form_load_include($form_state, 'inc', 'node', 'node.pages');
  $output = ctools_modal_form_wrapper($type . '_node_form', $form_state);
 
  if (!empty($form_state['executed'])) {
    $output = array();
    $output[] = ctools_modal_command_display(
      t('Successful.'),
      '<div class="modal-message">
          <h3>Thanks for submitting your event to us.</h3>
          <p>Your submission will be approved by an editor before it will appear on the site.</p></div>');
  }
  print ajax_render($output);
  exit;  
}
 
 
 /**
 * Implementation of hook_form_alter
 * Alter public_event creation form
 */
function mymodule_form_alter(&$form, &$form_state, $form_id)  {
 
  if ($form_id == 'public_event_node_form')  {
 
        $form['author']['#access'] = FALSE;
        $form['comment_settings']['#access'] = FALSE;
        $form['locations']['#weight'] = 3;
        $form['locations']['#title'] = t('Location or venue');
        $form['actions']['submit']['#value'] = t('Submit your event for review');
 
        $message = t('<p><strong>Create a public event. All submissions will be reviewed
        and approved by an editor before
        appearing on the site. </strong></p>When entering events:</ br>
        <ul>
            <li>Enter the event at least 2 weeks prior to event date.</li>
            <li>Fill out all information required as completely as possible.</li> </ul>');
        drupal_set_message($message, $type = 'status', $repeat = FALSE);
    }
}
∗ Permalink

Calendar

The above image is a partial screenshot for a calendar in Drupal 7 I've been working on. Working with the calendar and better exposed filters modules - I added a taxonomy vocabulary for the colored categories.

Full page layout

The first thing I wanted to do was remove sidebars on the calendar page and prevent any from being added because they would mess with my layout. This is a 'fusion theme so I needed to remove helper classes which determine the layout.

/**
 * Implementation of preprocess_page in template.php
 */
function awesometheme_preprocess_page(&$variables) {
 
    //Unset sidebar on calendar page, they get in the way of the calendar
   if (arg(0) == 'calendar') {
       unset($variables['page']['sidebar_second']);
   }
}
 
/**
 * Implementation of preprocess_html
 */
function awesometheme_preprocess_html(&$variables) {
 
  //We unset sidebar in preprocess_page(), now we remove helper classes
  //and add one new one ('no-sidebars')
   if (arg(0) == 'calendar') {
      foreach ( $variables['classes_array'] as $key => $item ) {
          if ( ($item == 'one-sidebar sidebar-second')) {
              unset($variables['classes_array'][$key]);
            }
        }
      $variables['classes_array'][] = 'no-sidebars';
    }
}

Popup tooltip

For the tooltip I used the beautytips module. In the readme.txt is some nice api stuff. I knew I wanted to load the full node (the page linked to in the calendar item) so I could create the content for the tooltip. I knew I'd use node_load() which requires the node_id as a parameter. I added the node_id as a field in the calendar to do this. Now I needed to write some logic that made all this happen so I turned on the theme developer module which told me all the calendar date items load inside the template "calendar-item.tpl.php". I copy / pasted it into my theme then opened up template.php to create my variables.

/**
 * Implementation of preprocess_calendar_item
 */
function awesometheme_preprocess_calendar_item(&$variables) {
 
   //Create tooltip variables for calendar-item.tpl.php
   if (arg(0) == 'calendar') {
      $nid = $variables['rendered_fields'][1];
      $fullnode = node_load($nid, NULL, TRUE);
 
      $variables['term'] = $fullnode->field_eventgenre[$fullnode->language][0]['tid'];
      $variables['body'] = $fullnode->body[$fullnode->language][0]['value'];
      $variables['title'] = check_plain($fullnode->title);
 
  if ( $fullnode->type == 'event' ) {
      $unixtimestamp = strtotime($fullnode->field_event_date[$fullnode->language][0]['value']);
      $drupaldate = format_date($unixtimestamp,'custom','M, j D - g:i a');
      $variables['public_date'] = $drupaldate;
      $variables['location'] = check_plain($fullnode->locations[0]['name']);
      $variables['address'] = check_plain($fullnode->locations[0]['street']);
      $variables['address'] .= ' ' .check_plain($fullnode->locations[0]['additional']);
      $variables['city'] = check_plain($fullnode->locations[0]['city']);
      $variables['state'] = check_plain($fullnode->locations[0]['province']);
   }
 
      //Remove node_id view field since we're done with it
      unset($variables['rendered_fields'][1]);
      //kpr($fullnode);
   }
}

Now that my variables were created I could build the tooltip in calendar-item.tpl.php where I also included the beautytips api so it would load into a nice html5 canvas popup.

  <!-- BeautyTips module API garnered from readme.txt -->
  <?php
    if (arg(0) == 'calendar') {
       $options['calendar_tooltip'] = array(
         'cssSelect' => '.view-item .calendar a',
         'contentSelector' => '$(this).next()',
         'trigger' => array('mouseover', 'click'),
         'width' => 350,
       );
      beautytips_add_beautytips($options);
   }
  ?>
 
   <!-- Tooltip popup for calendar, variables created in preprocess_calendar_item in template.php -->
   <?php if (arg(0) == 'calendar') { ?>
      <div class="tooltip" style="display:none;">
         //Should include 'isset' statements, removed for readability
         <h1 class="title"> <?php print $title ?> </h1>
         <h2 class="date-time"> <label>Date/Time:</label> <?php print $public_date; ?></h2>
         <h2 class="location"> <label> Location: </label> <?php print $location; ?>; <?php print $address; ?>, <?php print $city; ?>, <?php print $state; ?></h2>
         <span class="body"> <?php print $body; ?></span>
      </div><!-- /tooltip -->
    <?php } ?>

Anonymous create event form

The client also wanted anyone visiting the site to be able to submit calendar events. I created a new content type called "public event" because they had certain requirements for the fields and such. I needed to open a node/add form for this content type for anonymous users inside the same theme. I decided to create a module and load the form inside a function callback using hook_menu(). Which I cleaned up with hook_form_alter().

<?php
 
/**
 * @file
 * Create Event module file
 *
 * This module allows anonymous users to create calendar events.
 */
 
 
/**
 *  Implementation of hook_menu()
 */
function public_calendar_events_menu() {
    $items = array();
    $items['create-calendar-event'] = array(
      'type' => MENU_CALLBACK,
      'page callback' => 'public_calendar_events_page',
      'access arguments' => array('access content'),
    );
 
  return $items;
}
 
/**
 * Page callback to display the calendar public_event node creation form
 */
function public_calendar_events_page() {
 
    global $user;
    $type = 'public_event';
    $node = (object) array(
      'uid' => $user->uid,
      'name' => (isset($user->name) ? $user->name : ''),
      'type' => $type,
      'language' => LANGUAGE_NONE,
    );
 
    $form_state['build_info']['args'] = array($node);
    form_load_include($form_state, 'inc', 'node', 'node.pages');
    $form = drupal_build_form($type . '_node_form', $form_state);
 
    return $form;
}
 
 
/**
 * Alter public_event creation form
 */
 
function public_calendar_events_form_alter(&$form, $form_state, $form_id)  {
 
  if ( ($form_id == 'public_event_node_form') && (arg(0) == 'create-calendar-event') )  {
 
        unset($form['author']);
        unset($form['comment_settings']);
        unset($form['menu']);
        unset($form['path']);
        $form['locations']['#collapsed'] = 0;
        $form['locations']['#weight'] = 3;
        $form['locations']['#title'] = t('Location or venue');
        $form['actions']['preview']['#value'] = t('Preview form before submitting');
        $form['actions']['submit']['#value'] = t('Submit your event for review');
    }
}
∗ Permalink

GeSHi filter

Geshi filter allows for adding code with syntax highlighting in blog posts. It's a bit of a pain to set up in Drupal 7 properly with WYSIWYG buttons so I'm documenting the process here.

How to install

Download and turn on these Drupal 7 modules;

Download 1.0.8x version of Geshi and put it in sites/all/libraries;

Setup the wysiwyg. Go to "configuration -> wysiwyg profiles" to set up text formats. You may need to install an editor. I am using the ckeditor for Drupal 7 that should be installed in sites/all/libraries. You would then select the editor you're using, edit each text format and check off the buttons you want.

Configure GeSHi

Configure GeSHi at admin/config/content/formats/geshifilter (you will get WSOD if you installed the libraries API module prior to the 7.x-2.x branch). Under "Generic syntax highlighting tags" add "pre" after blockcode. Select the languages tab and check off the code languages you want to include along with the tags (<jquery> etc..).

Go to "configuration -> text formats". Select "Add text format". Check off "GeSHi filter" and uncheck everything else -- or else be careful for conflicts and make sure GeSHi Filter is listed before other filters in the "filter processing order".

Go to "configuration -> wysiwyg profiles" and select an editor for GeSHi Filter. Edit the filter and under "buttons and plugins" check off the Geshi buttons you want to use (GeSHi: PHP, GeSHi: CSS etc..).

Include an image uploader

These two modules go great with wysiwyg for uploading images;

Setup the profiles #overlay=admin/config/media/imce and configure permissions. Turn on "image" and "IMCE" in the buttons section under configuration -> wysiwyg profiles -> edit a filter.

I format images under the advanced tab in the Image Properties box. Include some CSS classes in your theme.

img.left {
  margin: 20px 20px 12px -250px;
  float: left;
}
 
img.right {
  margin: 20px -250px 20px 20px;
  float: right;
}
 
img.center {
  margin: 20px 0 20px -3em;
}
 
img.border {
  border: 1px solid rgba(255,255,255,0.25);
}
∗ Permalink
Subscribe to Drupal 7