Node CSS: My first Drupal 7 Module

Please note, code in this article may be out of date. Please download the module at the bottom of the page (or view the project page on drupal.org... when it exists!)

It was about time I created a module to get up on Drupal.org. Now that Drupal 7 is out I thought my first contributed module should be a Drupal 7 module.

The module is mostly a port and formalisation of bespoke work I did on www.nowmusic.com to allow customisation of background images via the CMS:
http://www.nowmusic.com/bundles/now-thats-what-i-call-80s
http://www.nowmusic.com/bundles/now-thats-what-i-call-no-1s

The final module will allow something like a CSS template per node type that can use information from fields in individual nodes to populate place-holders the CSS.

Immediately I found working with the field api difficult mainly because of lack of documentation. Trying to find out basic stuff like a list of widget types available produced pretty much nothing. All I wanted to do was find out how to add a single checkbox field!

Finally I stumbled across these pages which gave me the clue to options_onoff and the correct format of the widget array:
http://api.drupal.org/api/drupal/modules--field--modules--list--list.mod...
http://api.drupal.org/api/drupal/profiles--standard--standard.install/fu...

There is also the field api itself:
http://api.drupal.org/api/drupal/modules--field--field.module/group/field/7

Pro Drupal 7 Develoment shows:

'widget_type' => 'text_textarea_with_summary'

Which does work for a text field but may be incorrect since text is the default widget type.

Finally I got the field instance to appear as a checkbox. As with Drupal 6, checkbox labels are a bit sketchy and didn't appear so I added a description:

<?php
$instance
= array(
     
'field_name' => 'nodecss',
     
'entity_type' => 'node',
     
'bundle' => $key,
     
'label' => 'Generate CSS for this node',
     
'description' => 'Generate CSS for this node',
     
'widget' => array(
       
'type' => 'options_onoff',
      ),
     
'options' => $options,
    );
?>

Ok, so now we can select content types to generate CSS for and have a checkbox (that does nothing yet) to turn it on and off for individual nodes.

Handling the CSS template

There are two ways I see of handling the CSS template:

  1. Using a template file with name convention e.g. node-page.css.tpl.php
  2. Administering the CSS tempate via the content type edit page

To begin with I'll use a single template file for all content types and then look at node type specific templates. (This is essentially what I did for www.nowmusic.com since we only needed custom backgrounds for a single content type.)

The Drupal theme system is great. It can use a template file to which you can pass variables with a line like:

theme('nodecss', $css_vars);

To register the theme function we need to use hook_theme():

<?php
function nodecss_theme() {
  return array(
   
'nodecss' => array(
     
'template' => 'template/css',
    ),
  );
}
?>

What this does is registers 'nodecss' in the theme system and to use a template file 'css' in the 'template' folder. Drupal understands this as 'template/css.tpl.php'. We can pass an array into this theme function to pass a load of variables for the css.
We could have registered the theme function (template) using a name other than 'nodecss' but I thought that's appropriate.

So now in my module I have a template folder and a css.tpl.php file. In the CSS file all I'm going to put for now is:

body { background-color:<?php print $css_vars['field_body_bg_color']; ?>; }

Now I can put the following line and get back some populated CSS ready for saving to a CSS file:

<?php
theme
('nodecss', array('css_vars' => array('field_body_bg_color'=>'#FF0000')));
?>

Saving the CSS

Drupal has CSS aggregation built in so I think it's fair to create a CSS file for each node for the time being. This is easier to handle than trying to create a single CSS file for all nodes, although having a quick think, it would probably be quite easy to write the CSS for each node separately to the database and re-write the CSS file when a node is saved. This is the better way because the same CSS can always be served although you need to make sure the CSS is node specific, perhaps a CSS preprocessor could come in handy here to just wrap the CSS template in a selector identifying the node. Either way, the CSS file needs to be saved.

The appropriate time to write the CSS file is when a node is saved and updated. It appears hook_nodeapi() has disappeared from Drupal 7 and has been replaced with explicit hooks e.g. hook_node_update($node) and hook_node_presave($node).

In order to filter down to only the fields we need for the CSS I've decided on a field naming scheme where the field should be named field_css_. I.e. In the field creation form the user should enter css_ since the field (CCK) module prefixes fields with field_. Perhaps this should be a configurable naming scheme defaulting to field_css_. The array_filter() function is then used to get the array keys that conform to this naming scheme. All that's then left is to create an array of the conforming fields and pass them through to the CSS template. Beyond that it will probably be up to the user to get the value they need from they array with some help from dsm() or print_r() - something to note in the module documentation. I expect it will be difficult to provide a configuration form for this since there could be all kinds of field types with all kinds of data structures.

