Creating A Custom Panels Style Plugin With Background Image Support

In this post I want to give an example of how to use the power of ctools plugins to add additional styles options to your panels. This way you can have complete control over the wrappers of your panes and panels by retaining flexibility and allowing settings to be set in the Panels UI.

The idea is to show how to create a panels style plugin which would use an image field, attached to your content type or any entity, and show this image as background element of this entity, when displayed via panels:

style_form.jpg

You need to start by creating a basic custom module in your Drupal installation. The one I use here is called "my_module". Then in your .module file implement a hook to register your ctools plugins to the system:

/**
 * Implementation of hook_ctools_plugin_directory().
 */
function my_module_ctools_plugin_directory($module, $plugin) {
  if ($module == 'panels' && !empty($plugin)) {
    return 'plugins/' . $plugin;
  }
}

In you module you have to create a folder structure as the one returned by the hook above 'plugins/styles' (because we are creating a style plugin the value of $plugin should be 'styles'). Then inside the styles folder start creating your plugin by adding a file which will contain the code for the plugin. I've named the file "background_picture.inc", but you can have whatever name suits your type of style plugin best. Start by describing your plugin by implementing a $plugin-array as follows:

<?php
 
// Plugin definition
$plugin = array(
  // Title and description of the plugin
  'title' => t('Background picture'),
  'description' => t('Background picture style.'),
  // Define a theme function for this plugin
  'render region' => 'background_picture_style_render_region',
  // We'll be using a template for rendering
  'hook theme' => array(
    'background_picture_style' => array(
      'variables' => array(
        'content' => NULL,
        'style_attributes' => array(),
      ),
      'path' => drupal_get_path('module', 'my_module') . '/plugins/styles/background_picture',
      'template' => 'background-picture',
    ),
  ),
  // This defines the settings form for the plugin
  'settings form' => 'background_picture_style_settings_form',
);

The plugin-description array starts with title and description, which should be self-explanatory. The "render_region" key defines a callback which should contain the display logic. You can either return the final output here, but we'll be using this callback as a theme function. This way we can provide both a theme function and a template, which can make the life of a drupal themer easier. In order to do that we can add an element to our plugins-array with the key of "hook theme". This element is an associative array itself and behaves the way you would normally implement a hook_theme hook. So to make it a little bit more clear: our "render region"-callback should return variables which can be used in the template file, defined in the "hook theme"-array. The template file is called background-picture.tpl.php and should be found in the location defined in the element with the key of "path" inside of "hook theme". The last element not described yet in the plugins-array is the callback defined by "settings form" - this should present the user a form, where he can set some settings which make sense for this style plugin.

With the plugin-definition out of the way let's create our form callback as defined in "settings form":

/**
 * Settings form callback.
 */
function background_picture_style_settings_form($form, $form_state) {
  $form['image'] = array(
    '#type' => 'textfield',
    '#title' => t('Image field'),
    '#description' => t('Enter the image field for the background image. You may use substitutions in this field. E.g. for the default image field attached to articles use "%node:field_image"'),
    '#required' => TRUE,
    '#default_value' => (isset($form['image'])) ? $form['image'] : '',
  );
  $form['image_style'] = array(
    '#type' => 'select',
    '#title' => t('Image Style'),
    '#description' => t('Choose the appropriate image style for the background image.'),
    '#options' => image_style_options(),
    '#default_value' => (isset($form['image_style'])) ? $form['image_style'] : '',
  );
  $form['class'] = array(
    '#type' => 'textfield',
    '#title' => t('CSS Classes'),
    '#description' => t('Enter CSS classes for this style. Separate multiple classes by spaces.'),
    '#default_value' => (isset($form['class'])) ? $form['class'] : '',
  );
 
  return $form;
}