Bulding the $css_vars array:

<?php
/**
* Gets CSS fields from the node for fields named field_css_<name>
*/
function _nodecss_get_node_vars($node) {
 
// Get node array keys
 
$keys = array_keys((array)$node);
 
 
// Filter keys for configured name scheme, default: 'field_css_'
 
$css_fields = array_filter($keys, "_nodecss_check_field");

 
// Build $css_vars array
 
$css_vars = array();
  foreach (
$css_fields as $key) {
   
$css_vars[$key] = $node->$key;
  }
  return
$css_vars;
}


/**
* Checks a field matches format  <scheme><name> e.g. field_css_<name>
*/
function _nodecss_check_field($field) {
 
$nodecss_scheme = variable_get('nodecss_scheme', 'field_css_');

  if (
strpos($field, $nodecss_scheme) === 0) {
    return (
$field);
  } else {
    return;
  }
}
?>

Writing a file has changed quite a lot in Drupal 7 so something like the following was used to save the CSS file:

<?php
    $directory
= 'public://' . variable_get('nodecss_folder', 'nodecss');
   
    if (
file_prepare_directory($directory, FILE_CREATE_DIRECTORY)) {
     
//$dest = file_create_filename('node-' . $node->nid . '.css', $directory);
     
$dest = $directory . '/node-' . $node->nid . '.css';
   
file_save_data($css, $dest, FILE_EXISTS_REPLACE);
   
drupal_set_message("CSS saved to $dest");
   
// @todo - Allow configuration of messages
   
} else {
     
drupal_set_message("$directory does not exist or is not writeable please check the folder permissions", 'error');
    }
?>

Including the CSS

Including the CSS is the easy part. All we need to do is check the node is eligible and then call drupal_add_css() with the filename

<?php
function nodecss_node_view($node) {
if (
nodecss_node_enabled($node)) {
   
$directory = 'public://' . variable_get('nodecss_folder', 'nodecss');
   
$dest = $directory . '/node-' . $node->nid . '.css';
   
drupal_add_css($dest);
}
}
?>

That's it! All working, well kind of. I needed to add the node id to the $css_vars array and change the CSS template to be have a more specific selector. Basing the selector on the node id sets us up for using a single CSS file that is always loaded:

body.page-node-<?php print $css_vars['nid']; ?> #main-wrapper { background:<?php print $css_vars['field_css_body_bg_color']['und'][0]['value']; ?>; }

Adding configuration, help and documentation

So now it works but only in a single way. It's time to ice the cake with some more configuration options and some 'help' describing how to use the module.

Before writing the configuration I did a bit of restructuring of the module. Mainly moving a bit of functionality to discreet functions and adapting it to make use of settings variables. It's great when writing a module to use variable_get('variable_name', 'default_value') well before you think about writing the configuration form even if you're not going to set the variable for a while, it will just use the 'default_value'.

Creating the settings form is pretty easy, just make sure the element name is the same as the variable name in #default_value:

<?php
function nodecss_admin_settings() {
 
// Get an array of node types

 
$types = node_type_get_types();
  foreach (
$types as $node_type) {
   
$options[$node_type->type] = $node_type->name;
  }

 
$form['nodecss_types'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Node types'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );
 
$form['nodecss_types']['nodecss_node_types'] = array(
   
'#type' => 'checkboxes',
   
'#title' => t('Generate custom css for these node types'),
   
'#options' => $options,
   
'#default_value' => variable_get('nodecss_node_types', array()),
   
'#description' => t('A CSS file will be generated for these node types.'),
  );

 
$form['nodecss_settings'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('General settings'),
   
'#collapsible' => TRUE,
   
'#collapsed' => TRUE,
  );
 
$form['nodecss_settings']['nodecss_scheme'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Node CSS field naming scheme'),
   
'#field_prefix' => t('field_'),
   
'#default_value' => variable_get('nodecss_scheme', 'css_'),
   
'#description' => t("Fields using the Node CSS naming scheme will automatically be passed to the CSS template<br />
      and optionally (by default) removed from the node display<br />
      Drupal adds 'field_' so 'css_body_bg_color' will become 'field_css_bg_body_color' in the CSS template"
),
  );
 
$form['nodecss_settings']['nodecss_directory'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Node CSS directory'),
   
'#field_prefix' => t('public://'),
   
'#default_value' => variable_get('nodecss_directory', 'nodecss'),
   
'#description' => t("The directory to save CSS files to."),
  );
 
$form['nodecss_settings']['nodecss_hide_field'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Override and remove field from node display'),
   
'#default_value' => variable_get('nodecss_hide_field', 1),
   
'#description' => t("When enabled, all fields adhering to the naming scheme above will automatically be hidden from the node display."),
  );
 
$form['nodecss_settings']['nodecss_per_node'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Generate per node'),
   
'#default_value' => variable_get('nodecss_per_node', 0),
   
'#description' => t("When enabled, a CSS file will be added per node instead of putting all CSS in a single file.<br />
      Useful if you don't have a CSS class identifying the node, typically on the body.<br />
      <strong>WARNING: 'Always add CSS' should probably be disabled if this is enabled.</strong>"
),
  );
 
$form['nodecss_settings']['nodecss_always'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Always add CSS'),
   
'#default_value' => variable_get('nodecss_always', 1),
   
'#description' => t("When enabled, CSS generated by nodecss will always be included when a node is viewed.<br />
      Recommended because it allows the same aggregated CSS to be served across most of the site<br />
      but you must have a CSS class identifying the node."
),
  );
 
$form['nodecss_settings']['nodecss_devel'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Development mode'),
   
'#default_value' => variable_get('nodecss_devel', 0),
   
'#description' => t('When enabled, the $css_vars array will be output when the node is viewed.<br />
      If the devel module is installed the array will be nicely formatted with Krumo.'
),
  );
 
$form['nodecss_settings']['nodecss_display_messages'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Display messages'),
   
'#default_value' => variable_get('nodecss_display_messages', 0),
   
'#description' => t('Display messages when a CSS file is created or updated.'),
  );
 
$form['#submit'][] = 'nodecss_admin_settings_submit';


  return
system_settings_form($form);
}
?>

For help I just used the book module as an example to write hook_help():

<?php
function nodecss_help($path, $arg) {
  switch (
$path) {
    case
'admin/help#nodecss':
     
$output = '<h3>' . t('About') . '</h3>';
     
$output .= '<p>' . t('The Node CSS module uses values from node field to populate a CSS template. Templates can be written per content type and are then automatically included.') . '</p>';
     
$output .= '<h3>' . t('Uses') . '</h3>';
     
$output .= '<dl>';
     
$output .= '<dt>' . t('Templates per content type') . '</dt>';
     
$output .= '<dd>' . t('Use the following naming scheme: css-<type>.tpl.php, e.g. css-page.tpl.php') . '</dd>';
     
$output .= '<dt>' . t('Inserting CSS variables') . '</dd>';
     
$output .= '<dd>' . t('CSS variables are automatically passed to the template based on the field naming scheme defined in @nodecss-admin', array('@nodecss-admin' => url('admin/config/nodecss/settings'))) . '</dd>';
     
$output .= '</dl>';
      return
$output;
    case
'admin/config/nodecss/settings':
      return
'<p>' . t('The Node CSS module uses values from node fields to populate a CSS template.') . '</p>';
  }
}
?>

What I didn't realise about hook_help() is it adds a list of configuration to the bottom of the help page if you add text for a path.

Permissions

Something I have left up until now is permissions. I figured anyone that the only permissions needed should be handled by the file system in respect of editing the template file. However on further reflection, perhaps you would want to prevent users from generating a new CSS file.... and on even further reflection, there definitely needs to be a permission for configuring the module!

AttachmentSize
nodecss.tar.gz5.24 KB

Post new comment

By submitting this form, you accept the Mollom privacy policy.

User login

Author of...

  • My blog about 'Time is NOT money'... and value http://t.co/g2C6pZy0 12 years 7 weeks ago
  • I've found an excellent query logging and filtering module for Drupal. Not available via http://t.co/U1Xe92Th: http://t.co/2qvg1DgE 12 years 11 weeks ago
  • I've just spotted my first © 2011 at the bottom of a website. 12 years 11 weeks ago
  • @da_lune That's the spirit, oh, too late @gavinbrook, Wordpress is fantastic & now more than a blogging tool but Drupal = more functionality 12 years 15 weeks ago
  • @oliverpolden is having an amazing day for many reasons. I feel like giving back: http://t.co/JvKVVMBF Have a request? use the contact form. 12 years 15 weeks ago
  • The world's population is 7 billion. If a few 'competing' companies mutually helped each other get more customers business would be easy. 12 years 15 weeks ago
  • @Casablanca Amazing photos! Thank you for sharing. 12 years 16 weeks ago
  • Ahh, the Christmas Snow module for Drupal: http://t.co/QzvkQiT2 12 years 16 weeks ago
Oliver Polden