This form just demonstrates the implementation of the callback. The elements in the form-array could be anything you need for your style-plugin. In this particular case we give the user three fields:

  • image is a textfield where the user can enter a substituition code (provided by ctools' context) for a field, attached to the entity being panelized. This way the image field can be also taken from e.g. another entity, loaded as context into the panelized entity.
  • image_style would allow us to define an image style for the used image.
  • class - it's probably not a bad idea to give the user the possibility to add some CSS-classes to the wrapper.

So after choosing our "Background picture"-plugin the user should be presented with the following form:

settings_form.jpg

This should wrap up the form callback. Now let's use the values from the form in our theme function:

/**
 * Render callback.
 */
function theme_background_picture_style_render_region($vars) {
  $image_url = NULL;
 
  // Get the absolute path of the original image from the context substitution
  $image_abs_path = ctools_context_keyword_substitute($vars['settings']['image'], array(), $vars['display']->context);
 
  $image_style = $vars['settings']['image_style'];
 
  if ($image_style == '') {
    // If no image style is selected, use the original image.
    $image_url = $image_abs_path;
  } else {
    // Image style is provided in the settings form.
    // We need to get the original image uri to return the URL for an image derivative.
    global $base_url;
    $files_rel_path = variable_get('file_public_path', conf_path() . '/files');
    $image_rel_path = str_replace($base_url . '/' . $files_rel_path, '', $image_abs_path);
    $image_uri = file_build_uri($image_rel_path);
 
    $image_style_url = image_style_url($image_style, $image_uri);
 
    $image_url = $image_style_url;
  }
 
  $style_attributes = array();
  $style_attributes['style'] = 'background-image: url(' . $image_url . ');';
 
  // Add our classes to the attrubutes array, if any defined
  if ($vars['settings']['class']) {
    $style_attributes['class'] = explode(' ', $vars['settings']['class']);
  }
 
  $content = implode($vars['panes']);
 
  return theme('background_picture_style', array(
      'content' => $content,
      'style_attributes' => $style_attributes,
    )
  );
}

This function contains more custom logic than rules that would apply to a style-plugin. The important thing here is that we return a theme function with the first parameter corresponding to the key which we defined back in "hook theme" in our plugin definition array. The second parameter to the theme function is an array with the variables which will be passed to our template file. The $content-variable implodes the panes placed in the panel we apply the style to. The $style_attributes variable is a bit more complicated. It's an array ready to be passed through Drupals' drupal_attributes-function (see the template file code below) which will contain some attributes for our wrapper div. We need that in order to be able to define the style attribute, which will point to the image file through an "background"-inline style declaration. See the source code example of the finished page below on how that gets implemented at the end.

So far so good. One last thing is still missing and that is our template file. So go ahead and create a file named "background-picture.tpl.php" and place it inside of what you defined in "hook theme" inside the plugin definition-array. In our case it's in "plugins/styles/background_picture" - the same place where we have our background_picture.inc file. Add following code into the tempate:

<div<?php print drupal_attributes($style_attributes); ?>>
  <?php print render($content); ?>
</div>

Nothing too fancy here, but hey it's the way a template file should be. This is the result of article-node generated by devel_generate, you can see the markup:

end_result.jpg

Again, it's just a demo, but there are some cases, where you have the background-image on top of your nodes. Now you can define what field to use, what image style, a themer can come and override some of the logic in his theme, e.g. to prevent the image from scrolling or whatever needed. And last but not least the editor has the freedom to choose the right image for each article. And last but not least, it's reusable, exportable, context-aware etc.

If you have any remarks, proposals or question please leave a comment below. I'd happy for any input. Thanks!

Comments

ridgek (not verified)

Thanks for your work on this plugin. Unfortunately I can't get it to work. It looks like my context array is empty (http://s30.postimg.org/631lups29/screen.jpg) which is preventing the ctools_context_keyword_substitute function from working. I'd appreciate any help you could offer!

chinhvn (not verified)

can you upload your module? I do follow this but it do'nt action.

ridgek (not verified)

I just realized; I have been trying to use this on a separate (front) page I've created, where the node with image attached is placed in a panel, rather than using the plugin on the node page. I guess that explains why there's no context? My understanding of Panels context is obviously not great.

So, I've just tried using the plugin on a regular node page with image attached; the context object now has data, but it seems the ctools_context_keyword_substitute function still returns an empty value. This is on a clean install of Drupal 7.34 with only ctools/panels installed and using the default image field.

Could you possibly include more detailed steps on how to get this working?

Thanks again!

ridgek (not verified)

Sure, here u go: http://ge.tt/1ET2qo52/v/0

Thanks!

Ben (not verified)

I am also unable to get this working correctly. I assume for this to work one needs to make sure the panel page includes a node context. I have added a node context to the page using an nid of a pre-existing article with an image. This ensures that $vars['display']->context in theme_background_picture_style_render_region() is not empty.

However, despite this $image_abs_path is still empty.

Lars (not verified)

BTW: This could be done also with Clean Markup (https://www.drupal.org/project/clean_markup).

TravisC (not verified)

Can you commit this module to github?

joe (not verified)

I like to learn a bit more about panels. The module code is down so far. Could you remind up the code, please.

Thanlet you so much in advance!

Q (not verified)

Can I download an example of this module somewhere?

Q (not verified)

can you upload your module? the link above is dead.

Add new comment