Oct 20 2016
Oct 20

It has been a few years since I have had the opportunity to build a website from the absolute beginning. The most recent project I’m on continues in that vein, but it’s early enough for me to consider ripping it apart and starting all over again. The project is particularly interesting to me, as it’s my first opportunity to use Drupal 8 in earnest. As I’ve got an interest in automating as much as possible, I want to gain a better understanding of the configuration management features which have been introduced in Drupal 8.

Tearing it apart and starting again wasn’t the first thing considered. Being an arrogant Drupal dev, I figured I could simply poke around the GUI and rely on some things I’d seen at Drupalcon and Drupal camps in the past couple of years to see me through. I thought I would find it easy to build a replicated environment so that any new developer could come along, do a git clone, vagrant up, review a README.md file and/or wiki page and they’d be off and running.

Wrong.

This post outlines many of the things that I examined in the process of learning Drupal 8 while adopting a bit of humility. I’ve created a sample project with names changed to protect the innocent. Any comments are welcome.

The structure of the rest of this post is as follows:

Setting up and orientation with Drupal VM

I am a big fan of Jeff Geerling’s Drupal VM vagrant project, so I created a fork of it, and imaginatively called it D8config VM. We will be building a Drupal site with the standard profile which we’ll use to rapidly build a basic prototype using the Drupal GUI - no coding chops necessary. The only contributed module added and enabled is the Devel module at the start, but we will change that quickly.

Here are the prerequisites if you do follow along:

  • familiarity with the command line;
  • familiarity with Vagrant and that it’s installed on your machine (note: the tutorial requires Vagrant 1.8.1+);
  • as well as Vagrant 1.8.1+, you need to have Ansible 2.0.1+ and VirtualBox 5.0.20+ installed;
  • have installed the Vagrant Auto-network plugin with vagrant plugin install vagrant-auto_network. This will help prevent collisions with other virtual networks that may exist on your computer;
  • have installed the Vagrant::Hostsupdater plugin with vagrant plugin install vagrant-hostsupdater, which will manage the host’s /etc/hosts file by adding and removing hostname entries for you;
  • familiarity with git and GitHub;
  • if using Windows, you are comfortable with troubleshooting any issues you might come across, as it’s only been tested on a Mac;
  • familiarity with Drush.

Here is how the D8config VM differs from Drupal VM:

  • the config.yml and drupal.make.yml files have been committed, unlike the normal Drupal VM repo;
  • the hostname and machine name have been changed to d8config.dev and d8config respectively;
  • to take advantage of the auto-network plugin, vagrant_ip is set to 0.0.0.0. The d8config machine will then have an IP address from 10.20.1.2 to 10.20.1.254;
  • the first synced folder is configured with a relative reference to the Vagrant file itself:
# The first synced folder will be used for the default Drupal installation, if
# build_makefile: is 'true'.
- local_path: ../d8config-site         # Changed from ~/Sites/drupalvm
  destination: /var/www/d8config-site  # Changed from /var/www/drupalvm
  type: nfs
  create: true

I’ll do the same with subsequent shared folders as we progress - it’s a useful way to keep the different repos together in one directory. At the end of the tutorial, you’ll have something like:

└── projects
    ├── another_project
    ├── d8config
    │   ├── d8config_profile
    │   ├── d8config-site
    │   └── d8config-vm
    ├── my_project
    └── top_secret_project

top

New nice-to-haves (thanks to recent changes in Drupal VM)

If you have used Drupal VM before but haven’t upgraded in a while, there are a load of new features. Here are just two to note:

  • Ansible roles are now installed locally (in ./provisioning/roles) during the first vagrant up;
  • PHP 7 is now an option to be installed. In fact, it’s installed by default. You can select 5.6 if you like by changing php_version in the config.yml file (see PHP 5.6 on Drupal VM).

Now do this

Create a directory where you’re going to keep all of the project assets.

$ mkdir d8config

# Change to that directory in your terminal:
$ cd d8config

Clone the D8config VM repo. Or, feel free to fork D8config VM and clone your version of the repo.

$ git clone https://github.com/siliconmeadow/d8config-vm.git

# Change to the `d8config-vm` directory:
$ cd d8config-vm

# Checkout the `CG01` branch of the repo:
$ git checkout CG01

# Bring the vagrant machine up and wait for the provisioning to complete.
$ vagrant up

After successful provisioning you should be able to point your browser at http://d8config.dev and see your barebones Drupal 8 site. Username: admin; Password: admin.

Caveats

If you’ve used Drupal VM before, you will want to examine the changes in the latest version. From Drupal VM tag 3.0.0 onwards, the requirements have changed:

  • Vagrant 1.8.1+
  • Ansible 2.0.1+
  • VirtualBox 5.0.20+

One sure sign that you’ll need to upgrade is if you see this message when doing a vagrant up:

ansible provisioner: * The following settings shouldn't exist: galaxy_role_file

top

Build your prototype

In this section of the tutorial we’re going to start building our prototype. The brief is:

The site is a portfolio site for for a large multinational corporation’s internal use. But hopefully the content architecture is simple enough to keep in your head. The following node types need to be set up: Case study, Client, Team member (the subject matter expert), and Technologies used. Set up a vocabulary called Country for countries served and a second to called Sector classify the information which will contain tags such as Government, Music industry, Manufacturing, Professional services, etc. Delete the existing default content types. You can then delete fields you know you won’t need to avoid confusion - Comments for example - which will then allow you to uninstall the comment module. And, as you might deduce by the modules I’ve selected, it’s to be a multilingual site.

Hopefully this should feel comfortable enough for you, if you are familiar with Drupal site building. There are enough specifics for clarity, yet it’s not too prescriptive that you feel someone is telling you how to do your job. Whereas in one context, you may hear a manager say “don’t bring me problems, bring me solutions”, most engineers would rather say for themselves “don’t bring me solutions, bring me problems”. I hope this brief does the latter.

Have a go at making the changes to your vanilla Drupal 8 site based on the brief.

Beyond the brief

Every ‘site building’ exercise with Drupal is a move further away from the configuration provided by the standard or minimal profiles. In our circumstance, we will enable these modules via the GUI:

  • Responsive Image
  • Syslog
  • Testing
  • BigPipe
  • Devel Generate (Devel was installed due to settings in config.yml in the d8config-vm repo)
  • Devel Kint
  • Devel Node Access
  • Web Profiler
  • Configuration Translation
  • Content Translation
  • Interface Translation
  • Language

I’ve also added a couple of contributed themes via Drush so the site will no longer look like the default site.

# While in your d8config-vm directory:
$ vagrant ssh

# Switch to your Drupal installation:
$ cd /var/www/d8config-site/drupal

# Download and install two themes:
$ drush en integrity adminimal_theme -y

For more details on these themes, see the Integrity theme and the Adminimal theme. As you might expect, I set Integrity as the default theme, and Adminimal as the admin theme via the GUI.

After switching themes, two blocks appeared in the wrong regions. I went to the Block layout page and moved the Footer menu block from the main menu region to the footer first region and the Powered by Drupal block from the Main menu to the Sub footer block.

Due to the multilingual implication, I went to the Languages admin page and added French.

top

Replicate and automate

At this stage you’ve made quite a lot of changes to a vanilla Drupal site. There are many reasons you should consider automating the building of this site - to save time when bringing other members into the development process, for creating QA, UAT, pre-prod and production environments, etc. We will now start to examine ways of doing just this.

drush make your life easier

In this section we’re going to create a Drush makefile to get the versions of Drupal core, contrib modules and themes we need to build this site as it currently is. This file will be the first file added to the D8config profile repo. Makefiles are not a required part of a profile, and could reside in a repo of their own. However to keep administration down to a minimum, I’ve found that this is a useful way to simplify some of the asset management for site building.

Let’s first tweak the config.yml in the D8config VM repo, so that we have synced folder for the profile. To do so, either:

  1. git checkout CG02 in the d8config-vm directory (where I’ve already made the changes for you), or;
  2. Add the following to the config.yml in the vagrant_synced_folders section:
# This is so the profile repo can be manipulated on the guest or host.
- local_path: ../d8config_profile
  destination: /build/d8config/d8config_profile
  type: nfs
  create: true

After doing either of the above, do a vagrant reload which will both create the directory on the Vagrant host, and mount it from the d8config-vm guest.

Next, let’s generate a basic makefile from the site as it now is.

# From within the d8config vm:
$ cd /var/www/d8config-site/drupal
$ drush generate-makefile /build/d8config/d8config_profile/d8config.make

This makefile is now available in the d8config_profile directory which is at the same level as your d8config-vm directory when viewing on your host machine.

Because we only have Drupal core, two contrib themes and the Devel module, it’s a very simple file and it doesn’t need any tweaking at this stage. I’ve committed it to the D8config profile repo and tagged it as CG01.

Raising our profile

Since we’ve established that the makefile is doing very little on this site, we need to look at completing the rest of the profile which will apply the configuration changes when building the site. The How to Write a Drupal 8 Installation Profile is quite clear and we’ll use that page to guide us.

First, our machine name has already been chosen, as I’ve called the repo d8config_profile.

Rather than writing the d8config_profile.info.yml file from scratch, let’s duplicate standard.info.yml from the standard profile in Drupal core, as that’s what we used to build the vanilla site to begin with. We can then modify it to reflect what we’ve done since.

# From within the /build/d8config/d8config_profile directory in the vagrant machine:
$ cp /var/www/d8config-site/drupal/core/profiles/standard/standard.info.yml .

$ mv standard.info.yml d8config_profile.info.yml

The first five lines of the d8config_profile.info.yml need to look like this:

name: D8config
type: profile
description: 'For a Capgemini Engineering Blog tutorial.'
core: 8.x
dependencies:

At the end of the file it looks like this, which shows the required core modules and adding the modules and themes we’ve downloaded:

- automated_cron
- responsive_image
- syslog
- simpletest
- big_pipe
- migrate
- migrate_drupal
- migrate_drupal_ui
- devel
- devel_generate
- kint
- devel_node_access
- webprofiler
- config_translation
- content_translation
- locale
- language
themes:
- bartik
- seven
- integrity
- adminimal_theme

Also, don’t forget, we uninstalled the comment module, so I’ve also removed that from the dependencies.

You still need moar!

The profile specifies the modules to be enabled, but not how they’re to be configured. Also, what about the new content types we’ve added? And the taxonomies? With previous versions, we relied on the features module, and perhaps strongarm to manage these tasks. But now, we’re finally getting to the subject of the tutorial - Drupal 8 has a configuration system out of the box.

This is available via the GUI, as well as Drush. Either method allows you to export and import the configuration settings for the whole of your site. And if you look further down the profile how-to page, you will see that we can include configuration with installation profiles.

Let’s export our configuration using Drush. This is will be far more efficient than exporting via the GUI, which downloads a *.tar.gz file, which we’d need to extract a copy or move to the config/install directory of the profile.

While logged into the vagrant machine and inside the site’s root directory:

# Create the config/install directory first:
$ mkdir -p /build/d8config/d8config_profile/config/install

# Export!
$ drush config-export --destination="/build/d8config/d8config_profile/config/install"

When I exported my configuration, there were ~215 files created. Try ls -1 | wc -l in the config/install directory to check for yourself.

top

The reason we’re gathered here today (a brief intermission)…

I hope you are finding this tutorial useful - and also sensible. When I started writing this blog post, I hadn’t realised it would cover quite so much ground. The key thing I thought I would be covering was Drupal 8’s configuration management. It was something I was very excited about, and I still am. To demonstrate some of the fun I’ve had with it is still the central point of this blog. All of the previous steps to get to this point were fun too, don’t get me wrong. From my point of view, there were no surprises.

Spoiler alert

Configuration management, on the other hand - this is true drama. Taking an existing shared development site and recreating it locally using Drush make and a basic profile (without the included config/install directory) is just a trivial soap opera. If you want real fun, visit the configuration-syncing aspect, armed only with knowledge of prior versions of Drupal and don’t RTFM.

Do RTFM

No, really. Do it.

The secret sauce in this recipe is…

After doing the export of the configuration in the previous section, I finally started running into the problems that I faced during my real world project - the project mentioned at the beginning of this post. Importing the configuration repeatedly and consistently failed with quite noisy and complex stack trace errors which were difficult to make sense of. Did I mention that perhaps I should have read the manual?

We need to do two things to make the configuration files usable in this tutorial before committing:

# Within the d8config_profile/config/install directory:
$ rm core.extension.yml update.settings.yml
$ find ./ -type f -exec sed -i '/^uuid: /d' {} \;

The removal of those two files was found to be required thanks to reading this and this. At this stage, I can confirm these were the only two files necessary for removal, and perhaps as Drupal 8’s configuration management becomes more sophisticated, this will not be necessary. The second command will recursively remove the lines with the uuid key/value pairs in all files.

top

Packaging it all up and running with it.

We’ve done all the preparation, and now need to make some small tweaks and commit them so our colleagues can start where we’ve left off. To do so we need to:

  1. add the profile to the makefile;
  2. commit our changes to the d8config_profile repo;
  3. tweak the config.yml file in the d8config-vm repo, to use our makefile and profile during provisioning.

To have the profile be installed by the makefile, add this to the bottom of d8config.make (in the D8config profile):

d8config_profile:
  type: profile
  download:
    type: git
    url: [email protected]:siliconmeadow/d8config_profile.git
    working-copy: true

I’ve committed the changes to the D8Config profile and tagged it as CG02.

Then the last change to make before testing our solution is to tweak the config.yml in the D8config VM repo. Three lines need changing:

# Change the drush_makefile_path:
drush_makefile_path: "/build/d8config/d8config_profile/d8config.make"

# Change the drupal_install_profile:
drupal_install_profile: d8config_profile

# Remove devel from the drupal_enable_modules array:
drupal_enable_modules: []

As you can see, the changes to the vagrant project are all about the profile.

With both the D8Config VM and the D8Config profile in adjacent folders, and confident that this is all going to work, from the host do:

# From the d8config-vm directory
$ vagrant destroy
# Type 'y' when prompted.

# Go!
$ vagrant up

Once the provisioning is complete, you should be able to check that the site is functioning at http://d8config.dev. Once there, check the presence of the custom content types, taxonomy, expected themes, placement of blocks, etc.

top

Summary and conclusion

The steps we’ve taken in this tutorial have given us an opportunity to look at the latest version of Drupal VM, build a quick-and-dirty prototype in Drupal 8 and make a profile which our colleagues can use to collaborate with us. I’ve pointed out some gotchas and in particular some things you will want to consider regarding exporting and importing Drupal 8 configuration settings.

There are more questions raised as well. For example, why not simply keep the d8config.make file in the d8config-vm repo? And what about the other ways people use Drupal VM in their workflow - for example here and here? Why not use the minimal profile when starting a protoype, and save the step of deleting content types?

Questions or comments? Please let me know. And next time we’ll just use Docker, shall we?

Mar 07 2016
Mar 07
teaser image for blog post

In learning about custom Drupal 8 module development, I found plenty of very simple field module examples, but none that covered how to store more than one value in a field and still have it work properly, so it's time to fix that.

To save you typing or copy and pasting things around all the code in this post is available on Github at https://github.com/ixis/dicefield

Concepts

There are three main elements to define when creating a field type:

  • The field base is the definition of the field itself and contains things like what properties it should have.
  • The field widget defines the form field that is used to put data into your field, what its rules are and how those data are manipulated and stored in the field.
  • The field formatter is how the field will be displayed to the end user and what options are configurable to customise that display.

So far, so familiar if you've ever worked with Drupal 7 fields, and this is like so much of Drupal 8: on the surface, to the end user, it's very similar, but behind the scenes, it's a whole new world.

Use case

To create a (probably quite limited-use, in all honesty) real-world example, I decided to take on the challenge of creating a field to represent dice notation. For example, if you see 1d6 you would grab a single six-sided die and roll it. If you see 3d6-2, you would roll 3 six-sided dice and subtract 2 from the result.

There are three components here:

  • The number of dice
  • The number of sides on each die
  • The modifier: the part that is added or subtracted at the end

Although in practice you could store the whole thing as one big string, and it would be a walk in the park to set up, you would lose some of the more useful functionality, such as search indexing and sorting at a database level. Suppose you wanted to create a view that filtered only field values that involved rolling 5 dice. With a multi-value field such as the one we're creating, it's simple to do. If you store everything as one big string, it involves pattern matching or loading all the results and sifting through them.

Info file

Info files are now YAML format, and this is covered in detail elsewhere, but here's what I came up with:

dicefield.info.yml

name: Dice field
type: module
description: A way of specifying a dice value such as 1d6 or 2d8+3.
package: Field types
version: 1.0
core: 8.x
 
dependencies:
  - field

This is nice and straightforward, and, obviously, our module must depend on the core field module, or it cannot work at all.

Note that Drupal 8 no longer requires anything more than an info file to enable a module; previous versions required an empty .module file at least.

Field base

Now things get interesting. We're going to create a new plugin class to define our field type. The system generally works by extending one of the existing types and making the necessary changes to it. This is the biggest piece of advice I can give beyond reading articles like these: do as little work as possible! Copy/paste from existing things in core or contributed modules and change them to suit (although obviously give credit where it's due).

It's worth noting that, unlike in Drupal 7, our dicefield.info.yml file does not contain a list of "includes" that Drupal 8 should know about. These are loaded automatically by the PSR-4 autoloader, which is both more efficient and more convenient than the previous method. It does mean, however, that you must be careful to lay out your folder structure carefully and make sure things are named properly, because these things do matter in Drupal 8.

src/Plugin/Field/FieldType/Dice.php

/**
 * @file
 * Contains \Drupal\dicefield\Plugin\Field\FieldType\Dice.
 */
 
namespace Drupal\dicefield\Plugin\Field\FieldType;
 
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
 
/**
 * Plugin implementation of the 'dice' field type.
 *
 * @FieldType (
 *   id = "dice",
 *   label = @Translation("Dice"),
 *   description = @Translation("Stores a dice roll such as 1d6 or 2d8+3."),
 *   default_widget = "dice",
 *   default_formatter = "dice"
 * )
 */
class Dice extends FieldItemBase {
  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return array(
      'columns' => array(
        'number' => array(
          'type' => 'int',
          'unsigned' => TRUE,
          'not null' => FALSE,
        ),
        'sides' => array(
          'type' => 'int',
          'unsigned' => TRUE,
          'not null' => TRUE,
        ),
        'modifier' => array(
          'type' => 'int',
          'not null' => TRUE,
          'default' => 0,
        ),
      ),
    );
  }
 
  /**
   * {@inheritdoc}
  */
  public function isEmpty() {
    $value1 = $this->get('number')->getValue();
    $value2 = $this->get('sides')->getValue();
    $value3 = $this->get('modifier')->getValue();
    return empty($value1) && empty($value2) && empty($value3);
  }
 
  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    // Add our properties.
    $properties['number'] = DataDefinition::create('integer')
      ->setLabel(t('Number'))
      ->setDescription(t('The number of dice'));
 
    $properties['sides'] = DataDefinition::create('integer')
      ->setLabel(t('Sides'))
      ->setDescription(t('The number of sides on each die'));
 
    $properties['modifier'] = DataDefinition::create('integer')
      ->setLabel(t('Modifier'))
      ->setDescription(t('The modifier to be applied after the roll'));
 
    $properties['average'] = DataDefinition::create('float')
      ->setLabel(t('Average'))
      ->setDescription(t('The average roll produced by this dice setup'))
      ->setComputed(TRUE)
      ->setClass('\Drupal\dicefield\AverageRoll');
 
    return $properties;
  }
}

This looks quite complicated and also quite alien from most things in Drupal 7 unless you're used to working with ctools plugins or the migrate system. Let's break it down a little bit.

The namespace

We have defined our namespace at very nearly the top of the file:

namespace Drupal\dicefield\Plugin\Field\FieldType;

The standard way is to use the Drupal namespace, followed by the name of your module (exactly the same as the name of the folder your module lives in), then the other bits. It's this namespace that will tell the PSR-4 autoloader where to find the classes it needs, so make sure it's correct!

Annotation-based plugin definition

There are multiple ways of defining the plugin's core data. The standard Drupal 8 way is to use annotations, which are like code comment blocks, but contain actual code rather than a comment. Other ways include YAML files, for example, but we're going to keep things simple here.

Note that one downside to using annotations to define plugin data is that since they are effectively comments, not all IDEs can interpret them in the same way as code, so you lose the syntax highlighting and code suggestions associated with writing PHP code in a modern IDE (we use PhpStorm internally). While this might look bad, it's actually not a huge deal because:

  • The plugin definition is a tiny part of your overall code base.
  • The code is right there in front of you, instead of in a separate file.
  • There are still other options if you really don't like it.

Here's the code in question:

/**
 * Plugin implementation of the 'dice' field type.
 *
 * @FieldType (
 *   id = "dice",
 *   label = @Translation("Dice"),
 *   description = @Translation("Stores a dice roll such as 1d6 or 2d8+3."),
 *   default_widget = "dice",
 *   default_formatter = "dice"
 * )
 */

Everything starting from the @FieldType is the plugin definition and everything above is just a regular comment, so you can still write a useful description if you like (and in fact, you should).

The @FieldType part tells Drupal 8 that it is a new field type. There are other annotations that can define various things in Drupal, and we'll see a few others later in the article.

There are a number of key/value pairs in the definition, and these work as follows:

  • id is used to give this plugin a machine name. This only needs to be unique for the type of thing being defined here, so you could have a FieldType called "dice" and also a FieldFormatter called "dice" without worrying about the implications of a namespace collision.
  • label uses the @Translation() notation, which is just like using Drupal's t() function, and provides a human-readable name to be used in the admin UI and other places.
  • description also uses @Translation and just lets users know what your field is for.
  • default_widget is the machine name of the widget that will be used, by default, when this field is put in place on an entity. If there are multiple widgets available, users will be able to pick, but this will be the default. Note that this refers to the machine name of the widget, not the class name. Drupal makes this distinction a lot, so you will become used to working with two different types of notation: Drupal internal machine names, and class names. The class name is not needed here. As long as we define a @FieldWidget plugin later, with an id of "dice", we will be good to go.
  • default_formatter works the same way as default_widget, but is used for the formatter (what the user sees on the front end, rather than the way data are put into your field). Note how these both have the same name. Because they're different plugin types (one is a FieldWidget and the other is a FieldFormatter), they can have the same name and Drupal 8 won't get confused.

There are also a number of other keys that you can use here, but these are best detailed by the Drupal documentation on Entity annotation, and we've covered the ones we need.

Extending classes

Nearly every class you write in Drupal 8 will extend another class, or implement an interface, or apply a trait, or perhaps any combination of those. For example:

class Dice extends FieldItemBase {

We are extending from the base field class here and this will give us all of the functionality we need to implement a new field type. All we have to do is override the methods that we want to work in a different way.

The schema

The schema is simply the definition for how the data will be stored (in the database, or whatever storage engine you're using). We need to return an array (apparently we're still stuck in "array inception" mode for some parts of Drupal 8 but thankfully this is now a lot less common) of arrays, that contain arrays that define the columns we want to store. Yeah, that.

/**
 * {@inheritdoc}
 */
 public static function schema(FieldStorageDefinitionInterface $field_definition) {
   return array(
     'columns' => array(
       'number' => array(
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => FALSE,
       ),
       'sides' => array(
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
       ),
       'modifier' => array(
        'type' => 'int',
        'not null' => TRUE,
        'default' => 0,
      ),
    ),
  );
}

We are basically only defining the columns key in the outer array. In practice it's probably a good idea to define indexes and possibly also foreign keys if your fields will be linked to other data, but let's keep things simple for now.

The definitions in columns work the same way as the Drupal 8 schema API which you can use for reference if you need to.

Notice we're defining three integer fields: one for the number of dice, one for the number of sides on each die, and one for the modifier.

The number of dice and the sides are mandatory, so they do not have a default value. However, you can safely assume that unless otherwise stated, the modifier is optional, and should default to zero, which is why this one has a default value.

Note also that the first two are unsigned, because you can't have a die with -6 sides. The modifier is not unsigned, because both +3 and -3 are valid for modifiers.

The final thing worth mentioning here is that we're only defining fields that are actually stored as data. Later on, we'll see how to derive a computed field, but since the field is a calculated value (which is then cached in the render cache, so stop sweating about performance already!) it is not stored in the database and shouldn't be defined here.

isEmpty

It's very important to tell Drupal how to know if your field is empty or not. Without this, certain basic field functionality will not work properly. In our case, it's quite straightforward: the field is only really "empty" if none of the three values contain anything.

/**
 * {@inheritdoc}
 */
public function isEmpty() {
  $value1 = $this->get('number')->getValue();
  $value2 = $this->get('sides')->getValue();
  $value3 = $this->get('modifier')->getValue();
  return empty($value1) && empty($value2) && empty($value3);
}

Notice how we're using the internal method $this->get() to grab the value? The properties attached to the field will be called the same thing as those in propertyDefinitions() (see below). It makes sense for them to also match the properties we have defined in schema() above, but this does not necessarily have to be the case. Just have a good reason for doing otherwise!

propertyDefinitions

Next, we define the properties that this field will have. These will be the individual pieces of data we can retrieve from the field, and will affect things like view sorting order and how we will set up our formatter later.

/**
 * {@inheritdoc}
 */
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
  // Add our properties.
  $properties['number'] = DataDefinition::create('integer')
    ->setLabel(t('Number'))
    ->setDescription(t('The number of dice'));
 
  $properties['sides'] = DataDefinition::create('integer')
    ->setLabel(t('Sides'))
    ->setDescription(t('The number of sides on each die'));
 
  $properties['modifier'] = DataDefinition::create('integer')
    ->setLabel(t('Modifier'))
    ->setDescription(t('The modifier to be applied after the roll'));
 
  $properties['average'] = DataDefinition::create('float')
    ->setLabel(t('Average'))
    ->setDescription(t('The average roll produced by this dice setup'))
    ->setComputed(TRUE)
    ->setClass('\Drupal\dicefield\AverageRoll');
 
  return $properties;
}

Note that we have the same three properties that we defined as being stored in schema() above, plus a fourth one, called average. This is a computed field, which means that instead of storing the value in the database, we derive it from the values of the other fields. It is more useful to do it this way, because the average value is just the sum of the minimum and maximum possible roll, halved, then added to the modifier. If we were to store this in the database we would be wasting database space. You might think it inefficient to compute this value, but in fact, it's cached by Drupal's render cache system, and invalidated only when the field is updated, so except for the first time it's computed, it's not generally a performance hindrance.

Each of our four properties are basic types as defined by Drupal's typed data API. We have three integers and a float, but we could also use string or other types if we wanted to. We could even come up with our own types, but that's not necessary for this field so I won't cover it here.

We just return an array of properties by using DataDefiniton::create() and chaining the methods we want in order to create the property. As a minimum, you should use setLabel() and setDescription, but there are plenty of others that you can use. The fourth property, average, has two extra methods.

setComputed() is used to indicate that this field is computed rather than stored in the database, so there won't be a matching column in schema().

Given that it's computed, Drupal needs to know what class to use to do this computation, and this is where setClass() comes in. See below for more about computing field values in their own classes.

Widget

Now that we've set up our field base, we need to set up a widget so that people editing a node (or other entity) where this field is used are able to input or edit the data.

src/Plugin/Field/FieldWidget/DiceWidget.php

/**
 * @file
 * Contains \Drupal\dicefield\Plugin\Field\FieldWidget\DiceWidget.
 */
 
namespace Drupal\dicefield\Plugin\Field\FieldWidget;
 
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
 
/**
 * Plugin implementation of the 'dice' widget.
 *
 * @FieldWidget (
 *   id = "dice",
 *   label = @Translation("Dice widget"),
 *   field_types = {
 *     "dice"
 *   }
 * )
 */
class DiceWidget extends WidgetBase {
  /**
   * {@inheritdoc}
   */
  public function formElement(
    FieldItemListInterface $items,
    $delta,
    array $element,
    array &$form,
    array &$form_state
  ) {
    $element['number'] = array(
      '#type' => 'number',
      '#title' => t('# of dice'),
      '#default_value' => isset($items[$delta]->number) ? $items[$delta]->number : 1,
      '#size' => 3,
    );
    $element['sides'] = array(
      '#type' => 'number',
      '#title' => t('Sides'),
      '#field_prefix' => 'd',
      '#default_value' => isset($items[$delta]->sides) ? $items[$delta]->sides : 6,
      '#size' => 3,
    );
    $element['modifier'] = array(
      '#type' => 'number',
      '#title' => t('Modifier'),
      '#default_value' => isset($items[$delta]->modifier) ? $items[$delta]->modifier : 0,
      '#size' => 3,
    );
 
    // If cardinality is 1, ensure a label is output for the field by wrapping
    // it in a details element.
    if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() == 1) {
      $element += array(
        '#type' => 'fieldset',
        '#attributes' => array('class' => array('container-inline')),
      );
    }
 
    return $element;
  }
}

Class

Once again, we are inheriting from the base WidgetBase class because almost all of the work involved in being a widget is done for us, so we only need to lift a finger to tell Drupal what's different from the base.

class DiceWidget extends WidgetBase {

Annotation

Again, we see that the annotation at the top of the class defines basic data on this widget:

/**
 * Plugin implementation of the 'dice' widget.
 *
 * @FieldWidget (
 *   id = "dice",
 *   label = @Translation("Dice widget"),
 *   field_types = {
 *     "dice"
 *   }
 * )
 */

This time, @FieldWidget tells Drupal it's dealing with a widget, and the id and label properties work the same way as for the base field above.

We have an array this time, in the form of field_types, which tells Drupal which types of field are allowed to use this widget. Note that unlike regular PHP arrays in Drupal, you must not put a comma after the last element in these arrays.

This field_types allows us to create new widgets, even for existing field types, in case we want a better or different way of inputting data. For example, a geo-location field that stores map coordinates might have a text widget for inputting the data manually, and a separate map widget that allows the user to click on a map to choose a point.

formElement

In actual fact, we only need to override one method in this class:

  /**
   * {@inheritdoc}
   */
  public function formElement(
    FieldItemListInterface $items,
    $delta,
    array $element,
    array &$form,
    FormStateInterface $form_state,
  ) {
    $element['number'] = array(
      '#type' => 'number',
      '#title' => t('# of dice'),
      '#default_value' => isset($items[$delta]->number) ? $items[$delta]->number : 1,
      '#size' => 3,
    );
    $element['sides'] = array(
      '#type' => 'number',
      '#title' => t('Sides'),
      '#field_prefix' => 'd',
      '#default_value' => isset($items[$delta]->sides) ? $items[$delta]->sides : 6,
      '#size' => 3,
    );
    $element['modifier'] = array(
      '#type' => 'number',
      '#title' => t('Modifier'),
      '#default_value' => isset($items[$delta]->modifier) ? $items[$delta]->modifier : 0,
      '#size' => 3,
    );
 
    // If cardinality is 1, ensure a label is output for the field by wrapping
    // it in a details element.
    if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() == 1) {
      $element += array(
        '#type' => 'fieldset',
        '#attributes' => array('class' => array('container-inline')),
      );
    }
 
    return $element;
  }

This method tells Drupal how to render the form for this field. Because we need to know three things (the number of dice, the sides per die, and the modifier), we will provide three fields for this. Note how the form keys for these fields match what we defined in schema() and propertyDefinitions() above.

The #attributes on the fieldset causes the fields to be displayed inline instead of one line after another.

These fields use the number field type, which is basically a text field but with little up and down arrows that can be used to increase or decrease the value. It also provides some basic validation in that you need to put a numerical value in here, not a string, and there's no need to write this validation if it's already done for us.

The #default_value key shows how to extract the value from the current field. In the case where we're editing a field, we want the existing values to be in the form ready to be changed, and $items[$delta]->PROPERTY_NAME will do that for us.

I have also set up a default value in the case where we're creating a completely new node, as I felt it was nice to be able to show an example of the required input. Also, since the modifier is often zero, it makes sense to set this as a default value. I could have also used #placeholder to put an HTML5 placeholder value in the field instead of real input.

The last part of this method simply adds a fieldset so that if the field cardinality is 1 (only 1 "dice roll" field value can be put in, instead of allowing unlimited, or a higher number of entries), then the label for the field will still show up properly. This can be used as-is for most field widgets.

Formatter

The last required step (and it might not even be required if you can re-purpose a core formatter from Drupal itself) is to set up a new formatter. This will output the information in the field onto the screen so that users can see it.

src/Plugin/Field/FieldFormatter/DiceFormatter.php

/**
 * @file
 * Contains \Drupal\dicefield\Plugin\Field\FieldFormatter\DiceFormatter.
 */
 
namespace Drupal\dicefield\Plugin\Field\FieldFormatter;
 
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
 
/**
 * Plugin implementation of the 'dice' formatter.
 *
 * @FieldFormatter (
 *   id = "dice",
 *   label = @Translation("Dice"),
 *   field_types = {
 *     "dice"
 *   }
 * )
 */
class DiceFormatter extends FormatterBase {
  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode = NULL) {
    $elements = array();
 
    foreach ($items as $delta => $item) {
      if ($item->sides == 1) {
        // If we are using a 1-sided die (occasionally sees use), just write "1"
        // instead of "1d1" which looks silly.
        $markup = $item->number * $item->sides;
      }
      else {
        $markup = $item->number . 'd' . $item->sides;
      }
 
      // Add the modifier if necessary.
      if (!empty($item->modifier)) {
        $sign = $item->modifier > 0 ? '+' : '-';
        $markup .= $sign . $item->modifier;
      }
 
      $elements[$delta] = array(
        '#type' => 'markup',
        '#markup' => $markup,
      );
    }
 
    return $elements;
  }
}

Annotation

The annotation defines the basic formatter data, and works just like the others above.

  /**
   * Plugin implementation of the 'dice' formatter.
   *
   * @FieldFormatter (
   *   id = "dice",
   *   label = @Translation("Dice"),
   *   field_types = {
   *     "dice"
   *   }
   * )
   */

In fact, this is almost identical to the one for DiceWidget that we defined above, except we're now using the @FieldFormatter type.

Class

As we've seen above, it's easiest to just extend the base class, FormatterBase, since this does all the heavy lifting already and we can pick and choose what to override.

class DiceFormatter extends FormatterBase {

viewElements

We are overriding the method that actually produces the markup that will be displayed on the page:

/**
 * {@inheritdoc}
 */
public function viewElements(FieldItemListInterface $items, $langcode = NULL) {
  $elements = array();
 
  foreach ($items as $delta => $item) {
    if ($item->sides == 1) {
      // If we are using a 1-sided die (occasionally sees use), just write "1"
      // instead of "1d1" which looks silly.
      $markup = $item->number * $item->sides;
    }
    else {
      $markup = $item->number . 'd' . $item->sides;
    }
 
    // Add the modifier if necessary.
    if (!empty($item->modifier)) {
      $sign = $item->modifier > 0 ? '+' : '-';
      $markup .= $sign . $item->modifier;
    }
 
    $elements[$delta] = array(
      '#type' => 'markup',
      '#markup' => $markup,
    );
  }
 
  return $elements;
}

The most important thing here is that we loop through the $items because each field could have a cardinality of greater than one, meaning multiple dice rolls can be stored in a single field.

There is a fringe case where technically it's possible (not in the physical world) to have a one-sided die, which will always roll a 1, no matter what, so instead of writing 1d1, we tell the formatter to present it as just 1 for clarity.

Next we add the modifier, but only if it's a non-zero, because 1d6 looks cleaner than 1d6+0. Note that the modifier could be positive or negative, so we need to account for that in the code. Negative numbers, when converted to strings, already have a negative symbol at the front, but positive ones don't, so we add that on.

The last part is quite important, and that is presenting the return value as a series of render arrays, rather than just plain text. Everything should be presented as a render array where possible, because this allows Drupal to delay its rendering until the last possible moment, affording other modules the opportunity to override where necessary.

AverageRoll type

Earlier, we defined a computed field when we set up propertyDefinitions(). This means that we need to tell Drupal how to compute the value of this field, and we will create a separate class for this.

src/AverageRoll.php

/**
 * @file
 * Contains \Drupal\dicefield\AverageRoll.
 */
 
namespace Drupal\dicefield;
 
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\String;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedData;
 
/**
 * A computed property for an average dice roll.
 */
class AverageRoll extends TypedData {
 
  /**
   * Cached processed value.
   *
   * @var string|null
   */
  protected $processed = NULL;
 
  /**
   * Implements \Drupal\Core\TypedData\TypedDataInterface::getValue().
   */
  public function getValue($langcode = NULL) {
    if ($this->processed !== NULL) {
      return $this->processed;
    }
 
    $item = $this->getParent();
 
    // The minimum roll is the same as the number of dice, which will occur if
    // all dice come up as a 1. Then apply the modifier.
    $minimum = $item->number + $item->modifier;
 
    // The maximum roll is the number of sides on each die times the number of
    // dice. Then apply the modifier.
    $maximum = ($item->number * $item->sides) + $item->modifier;
 
    // Add together the minimum and maximum and divide by two. In cases where we
    // get a fraction, take the lower boundary.
    $this->processed = ($minimum + $maximum) / 2;
    return $this->processed;
  }
 
  /**
   * Implements \Drupal\Core\TypedData\TypedDataInterface::setValue().
   */
  public function setValue($value, $notify = TRUE) {
    $this->processed = $value;
 
    // Notify the parent of any changes.
    if ($notify && isset($this->parent)) {
      $this->parent->onChange($this->name);
    }
  }
}

There is no annotation!

Since we're not defining a plugin here, there's no annotation at the top of this class. There's no need.

Class

We are simply extending an existing type, to do the heavy lifting for us, as with previous classes above.

class AverageRoll extends TypedData {

Caching

I mentioned earlier that Drupal's render cache means that we only need to process the value when it changes, rather than every time we see the field, and we can add a mechanism to achieve this. It's via a protected property:

/**
 * Cached processed value.
 *
 * @var string|null
 */
protected $processed = NULL;

Note how even though this is just a property on a class, it is still fully documented like anything else!

getValue

This method will, as the name suggests, get the value of the computed field when Drupal asks for it. Note that method doesn't need to be called directly. Drupal takes care of this internally which means that you can just use $item->average instead of having to write $item->average->getValue() or anything complicated like that.

/**
 * Implements \Drupal\Core\TypedData\TypedDataInterface::getValue().
 */
public function getValue($langcode = NULL) {
  if ($this->processed !== NULL) {
    return $this->processed;
  }
 
  $item = $this->getParent();
 
  // The minimum roll is the same as the number of dice, which will occur if
  // all dice come up as a 1. Then apply the modifier.
  $minimum = $item->number + $item->modifier;
 
  // The maximum roll is the number of sides on each die times the number of
  // dice. Then apply the modifier.
  $maximum = ($item->number * $item->sides) + $item->modifier;
 
  // Add together the minimum and maximum and divide by two. In cases where we
  // get a fraction, take the lower boundary.
  $this->processed = ($minimum + $maximum) / 2;
  return $this->processed;
}

At the beginning of this method is the test to see if we have already assigned a value to $this->processed. If we have, we don't need to compute the value. We only do that part if the value is null, to save on processing power.

The internals of this method first work out the minimum and maximum possible rolls, applying the modifier to each, and then divide their total by two, which gives the average. For many dice rolls this will be a fraction, which is why we have defined this field as a float rather than an integer.

setValue

Lastly, to make sure cache invalidation works correctly, we need to define a method to take care of setting our value:

/**
 * Implements \Drupal\Core\TypedData\TypedDataInterface::setValue().
 */
public function setValue($value, $notify = TRUE) {
  $this->processed = $value;
 
  // Notify the parent of any changes.
  if ($notify && isset($this->parent)) {
    $this->parent->onChange($this->name);
  }
}

Here, we set $this->processed to the new value, but we also notify the class's parent via the onChange() method. Remember that the vast majority of implementation is handled by the base class instead of making us do the work here, so we ought to be happy to pass the buck to Drupal itself where we can!

AverageRollFormatter

Lastly, it's worth bearing in mind that we have a formatter than can display the dice roll itself, and we also have a computed field that can calculate the average roll, but we have no way of actually showing the average roll to the end user. We will create one more formatter class that will take care of this for us. Because this is a separate formatter, it will allow the site administrator to choose, when displaying dice fields (in views, say), whether to show the dice notation, the average value, or even both (by adding the field twice with different formatters).

src/Plugin/Field/FieldFormatter/AverageRollFormatter.php

/**
 * @file
 * Contains \Drupal\dicefield\Plugin\Field\FieldFormatter\AverageRollFormatter.
 */
 
namespace Drupal\dicefield\Plugin\Field\FieldFormatter;
 
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
 
/**
 * Plugin implementation of the 'average_roll' formatter.
 *
 * @FieldFormatter (
 *   id = "average_roll",
 *   label = @Translation("Average roll"),
 *   field_types = {
 *     "dice"
 *   }
 * )
 */
class AverageRollFormatter extends FormatterBase {
  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode = NULL) {
    $elements = array();
 
    foreach ($items as $delta => $item) {
      $elements[$delta] = array(
        '#type' => 'markup',
        '#markup' => $item->average,
      );
    }
 
    return $elements;
  }
}

The details here are much the same as our DiceFormatter from above, but the implementation is even more simple. Since the value of the field is computed for us already, all we need to do is print it out (by putting it into a render array) in viewElements().

One thing to bear in mind here is that we could have done the average calculation directly in the formatter. There's certainly scope for it: we have all the field data and the ability to execute whatever custom code we like. However, this would have been the wrong thing to do because it would have meant that the average can only be displayed on the front end, and not accessible internally to Drupal. By doing it the way we have done it above, we ensure that, for example, views is able to sort this field by average value, or another developer can plug into this module and grab the average value for his or her own use.

Wrapping up

Now that you have all the elements in place, you can enable the module and try it out!

Once enabled, you will see the new "dice roll" option when picking a field type to add to an entity. When added, you can try creating a new entity of this type, and see the widget in action. Then, the formatter will take care of showing the end result on your entity's view page. Don't forget: you have a choice of two formatters, so you can either show the dice notation or the average value (or even both if you use a custom template or views).

If you have any comments, corrections or observations about this tutorial, please feel free to leave them in the comments below. Hopefully the information about the principles and design decisions will be useful!

The full codebase is available on Github.

Jan 01 2015
Jan 01

In this tutorial we will

1. Configure Views to show nodes with only the current language.

2. Translate  strings in views that cant be translated via admin UI.

3. Translate menu items.

1. On each views (pages, blocks etc) we need to add filter criteria ‘Content: Language’ and choose ‘Current user’s language’ – https://screencast.com/t/hvRR4XmL8DQZ After this you the view will only show nodes that contain your current language on site.

2. Sometimes we have views with a difficult structure and fields with a lot of HTML tags, tokens and strings. These can’t be translated via Drupal admin UI. For example this global custom field – https://screencast.com/t/S0U6Zibb0p54 a method for translating something irregular like this is as follows.  Create a .tpl file for this field and wrap strings and links in t() and l() functions.  https://screencast.com/t/3Dj1gGjEe after this we Then these can easily found and translated in admin UI.

The Same method can be used for other views templates and nodes if there are links

3. To translate the menu we need to enable ‘Menu translation’ module (from i18n). Now we go to edit menu and enable translation mode – https://screencast.com/t/JRw0ngrBa8  Now we can translate any menu item – https://screencast.com/t/s6maX10qk

Then add (or edit) the Arabic variant  – https://screencast.com/t/LGoewMdCo

Nov 14 2012
Nov 14

This is a small tidbit of information in the event that you wanted to alter the Drupal search results page. You can add a custom CSS class to the last search result item (for whatever reason you may have). In my case, I wanted to remove the border-bottom from the last result, so I had to add a special CSS class to do this.

Just follow these simple steps:

  1. Override template_preprocess_search_results

    Here is how to alter the code. This goes in your template.php:

    function yourthemename_preprocess_search_results(&$variables) {
      $variables['search_results'] = '';
      if (!empty($variables['module'])) {
        $variables['module'] = check_plain($variables['module']);
      }
      //checking the total number of results
      $num_results = count($variables['results']);
      $counter = 0;
      foreach ($variables['results'] as $result) {
        $counter++;
       
        if ($num_results == $counter) {
            //means we have the last result so we add the class
            $variables['search_results'] .= theme('search_result', array('result' => $result, 'module' => $variables['module'], 'last' => 'last'));   
        }
        else {
            $variables['search_results'] .= theme('search_result', array('result' => $result, 'module' => $variables['module']));
        }
      }
     
      $variables['pager'] = theme('pager', array('tags' => NULL));
      $variables['theme_hook_suggestions'][] = 'search_results__' . $variables['module'];
    }

  2. Now we override search-result.tpl.php Create this file and put it in your custom theme folder.

    <li class="<?php print $classes.' '.$last; ?>"<?php print $attributes; ?>>
        <?php print render($title_prefix); ?>
        <h3 class="title"<?php print $title_attributes; ?>>
        <a href="<?php print $url; ?>"><?php print $title; ?></a>
        </h3>
        <?php print render($title_suffix); ?>
        <div class="search-snippet-info">
        <?php if ($snippet): ?>
        <p class="search-snippet"<?php print $content_attributes; ?>><?php print $snippet; ?></p>
        <?php endif; ?>
        <?php if ($info): ?>
        <p class="search-info"><?php print $info; ?></p>
        <?php endif; ?>
        </div>
        </li>

  3. Clear your cache

Now if you search for something, you will notice that your very last search result has the CSS class of "last".

This also works for search results that have a pager. That is, the last result on every page will have the class of "last".

May 31 2012
May 31

[box type=”note”]Unless you are using MediaTemple, you should try the directions to install Drush using Pear on a shared host first, as that method is preferred.[/box]

A while back I wrote a quick guide for getting Drush up and running on a shared Dreamhost account and it was great to see lots of folks taking advantage of the power of Drush on a more commodity (read: cheap) host. But what if you’re running on a slightly more expensive and more featured host like MediaTemple’s grid offering? Well; you can run Drush there too, and I’ll show you how.

First off, we’re going the more manual route this time since I’ve had more success with it on MT. So head on over to the Drupal.org Drush project page and copy the latest tar.gz version of Drush to your clipboard. Then, log into your MT (gs) instance (or just run the command `cd` if you already have an SSH session open to your (gs) already) and create a directory called bin in your home directory and enter it:

mkdir ~/bin
cd ~/bin/

Next, download and extract the tarball of Drush you copied earlier:

wget http://ftp.drupal.org/files/projects/drush-All-versions-5.x-dev.tar.gz
tar xvpfz drush-*.tar.gz

Now, make a new folder in your home directory called .drush and enter it:

mkdir ~/.drush
cd ~/.drush/

Because of the more tightened security on the default CLI version of PHP MediaTemple provides we’re going to need to override some configuration values to suit us. In the ~/.drush/ directory make a file named drush.ini and place in it the following values (I prefer to use nano drush.ini for this):

memory_limit = 128M
error_reporting = E_ALL | E_NOTICE | E_STRICT
display_errors = stderr
safe_mode =
open_basedir =
disable_functions =
disable_classes =

Lastly, we need to add Drush into our path and reload the BASH configuration:

echo "export PATH=\"~/bin/drush\:$PATH\"" >> ~/.bash_profile
source ~/.bash_profile

That’s it! You now have a fully working Drush on MediaTemple; get out there and `drush up`. Go on, I know you want to :)

tl;dr: Install the latest 5.x-dev Drush on your (gs) by running this long command, or replace the highlighted tar.gz with the version you want:
mkdir ~/bin;cd ~/bin/;wget http://ftp.drupal.org/files/projects/drush-All-versions-5.x-dev.tar.gz;tar xvpfz drush-*.tar.gz;mkdir ~/.drush;cd ~/.drush/;echo -e "memory_limit = 128M\nerror_reporting = E_ALL | E_NOTICE | E_STRICT\ndisplay_errors = stderr\nsafe_mode =\nopen_basedir =\ndisable_functions =\ndisable_classes =" >> drush.ini;echo "export PATH=\"~/bin/drush\:$PATH"" >> ~/.bash_profile;source ~/.bash_profile

Updated 2/14/2013: Dreamhost made some config changes that required a different PATH variable for this method to work. The instructions above have been updated to reflect this. Editing drush.ini is now suggested to be done with nano, since some folks were having issues with the quad-chevron approach.

[box type=”note”]

Robin Monks has a passion for openness and freedom in technology and he’s spent the last 8 years of his life developing, supporting and maintaining open source software. He’s part of the panel of Drupal community experts who authored The Definitive Guide to Drupal 7 and currently provides independent consulting services. Reach him at 1-855-PODHURL.[/box]

Share this:

May 28 2012
May 28

In my last post, we learned how to customize CKEditor with the WYSIWYG Module. We explored how to apply our own settings to CKEditor using a little javascript and a small custom module. Today we're going to delve even deeper into what we can customize in CKEditor. Specifically we'll be looking into the dialog API. By the time we're done with it, our CKEditor will be jacked up with a faux-hawk,  flaming skull tattoos, and ready to stroll into the club and punch the first guy that looks at him cock-eyed.

Getting Started

To start, you'll need to have read my last post (http://fuseinteractive.ca/blog/wysiwyg-module-ckeditor-taming-beast) because that's where we're taking off from. You'll also need the little module we made because we're going to be adding some stuff to it. We won't be doing much php here, but a bit of javascript knowledge and OOP concepts will be helpful, especially if you want to make your own customizations beyond what we go over in this tutorial.

This tutorial should work for either Drupal 6 or 7. If you want to skip the tutorial and just browse the module code (there's lots of comments), I've attached a zipped copy of the finished module at the bottom of the post.

1. Dialog Defaults

Clients always seem to want the "Table" button in CKEditor and I sort of hate giving it to them because they always look terrible in the end. More often than not, however, it can't be avoided. Recently I had a client who didn't want borders around their tables. I told them to just change the "border" field to zero in the dialog, and they said "Can't you just change the default?". I said "No", then they said "Seriously?", then I said "I'll look into it". And I looked into it. Turns out you can and it's actually kind of easy, thanks to our ckeditor_custom module. All you need to do is open up the ckeditor_custom_config.js file in your favorite text editor and add this to the end of the file:

// When opening a dialog, a "definition" is created for it. For
// each editor instance the "dialogDefinition" event is then
// fired. We can use this event to make customizations to the
// definition of existing dialogs.
CKEDITOR.on( 'dialogDefinition', function( event ) {
 
  // Take the dialog name
  var dialogName = event.data.name;
 
  // The definition holds the structured data that is used to eventually
  // build the dialog and we can use it to customize just about anything.
  // In Drupal terms, it's sort of like CKEditor's version of a Forms API and
  // what we're doing here is a bit like a hook_form_alter.
  var dialogDefinition = event.data.definition;
 
  // Uncomment to print the dialogDefinition to the console
  // console.log(dialogDefinition);
 
  // Check if the definition is from the dialog we're
  // interested in (the "table" dialog).
  if ( dialogName == 'table' ) {
 
    // .getContents() returns an object reference to a set of fields in the
    // dialog, also referred to as tabs. The Table dialog has two tabs:
    // "Table Properties" and "Advanced". Each of those has an id. In this case,
    // the id we're interested in is 'info' for the Table Properties tab.
    var infoTab = dialogDefinition.getContents( 'info' );
 
    // Once we have the tab reference, we can use the object's .get() method
    // to get another object reference, this time to the field we want to change
    // Fields also have ids. The border field id is "txtBorder"
    var borderSizeField = infoTab.get("txtBorder");
 
    // Set the border to 0 (who uses html table borders anyway?)
    borderSizeField['default'] = 0;
  }
});

Then, turn on your browser's javascript console (in chrome go View > Developer > Javascript Console), reload the page, and click the table button. An object will be spat out into your console. You can expand it to see the various properties and methods that belong to the dialog definition. Here's what mine looks like:

I expanded the "contents" propery because I know that's where they store the tabs and fields. Oh look, it's the id we're looking for: "advanced". I guess I could have guessed that, but now I know for sure. I can do the same thing to figure out which field to customize:

var advancedTab = dialogDefinition.getContents( 'advanced' );
console.log(advancedTab);

And the output again:

Cool. It's a big mess of nested objects and arrays. Spitting it out is easy, figuring out what you're looking for is a the hard part. It's sort of a mix of examining the object in the console, reading through the API documentation, and a bit of testing. For example, you may have been wondering how I knew about the .getContents() and .get() methods. Well it was a combination of the API documentation (specifically the definitionObject class and the contentObject class)  and the example code on one of the "How to" pages. Also, the dialog API example page was helpful.

Another REALLY nifty thing that you can do is use the Developer Tools plugin which was added in CKEditor 3.6.

2. Custom Dialogs

So what else can we do? Well, you could define your own dialog and assign it to a button in the toolbar. Why would you want to do this? I don't know. But it could come in handy. Just for fun, let's make a new dialog that takes a youtube Video ID, and a few parameters, and outputs the correct iframe html. (Yes, there are plenty of existing drupal modules that handle youtube embedding already, but really this is more about learning the API, not about innovation).

Open up your ckeditor_custom.js file and add this to the end: 

// Listen for the "pluginsLoaded" event, so we are sure that the
// "dialog" plugin has been loaded and we are able to do our
// customizations. We're going to do this for every instance of CKEditor
// but technically you could only do it for certain ones
for (var editorId in CKEDITOR.instances) {
 
  // Get a reference to the editor
  var editor = CKEDITOR.instances[editorId];
 
  // Add the even listener with the editor's .on() function
  editor.on('pluginsLoaded', function(ev) {
 
    // If our custom dialog has not been registered, do that now.
    if (!CKEDITOR.dialog.exists('youtubeDialog')) {
 
      // Register the dialog. The actual dialog definition is below
      CKEDITOR.dialog.add('youtubeDialog', ytDialogDefinition);
    }
 
    // Now that CKEditor knows about our dialog, we can create a
    // command that will open it
    editor.addCommand('youtubeDialogCmd', new CKEDITOR.dialogCommand( 'youtubeDialog' ));
 
    // Finally we can assign the command to a new button that we'll call youtube
    // Don't forget, the button needs to be assigned to the toolbar
    editor.ui.addButton( 'YouTube',
      {
        label : 'You Tube',
        command : 'youtubeDialogCmd'
      }
    );
  });
}
 
/*
  Our dialog definition. Here, we define which fields we want, we add buttons
  to the dialog, and supply a "submit" handler to process the user input
  and output our youtube iframe to the editor text area.
*/
var ytDialogDefinition = function (editor) {
 
  var dialogDefinition =
  {
    title : 'YouTube Embed',
    minWidth : 390,
    minHeight : 130,
    contents : [
      {
        // To make things simple, we're just going to have one tab
        id : 'tab1',
        label : 'Settings',
        title : 'Settings',
        expand : true,
        padding : 0,
        elements :
        [
          {
            // http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.dialog.definition.vbox.html
            type: 'vbox',
            widths : [ null, null ],
            styles : [ 'vertical-align:top' ],
            padding: '5px',
            children: [
              {
                // http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.dialog.definition.html.html
                type : 'html',
                padding: '5px',
                html : 'You can find the youtube video id in the url of the video. 
 e.g. http://www.youtube.com/watch?v=<strong>VIDEO_ID</strong>.'
              },
              {
                // http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.dialog.definition.textInput.html
                type : 'text',
                id : 'txtVideoId',
                label: 'YouTube Video ID',
                style: 'margin-top:5px;',
                'default': '',
                validate: function() {
                  // Just a little light validation
                  // 'this' is now a CKEDITOR.ui.dialog.textInput object which
                  // is an extension of a CKEDITOR.ui.dialog.uiElement object
                  var value = this.getValue();
                  value = value.replace(/http:.*youtube.*?v=/, '');
                  this.setValue(value);
                },
                // The commit function gets called for each form element
                // when the dialog's commitContent Function is called.
                // For our dialog, commitContent is called when the user
                // Clicks the "OK" button which is defined a little further down
                commit: commitValue
              },
            ]
          },
          {
            // http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.dialog.definition.hbox.html
            type: 'hbox',
            widths : [ null, null ],
            styles : [ 'vertical-align:top' ],
            padding: '5px',
            children: [
              {
                type : 'text',
                id : 'txtWidth',
                label: 'Width',
                // We need to quote the default property since it is a reserved word
                // in javascript
                'default': 500,
                validate : function() {
                  var pass = true,
                    value = this.getValue();
                  pass = pass &amp;&amp; CKEDITOR.dialog.validate.integer()( value )
                    &amp;&amp; value &gt; 0;
                  if ( !pass )
                  {
                    alert( "Invalid Width" );
                    this.select();
                  }
                  return pass;
                },
                commit: commitValue
              },
              {
                type : 'text',
                id : 'txtHeight',
                label: 'Height',
                'default': 300,
                validate : function() {
                  var pass = true,
                    value = this.getValue();
                  pass = pass &amp;&amp; CKEDITOR.dialog.validate.integer()( value )
                    &amp;&amp; value &gt; 0;
                  if ( !pass )
                  {
                    alert( "Invalid Height" );
                    this.select();
                  }
                  return pass;
                },
                commit: commitValue
              },
              {
                // http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.dialog.definition.checkbox.html
                type : 'checkbox',
                id : 'chkAutoplay',
                label: 'Autoplay',
                commit: commitValue
              }
            ]
          }
        ]
      }
    ],
 
    // Add the standard OK and Cancel Buttons
    buttons : [ CKEDITOR.dialog.okButton, CKEDITOR.dialog.cancelButton ],
 
    // A "submit" handler for when the OK button is clicked.
    onOk : function() {
 
      // A container for our field data
      var data = {};
 
      // Commit the field data to our data object
      // This function calls the commit function of each field element
      // Each field has a commit function (that we define below) that will
      // dump it's value into the data object
      this.commitContent( data );
 
      if (data.info) {
        var info = data.info;
        // Set the autoplay flag
        var autoplay = info.chkAutoplay ? 'autoplay=1': 'autoplay=0';
        // Concatenate our youtube embed url for the iframe
        var src = 'http://youtube.com/embed/' + info.txtVideoId + '?' + autoplay;
        // Create the iframe element
        var iframe = new CKEDITOR.dom.element( 'iframe' );
        // Add the attributes to the iframe.
        iframe.setAttributes({
          'width': info.txtWidth,
          'height': info.txtHeight,
          'type': 'text/html',
          'src': src,
          'frameborder': 0
        });
        // Finally insert the element into the editor.
        editor.insertElement(iframe);
      }
 
    }
  };
 
  return dialogDefinition;
};
 
// Little helper function to commit field data to an object that is passed in:
var commitValue = function( data ) {
  var id = this.id;
  if ( !data.info )
    data.info = {};
  data.info[id] = this.getValue();
};

I'm not going to go into too much detail about the above code (I'll let the comments help guide you along), but just to recap, there's a couple of key concepts going on here. The first bit of code, we're dealing with registering the dialog with CKEditor so it knows it exists and how to find it. We're also creating the button that will be available in the toolbar, and binding it to a new command that will launch the dialog. The second part of the code we're actually defining the dialog itself. We add some form elements and buttons, a couple layout boxes, and finally a "submit" function that handles the user input and outputs to the editor text area.

Okay so that's all well and good. Clear the necessary caches and reload the page your editor is on. Wait... there's no button. That's because there's one more step we need to do. Since the available buttons are defined in the WYSIWYG profile, our button never gets passed on to the toolbar. To make things simple, we're going to manually add it in our ckeditor_custom.module file:

if (!empty($remaining_buttons)) {
  // reset the array keys and add it to the $new_grouped_toolbar
  $new_grouped_toolbar[] = array_values($remaining_buttons);
}
 
// This is our new youtube command / dialog that we created in
// ckeditor_custom_config.js. If we don't add this here, it won't
// show up in the toolbar!
$new_grouped_toolbar[] = array('YouTube');
 
// Replace the toolbar with our new, grouped toolbar.
$settings['toolbar'] = $new_grouped_toolbar;

Now, reload the page with your editor on it (you may need a cache clear) and you should see your new button in the toolbar. Unfortunately, the button is blank. That's okay though because we can style it up with a little css. First, create a css file called "ckeditor_custom.css" in the module folder with this css in it:

/* Hide the icon */
.cke_button_youtubeDialogCmd .cke_icon {
  display: none !important;
}
 
/* Show the label */
.cke_button_youtubeDialogCmd .cke_label {
  display: inline !important;
}

The we need to add the css to the page in our module file, just after where we added the YouTube button

// This is our new youtube command / dialog that we created in
// ckeditor_custom_config.js. If we don't add this here, it won't
// show up in the toolbar!
$new_grouped_toolbar[] = array('YouTube');
// Add a css file to the page that will style our youtube button
drupal_add_css(drupal_get_path('module', 'ckeditor_custom') . '/ckeditor_custom.css');

Reload the page, and you should see your button there in all it's glory. Try it out. It should look something like this:

3. Conclusion

So we've done it. We customized an existing dialog and even created our own. To be honest, however, our dialog implementation probably wasn't the most robust way of doing it. The WYSIWYG module actually comes with api functions to add your own "plugins" as buttons in the toolbar. This would really be the way to go moving forward as it would integrate the plugin into WYSIWYG and would make the button available in the WYSIWYG profile settings page. Plus it would be the more "Drupal" way of doing things. My example above was really more of a quick and dirty dialog implementation that works in a pinch. Plus, it gave us a change to quickly explore the CKEditor API and learn something new.

Perhaps in a future post, I'll describe how to take our custom dialog and integrate it as a plugin in WYSIWYG, but until then, you can always browse the WYSIWYG module code yourself and see what you can figure out. There are also some good resources listed on the WYSIWYG Project page.

Here's the finished ckeditor_custom module:

Mar 31 2012
Mar 31

Views Or allows you to define filter blocks with multiple alternatives in drupal views.

for the video inclined here’s the two minute walk through – http://www.youtube.com/watch?v=pWE78xQK3Z0

A bit sleepy this morning and forgot coffee.  It shows, I know.

Use case: we have some old data – some of it comes in with the number 0 meaning blank, some of the rows contain the word NULL and some of the rows are empty. All three variations mean the same thing. We want to create a single view with all three options open – if it’s the number 0 OR the word NULL or simply empty.

Views Or adds to your filters options

Views Or adds to your filters options

Installation and usage: Install as usual – obviously you’ll need views installed…. To use you will need to add all three of the new fields into your filters block. First will be the “Begin alternatives” then the “Next alternative between your filters, (repeated for as many alternatives as you need) and lastly you’ll need to “end alternatives.”

Views or fields in the filters area

Views or fields in the filters area


All the pretty fields need to be reorganized

All the pretty fields need to be reorganized

As a lazy lad I like to just add them all at once and then move the options around afterwards, do whatever your gods tell you to…

The final views or arrangement

The final views or arrangement

Programmatically what this has achieved is

AND ((UPPER(node_data_field_er_acq_uid.field_er_acq_uid_value) = UPPER(‘NULL’)) OR (node_data_field_er_acq_uid.field_er_acq_uid_value IS NULL))

In any case it’s a pretty simple module – installs well and saved us from cruddy data. Merci a bunch to all

Mar 28 2012
Mar 28

If you use CKEditor as your WYSIWYG editor, you've probably, at some point, had to figure out which module to use to integrate it into your Drupal site. CKEditor or WYSIWYG? Without going into too much detail, there are arguments for (and against) both modules. CKEditor offers more granular control over some of the editor configuration settings on the module settings page, while WYSIWYG offers integration of multiple different client-side editors and the ability to assign different ones to different input formats. Ultimately it comes down to what you need, and personal preference. I used to favor the CKEditor module because it was easier to configure, but on a recent project I decided to give WYSIWYG another try. It was a little challenging to get it set up the same way as I usually do with CKEditor, but I was able to get it to do what I wanted.

Here's what I wanted to do:

  • Make sure my editor uses the same css that my site uses so that what I see is actually what I get
  • Define my own "styles" for the "Font Styles" drop down in the editor toolbar
  • Override the default grouping of buttons in the toolbar


This tutorial is for Drupal 7 or 6. You'll need to know a bit about javascript and php to follow along. There is also a bit of custom module code involved. I've attached the custom module we'll be using at the end of the post.

NOTE: The version of CKEditor I was using for the example was 3.6.2.7275. Sometimes CKEditor changes things in it's api. For example, I tried using the D6 module on a site that was running an older version of CKEditor (3.2.x.xxxx) and it didn't work. Just keep that in mind if you did everything correctly and it's still not working.

Getting started

So firstly, you'll need to download the WYSIWYG module and install it. After successfully installing the module, go to the configuration page at admin/config/content/wysiwyg. There is a list of supported editors along with some basic instructions of where to put the editor's code once you've downloaded it. If you un-collapse the "Installation Instructions" fieldset and scroll to the CKEditor entry, there's a nifty link that will take you to the CKEditor download page. Download the most recent version of CKEditor and unpack it to sites/all/libraries. If you did everything right, the CKEditor installation instruction should look like this:

At the top of the wysiwyg configuration page, you can now assign CKEditor to your input formats. Don't forget that any html produced by the editor is still subject to any input filter rules you have set up. So, even if you have all the buttons enabled on the editor toolbar, but you're limiting the allowed HTML tags in your input filter settings, your text may not look like what you expect when it's rendered onto the page. To make things easy, I'm going to do this tutorial using the Full HTML input filter.

CSS Setup

All we're trying to do here is make sure the editor text area is using the same css as my content area that the text is going to be rendered into. If you're not using an administration theme, then you probably don't need to do anything because the default is to use the theme css in the editor. If you are using an administration theme, then you need to change this setting. From the WYSIWYG configuration page, click on the "edit" link on the "Full HTML" input format.

Expand the CSS fieldset and change the Editor CSS to "Define CSS".

Now, in the CSS Path textfield we just need to provide a comma separated list of css files for the editor to use. Don't be fooled by the "%t" token! The theme that it's referring to is the current theme, which is the administration theme. In my example, I'm using Bartik so I'm going to supply the following css files:

%bthemes/bartik/css/style.css,%bthemes/bartik/css/layout.css,%bthemes/bartik/css/colors.css

Save the settings and check out your editor. The easiest way to check if it worked is to just type some text and see if it looks like it's supposed to, but to be double-sure, use firebug or Chrome's built-in inspector to inspect the editor text area. Find the editor iframe element and expand the <head> tag. You should see your css listed there.

Now depending on how you've css'd the text on your site, you may need to consider that everything in the editor's text area is wrapped only in a <body> tag. So if you have any special text formatting that's more specific than that (e.g. #content-area ul li {font-weight:bold;}), you'll need to add some css for the formatting to be reflected in the editor. The tricky part is targeting just the <body> tag in the editor and not on the whole site. CKEditor outputs the <body> tag with certain classes depending on how it's configured (ex: cke_show_borders), but you might not always be able to rely on that. One way around this is to have a special css file that gets added only to CKEditor through the settings page (like we did above). Another way would be to add a custom class to the <body> tag itself. This is possible with the WYSIWYG module API, but is a little more complicated. More on that later.

Define Styles

I quite like the "Font Styles" tool for formatting text. It's a good way to define specific formatting sets so that my content looks consistent with other text on the site. More importantly, though, it's a good way to limit the amount of formatting a client has access to so that they don't end up making a beautiful site look awful by changing the font to Times New Roman, 16pt, bold, and bright red with a yellow background (you know what I'm talking about). The problem is that the default styles that come with CKEditor are just... hideous. Seriously? Blue Title and Red Title are the top options? That needs to change.

Now, you're supposed to be able use the "CSS classes" text field on the settings page to specify your own styles, but it doesn't really work right now. It's being dealt with in the issue queue, though, and likely will be fixed very soon. The current workaround is still good to know, however, as it teaches us how to override any CKEditor config setting (even the ones not available through the WYSIWYG configuration page, of which there are many), through the use of hook_wysiwyg_editor_settings_alter().

If you haven't done any module development before, don't be afraid, this is really easy. If you are a module dev first timer, a quick note about hooks: The hook system in Drupal gives module developers a way for other developers to interact/intervene with something their module is doing. (The hook system roughly mimics an Aspect Oriented Programming Paradigm, if you're into that kind of stuff). In this case, we will be using the hook called "hook_wysiwyg_editor_settings_alter" to alter the CKEditor settings before they're actually output. You can read more about hooks here.

I'm going to create a new module called "ckeditor_custom" (if you are working on a site that you already have a custom module on for hooks, by all means use that instead). I'll start by creating a folder of the same name in sites/all/modules/custom, and adding two files: ckeditor_custom.info and ckeditor_custom.module.

The info file will just have the minimum info to get started:

name = CKEditor Custom
description = Custom configuration for CKEditor
core = 7.x
;core = 6.x if you're on drupal 6

And then, in the module file, we'll add our hook -- I've added some comments to explain what's going on (don't forget to put "<?php" at the top of the file):

/**
 * @file hillside_custom.module
 *
 * Includes minor tweaks to hillside that can't be performed through config.
 * This should mostly be just drupal hooks etc.
 */
 
/**
 * Implements hook_wysiwyg_editor_settings_alter()
 */
function ckeditor_custom_wysiwyg_editor_settings_alter(&amp;$settings, $context) {
 
  // The $context variable contains information about the wysiwyg profile we're using
  // In this case we just need to check that the editor being used is ckeditor
 
  if ($context['profile']-&gt;editor == 'ckeditor') {
 
    // The $settings variable contains all the config options ckeditor uses. 
    // The array keys correspond directly with any setting that can be applied 
    // to CKEditor - as outlined in the CKEditor docs: 
    // http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.config.html 
    // Another way to override configuration is to use your own configuration javascript
    // file. In this case, we're going to add our own configuration file that will
    // Hold our stylesSet customizations... 
    $settings['customConfig'] = base_path() . drupal_get_path('module', 'ckeditor_custom') .
                                '/ckeditor_custom_config.js';
 
    // We are also going to specify a custom body id and class
    $settings['bodyId'] = 'ckeditor_custom_id';
    $settings['bodyClass'] = 'ckeditor_custom_class';
 
    // To see what's in $settings and $context, install the devel module 
    // And run the variables through the dpm() function. 
  }
}

NOTE: I just want to point out that (in addition to changing config options in the customConfig file) the $settings array can have any setting that ckeditor describes in http://docs.cksource.com/ckeditor_api/symbols/CKEDITOR.config.html. This includes "bodyClass" and "bodyId". By adding those settings to the array, you can set custom body classes or an id. I know I commented on this fact in the code, but it's a useful and important feature of this hook.

Now, we need to add the ckeditor_custom_config.js file that we just referred to. Create the file at the root level of your module folder and add this:

/*
  Custom configuration for ckeditor.
 
  Configuration options can be set here. Any settings described here will be
  overridden by settings defined in the $settings variable of the hook. To
  override those settings, do it directly in the hook itself to $settings.
*/
CKEDITOR.editorConfig = function( config )
{
  // config.styleSet is an array of objects that define each style available
  // in the font styles tool in the ckeditor toolbar
  config.stylesSet =
  [
        /* Block Styles */
 
        // Each style is an object whose properties define how it is displayed
        // in the dropdown, as well as what it outputs as html into the editor
        // text area.
        { name : 'Paragraph'   , element : 'p' },
        { name : 'Heading 2'   , element : 'h2' },
        { name : 'Heading 3'   , element : 'h3' },
        { name : 'Heading 4'   , element : 'h4' },
        { name : 'Float Right', element : 'div', attributes : { 'style' : 'float:right;' } },
        { name : 'Float Left', element : 'div', attributes : { 'style' : 'float:left;' } },
        { name : 'Preformatted Text', element : 'pre' },
  ];
 
}

Now enable the module, make sure the "Font Style" button is enabled on the ckeditor WYSIWYG profile settings page, clear your browser cache if necessary, and go check out your new styles.

<< Nice! Done.

It's worth noting that you can actually skip this whole module entirely by just adding the javascript directly into the config.js file included in the ckeditor library at sites/all/libraries/ckeditor/config.js. I wouldn't recommend this though because you'll likely lose the customization when you upgrade ckeditor at some point. Also it's bad practice to edit files in third party libraries/contrib modules etc.
 

Overriding the Button Groupings

This last thing I want to do is a little tricky and requires a bit of a hack. What I want to do is make the button groupings different so that my editor toolbar doesn't look like this:

There isn't a way to do this through the ckeditor_custom_config.js file so we have to modify the $settings variable in our hook. The $settings array contains an array keyed as "toolbar". By default all the buttons are lumped in an array under $settings['toolbar'][0]. When the butons are rendered into the toolbar, the default ckeditor grouping is used. We can modify the grouping by regrouping the buttons into multiple arrays in $settings['toolbar'].

/**
 * Implements hook_wysiwyg_editor_settings_alter()
 */
function ckeditor_custom_wysiwyg_editor_settings_alter(&amp;$settings, $context) {
 
  // The $context variable contains information about the wysiwyg profile we're using
  // In this case we just need to check that the editor being used is ckeditor
  if ($context['profile']-&gt;editor == 'ckeditor') {
 
    // Specify the custom config file that defines our font styles
    $settings['customConfig'] = base_path() . drupal_get_path('module', 'ckeditor_custom') .
                                '/ckeditor_custom_config.js';
 
    // We are also going to specify a custom body id and class
      $settings['bodyId'] = 'ckeditor_custom_id';
      $settings['bodyClass'] = 'ckeditor_custom_class';
 
    // Make sure the toolbar is there
    if (!empty($settings['toolbar'])) {
 
      // These are our desired groupings. Buttons that aren't listed here will be
      // Grouped in one big group at the end
      $preferred_groupings[] = array('Source');
      $preferred_groupings[] = array('Bold', 'Italic', 'Underline', 'Strike');
      $preferred_groupings[] = array('JustifyLeft', 'JustifyCenter',
                                     'JustifyRight', 'JustifyBlock');
      $preferred_groupings[] = array('BulletedList', 'NumberedList', 'Outdent', 'Indent');
      $preferred_groupings[] = array('Undo', 'Redo');
      $preferred_groupings[] = array('Image', 'Link', 'Unlink', 'Anchor', '-');
      $preferred_groupings[] = array('TextColor', 'BGColor');
      $preferred_groupings[] = array('Superscript', 'Subscript', 'Blockquote');
      $preferred_groupings[] = array('HorizontalRule', 'break');
      $preferred_groupings[] = array('Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord');
      $preferred_groupings[] = array('ShowBlocks', 'RemoveFormat', 'SpecialChar', '/');
      $preferred_groupings[] = array('Format', 'Font', 'FontSize', 'Styles', 'Table');
      $preferred_groupings[] = array('SelectAll', 'Find', 'Replace');
      $preferred_groupings[] = array('Flash', 'Smiley');
      $preferred_groupings[] = array('CreateDiv', 'Maximize', 'SpellChecker', 'Scayt');
 
      // An array to hold our newly grouped buttons
      $new_grouped_toolbar = array();
 
      // Compare each desired groupings to the configured buttons in the toolbar
      // and add them if they are there
      foreach($preferred_groupings as $button_group){
 
        // array_intersect() compares two arrays and returns an array of values
        // That are present in both arrays.
        $matching_buttons = array_intersect($button_group, $settings['toolbar'][0]);
 
        if (!empty($matching_buttons)) {
          // If there are matching buttons, we add it as an array to our
          // $new_grouped_toolbar. We run $matching_buttons through array_values()
          // to reset the array keys back to starting from 0.
          $new_grouped_toolbar[] = array_values($matching_buttons);
        }
 
      }
 
      // For extra safety, we're going to find any remaining buttons that we
      // missed. To do this we need to flatten our grouped buttons and compare
      // that against the original toolbar to see if we missed anything
      $new_flattened_toolbar = array();
 
      // Flatten our grouped buttons
      foreach ($new_grouped_toolbar as $key =&gt; $group) {
        $new_flattened_toolbar = array_merge($new_flattened_toolbar, $group);
      }
 
      // Array diff returns the keys that are present in the first argument, but
      // not not present in the second
      $remaining_buttons = array_diff($settings['toolbar'][0], $new_flattened_toolbar);
 
      if (!empty($remaining_buttons)) {
        // reset the array keys and add it to the $new_grouped_toolbar
        $new_grouped_toolbar[] = array_values($remaining_buttons);
      }
 
      // Replace the toolbar with our new, grouped toolbar.
      $settings['toolbar'] = $new_grouped_toolbar;
 
    }
  }
}

And that's it. Reload the editor and it should look something like this (depending on which buttons you have enabled):

It seems like a lot of code just to group your buttons, but it works. Eventually, I'm sure the WYSIWYG module will have some sort of GUI to order and group the buttons, but until then, this will have to do.

Conclusion

So that's it. We've managed to get our CKEditor to look the way we want, using the WYSIWYG module, without having to hack any module code or libraries. Great. Of course all this stuff is easier using the CKEditor module and doesn't require a custom module, but you never know when you're going to be stuck with the WYSIWYG module on a project and need this.

In case you just wanna quickly get going with this, I've attached the ckeditor_custom modules (D7 and D6) here:

Feb 16 2012
Feb 16

A common design element we see lately in a lot of sites is a banner rotator - image slideshow with some text and links on the side.

There are several modules that try to deal with it in Drupal (such as "ddblock", which we don't use), however we find that using the good old Views slideshow module can give us everything we need. The following blog post gives an overview of how we achieved this task without any code, just Views configuration and a bit of CSS. As always we provide a feature that can be installed and enabled (visit /banner-rotator).

Download Views slideshow module, and follow the instructions in the README.txt on how to enable "Views slideshow cycle".
Next, create a content type called “Banner rotator” with 4 fields:

  • Title - The tile of the node
  • Body - Additional text to the node
  • Image - The image that will be displayed
  • Url - A filed that contain a url to any page. It could be any custom address we want to direct the user

Next add a View, as in the image.

I wanted that only the picture will be “rotated” therefore I exposed every field except for the image field.
After setting the above click on the settings link in the “FORMAT” area. Scroll down to the widgets area, under the top widgets click on the pager and follow these options

I chose to show the fields: Title and body and I selected to "Activate Slide and Pause on Pager Hover", this will pause the cycle of the images when hovering on the title and body. Also, under the Cycle options -> Action I chose to "Pause on hover", it will pause the cycle when I'm on a picture.

As we added a URL field, we can use its data to wrap other fields with the link, e.g. in the title field go to "REWRITE RESULTS" and select "Output this field as a link", and use the token of the URL field in the link path input.

Last thing, you need to do, is apply your CSS. A simple float will the text where you expect it to be.

.views-slideshow-controls-top {
  float: right; /* LTR */
}

Be creative!

Jan 05 2012
Jan 05

Posted Jan 5, 2012 // 11 comments

Overview

Happy Holidays!

In our last installment (A New Paradigm for Overriding Drupal Features) I presented a new sandbox module that allowed you to override Field Settings from existing Features. As promised, I have now generalized that approach and have created a new sandbox module called Features Override 2 that applies this method to any feature.

The Problem (revisited)

As a quick review, the problem we are trying to solve is dealing with site-specific changes to existing Features. The Drupal Features module allows you to capture configuration data, such as entities, fields, variables, views, permissions, etc into code modules.  These code modules can then be placed into version control, enabled and disabled, or used on a separate Drupal installation to provide the same feature.

In many cases, a Feature is developed for a generic site (or Distribution, such as OpenPublish) but needs to be slightly changed for the specific site you are developing. If you change the base distribution feature, it will be hard to upgrade to a newer version of that distribution feature. What you really want to do is just capture your changes to a site-specific "Override" feature and leave the original distribution untouched.

The New Solution

As we previously discussed, the existing Features Override module attempted to solve this problem but had many drawbacks. I have now taken the approach used to override Field Settings in the previous blog entry and applied that to all features types. As a new maintainer of the Features Override module, I intend this new sandbox to eventually become the 7-2.x release of Features Override.

Currently, the new Features Override 2 sandbox module requires a patch to the Features module: #1317054: Provide a universal hook for altering default components (although by the time you read this it may already be committed to the Features 1.x dev branch). The README.txt file included with the module documents the basic usage, but I'll review the highlights here.

Basic Usage

(Install and enable sandbox module. Apply patch to Features as needed)

Unlike the old Features Override module, overrides are handled just like any normal feature in the new sandbox. First install (or create) some "base" features for your site. For example, "base_article" containing the fields, content-type, or whatever you want. Next, make some changes to that component using the normal Drupal UI (for example, change the display settings of a field, or change the name of a view).

Once you have made changes, the Features page will show your base feature as being "overridden". To capture these changes into a new feature, just click Create Feature and select the new "Feature Override" exportable from the Components drop-down list. A list of overridden components will be shown. Check the box next to the ones you want to save, then Download Feature to create the feature. Install this new Override feature module on your site and enable it. Now the original feature will be shown in it's "Default" state, but the changes you made will be applied by the new Override feature.

Examining Overrides

Even if you never use this module to create any overrides it adds a very useful new tab for inspecting changes. In the past, the "Review Overrides" tab could be used to look at the "diff" between the live database and the stored feature code. But it couldn't really show the full context of the change. For example, if the change was to a display in a view, it was difficult to determine what view was being changed.

The new "Overrides" tab added by the sandbox module shows the line-level detail of the current changes.

Override_screen1

When you select a specific Override feature and click the new Overrides tab, a line-by-line list of the changes being overridden are displayed, along with a list of any new changes (not yet saved to a feature). Using the checkboxes you can completely control the line-level detail of which changes are saved to the Override feature and which are not.

Merging Features and Overrides

But wait, there's more! As detailed in the README file, you can also easily capture changes to an Override module, then apply those changes to another feature to "merge" the two features together. You can use this to make changes to the base feature without disturbing any override features. Or you can use this to update existing override features. The module provides a lot of flexibility for handling overridden features in a simple and intuitive way.

The Magic

The magic behind the scenes that allows feature overrides to finally work is a new hook that allows external modules to modify the code being generated by Features exportables. The Features Override 2 sandbox uses this new hook to alter the code being written, adding it's own "alter hook" to the exportable code. This alter hook is used to apply the changes from the override feature to the base feature. In the past, no such hook was available to modify the Features exportable code at this level. The old Features Override module tried to work around this limitation, but it just wasn't possible. Some features (such as Views) require careful handling of the changes so they are made to the correct part of the exported array structure. For example, changes to "current display" are ignored in favor of changes to the "displays[display-name]" array.

The core "diff engine" used in the Features Override module was maintained (slightly modified) to determine the set of additions and deletions needed for the override. But these changes are now selected "on the fly" in the Create Feature screen rather than needed to write them to an intermediate database table and module file. The new sandbox doesn't use any additional database tables or files.

As was the case in the previous blog article, the key was applying the alter hooks to the actual exportable code and then treating overrides as any other normal feature. It's a simple concept that gets a bit tricky in the implementation. And the nested level of hooks and alters can be confusing the first time you look at it. But in the end the goal of providing easy-to-use overrides of existing features was met.

Conclusion

I encourage you to grab the sandbox module and Features patch and start playing with this. Post any problems in the sandbox issue queue. Once a few other people have tested this and we get any major issues resolved, then I'll go ahead and rename this to Features Override and post it as the 7-2.x release.

Thanks to the Drupal community members: nedjo, tim.plunkett, hefox, febbraro, e2thex, and jec006 for their help and support with this work.

Attachments

Override_screen1-1.jpg

Mike Potter is a Senior Developer at Phase2 who loves to pair a solid technical solution with an intuitive client-focused design. Mike started his career as an experimental neutrino particle physicist before creating the first WWW home page ...

Nov 17 2011
Nov 17

Creating a simple Drupal 6 blog using CCK and Views - Pt2.

Posted on: Thursday, November 17th 2011 by Erico Nascimento

Sometime ago I posted a blog: "Creating a simple Drupal 6 blog using CCK and Views - Pt1." where I explained the basics of creating a content type and a view page that displays a listing of that content type. Today I'm going to complete the process by adding tags and/or categories to the content type and creating an RSS feed. This post is targeted at beginners so, if you're already familiar with Drupal you probably won't see anything new here,

Now that you have your content type and view setup, next thing you'll want to do is to add taxonomy terms (aka tags/categories) to it. Here is what were going to do:

  1. Go to: YourSiteAddress/admin/content/taxonomy and click add Vocabulary.
  2. Name the vocabulary accordingly and select which content type(s) it applies to
  3. Select if you'll be using free-tagging and if the user can select multiple terms and save it

Simple! Now if you edit one of your nodes or create a new one you'll have the option to add tags to it.

For the next step, you'll probably want to add a tag cloud, which is very easy with the Tagadelic module.

  1. Download and enable the Tagadelic module
  2. Visit the blocks page and place the "Tags in Tags" block on your blog pages.

For the final step we'll create an RSS feed for our content type.

  1. Go to the blog view and add a feed display
  2. Select Style:RSS Feed - Fields
  3. Select a Path to the feed
  4. Add whatever fields you want to be displayed on your feed.

All you have to do now is add the link to your RSS feed on any desired locations on your site and we're done! Note that I'm using a blog as an example but this process can be used to essentially any content type.

Happy theming!
Érico Vinicius

Oct 05 2011
Oct 05

A new command in Drush 5 is the runserver command (alias "rs"), which is a built in web server for development (if you have used Django or Rails you will be familiar with this concept). All you need is the php-cgi binary and you can bring up a server (and logged in web-browser, if you like!) for a specified site in seconds - no need to have Apache configured for the site, or even running. Click full-screen and HD for easier reading, or view on YouTube.

Please share any feedback or questions in the comments.

Oct 04 2011
Oct 04

A new command in Drush 5 is the core-quick-drupal command (alias "qd") - the purpose of this command is to get you running in Drupal with zero setup. All you need is php-cgi and the PDO sqlite driver - you don't need MySQL or Apache running at all. Running the command will download and install Drupal (or your choice of distribution) as well as any contrib projects you like, start the built in php webserver and open your default browser with you already logged in as user 1. Fun stuff.

Click full-screen and HD for easier reading, or view on YouTube. Also, you may want to check out the runserver screencast for more detail on using the built in webserver.

Please share any feedback or questions in the comments.

Oct 03 2011
Oct 03

This screencast covers the Drush "help" command (a command important enough to warrant it's own screencast!) and some handy tricks to use it well, as well as the "topic" command. If you are new to Drush or an experienced user looking to more easily discover (or rediscover!) commands or options, this is a great place to start. Click full-screen and HD for easier reading, or view on YouTube.

Please share any feedback or questions in the comments.

Sep 16 2011
Sep 16

Recently, I designed, themed and developed a new site for my photography, High Rock Photo. The obvious choice for this new site was to use Drupal 7. I wanted the ability to easily create node galleries and this screencast shows you how to create and theme a node photo gallery using Drupal 7. I will also point out what modules are needed and make reference to those that would have been used in Drupal 6 and are now integrated into core in Drupal 7. You will also find some custom template code published that I mention in the video for your theming.

field--field_gallery_photo.tpl.php » /sites/all/themes/[your_theme]/templates/

  1. <?php

  2. /*

  3. template - drupal.org/node/1224106#comment-4969404

  4. Photo title - drupal.org/node/432846#comment-4125056

  5. used at: www.highrockphoto.com

  6. /*

  7. /* change the column count to the number of photos you want to appear going across. (Adjust thumbnail size as needed) */

  8. $columns = 2;

  9.   $rows = array_chunk($items, $columns);

  10. ?>

  11. <table class="mini-gallery">

  12.   <tbody>

  13.     <?php foreach ($rows as $row_number => $columns): ?>

  14.       <?php

  15.         $row_class = 'row-' . ($row_number + 1);

  16.         if ($row_number == 0) {

  17.           $row_class .= ' row-first';

  18.         }

  19.         if (count($rows) == ($row_number + 1)) {

  20.           $row_class .= ' row-last';

  21.         }

  22.       ?>

  23.       <tr class="<?php print $row_class; ?>">

  24.         <?php foreach ($columns as $column_number => $item): ?>

  25.           <td class="<?php print 'col-'. ($column_number + 1); ?>">

  26.       <div class="photo-image"><?php print render($item); ?></div>

  27.       <div class="photo-title"><?php print($item['#item']['title']); ?>

  28.       <div>

  29.           </td>

  30.         <?php endforeach; ?>

  31.       </tr>

  32.     <?php endforeach; ?>

  33.   </tbody>

  34. </table>

Aug 09 2011
Aug 09

My first ever Fuse blog post will focus on the Context module developed by the DC based Development Seed. With 29577 reported installs of the module, Context is quickly climbing the module ranks. It's already part of our base install for all sites we work on here at Fuse. 

Simply put, Context lets you determine specific reactions on a set of conditions. On every page load, it checks to see if any active contexts have conditions that have been fulfilled, and if so, it performs the reaction. To show you how it works I will give you an example of what can be achieved with Context.  In this example we want to create an active menu trail for content tagged with a specific taxonomy term. That taxonomy term will be your condition and the reaction is the desired active menu trail.  Here are the steps to take to make this work: 

1. Install Context:

As of today, the latest version of context is 7.x-3.x, which is not that different from the version 6.x-3.0. I will be working with Drupal 7 version since we're using D7 for all our new builds at Fuse. Install Context the usual way just don't forget CTools, as it is a dependent module. In Drupal 6.x environment you will also need the jQuery UI module which provides you with an admin interface for some extra features. (D7 has the jQuery included within core)

2. Add a new context:

Under Structure > Context you’ll get a list of all the contexts you've created and a search bar. You should be looking at an empty list after installing the module.

On top you can +Add or +import. Lets add a new context for now (we’ll get to importing a bit later.) Adding a new context will prompt you for Name, Tag a description. The "Tag" field will be used to group contexts on the context listing page.

3. Set your conditions:

This is where you will set the various conditions for your context. As mentioned above, conditions are checked on page load, and if the condition is met, the configured reactions are performed. Context comes built in with quite a few default conditions that will probably, for the most part, fulfill your needs. However Context is fully extendible and there are already modules out there that provide new and exciting conditions and reactions. This extendibility is discussed further at the end of this post. For now, we'll just go over the default conditions:

Context: The condition is met, if another context's conditions are met. Perfect for recycling your already set context, if there are currently active contexts that you would like to base your new context on, the context option would be perfect for it. I hardly ever duplicate the exact same condition set between two or more contexts, but there is the odd time when I like to use a context I have already set and fine tune it (ie. create another condition on top of it).
Menu: Allows you to select any number of menu items. The condition is met when any of the selected menu items belong to the current active menu trail.

Node Type: Select from a list of node types. The condition is met when viewing a node page (or using the add/edit form -- optional) of one of the selected content types. 

Taxonomy: Your condition is met if the current node being viewed is referring to a particular taxonomy term. Don't confuse this condition withTaxonomy term.

Path: Allows you to supply a list of paths. The condition is met when any of one or more paths match the supplied paths.

Site-wide Context: The condition is met at all times.

Taxonomy term: Will set the context when viewing the taxonomy term's page (not a node that is referring to that taxonomy term).

User Role: The condition is met if the current user has one of the selected role(s).

User Page: Lets you choose from a list of 'User' pages. (i.e. User profile, User account form, Registration form). Condition is met when viewing the selected pages.

Views: This option will list all active views and their specific generated pages. This allows you to trigger your context for any pages that a particular view is active on.

4. Set your reaction:

Once your conditions are set, it's time to set up your reactions. Once again, we'll just go over a few of the reactions that comes with Context built-in:

Blocks: The blocks reaction is probably my most used reaction of all. It allows you to place any block in any region when the condition is met. This provides a much more flexible way to add blocks to the page than the blocks administration page (admin/structure/block) since you can use more than just the path as the criteria for when a block should be visible or not.
 

Note: 

There is one tricky thing when using Context to place your blocks and that is the ordering of the blocks within a particular region. Within a context, it's easy to reorder the blocks within a region using the standard drupal drag and drop interface. However, If you have two

different

contexts adding blocks to the same region you will need to order them manually. Under the "+add" in the region header, click the 

 icon and the weight field will appear. Here you can assign a specific weight number to your block. The weight will be respected accross all contexts so you just need to make sure the blocks you want to appear first have lower weights than ones you want to appear after.


By drag and drop sort method vs. weight select sort method:

 

Breadcrumb: Set the breadcrumb trail to a particular menu item.

Menu: Set the Menu Active class

Theme Page: Override the section title and the section subtitle of the page. This will also override your $section_title and $section_subtitle variables within your page.tpl.php.

Theme html: Add an additional html body class to the page

5. Import / Export:

You can easily export an Context by clicking on "Export" (under Operations) on the Context listing page.

The result will be a block of text that can be copied and then imported back to another site. Just select "+Import" from the top (next to the "+Add" button) and paste the exported text. Hit save and you will have an exact copy of the context.

6. Context Editor:
 

Having the Admin menu module installed, there is the handy context editor window for testing and editing contexts. Active contexts are easily detected and can be modified on the fly by adding conditions, blocks (drag and drop) and theme variables.

7. Book keeping:
 

Usually on substantial projects the Context overview list gets messy and a bit confusing. When there are a lot of contexts it can be hard to find the one that is outputting a certain block on a certain page. To avoid this confusion I recommend a few things:
 

  • Write Descriptive Descriptions! It sounds redundant, but the better your description is, the easier it will be to figure out which context is outputting "that block" in "that region" on "that page".
  • Use Tags Wisely! Tagging can be very useful since the contexts on the context listing page get grouped by tag. If you group your contexts intuitively using tags, you'll spend less time finding your contexts and more time trying to figure out if we're ever going to get multigroups back.
     

8. Extending Context using the API

As mentioned above, Context comes with an API to extend it's functionality by adding custom conditions and reactions. An example of one of these modules is Background Images (built by Fuse's own Chris Eastwood). It provides a new reaction that can change the background image of any css-selectable element on the page. While this tutorial will not delve into how to use the API to extend context (perhaps in another tutorial down the road?), I thought it was worth mentioning in case you need a condition or reaction that isn't built-in. You know how it often goes with Drupal, if it's not built-in, there may just be a module for that! 

Jul 15 2011
Jul 15

When I first started with Drupal, the exercise that helped me the most to understand how Drupal works was to create a simple blog. Now, back then, there were a lot of tutorials on how to do it using the blog module (which is great) but this is a learning experience so I’ll show you how I did it using CCK and Views.

First of all we’re going to download and enable a couple of modules we’ll need to get this done (If you’re somewhat familiar with Drupal I recommend using Drush for this).

read more

May 11 2011
May 11

Why should you use panels node-preview-example module? You shouldn't, it's just an example ;)

This post covers how to build a CTools content-type plugin that has context. CTools "Context" should not be mistaken with the context module. Context is CTools way of saying, my plugin (read, block) shouldn't be dumb. If it requires a node or a user to extract data from, then it should let others know about it. "Others" in our case is Page manager module. It will load the context for the plugins and hand it over. In fact, if the context is not present it will not even bother loading the plugin.

I have created an example module called node-preview-example, that you can simply download and enable, create an Article page, and see how Panels is now showing the body field on the left and on the right a (silly) "Summary" of the node -- this is our plugin.

drush dl ctools panels features
drush en ctools page_manager panels features

And the example module is in my sandbox:

Let's go over the important parts in the code.

<?php
/**
* Plugin definition.
*/
$plugin = array(
  ...
'required context' => array(
    new
ctools_context_required(t('User'), 'user'),
    new
ctools_context_required(t('Node'), 'node'),
  ),
  ...
);
?>

The plugin definition is where we let the system know we need a user object and a node object to operate on.

Next the render callback can use those context, after making sure they are valid.

<?php
/**
* Render callback.
*/
function node_preview_example_summary_content_type_render($subtype, $conf, $args, $context) {
 
// Seperating the context to two different variables
 
list($user_context, $node_context) = $context; // Make sure that context variable arent empty.
 
if (empty($user_context) || empty($user_context->data)) {
    return;
  }

  if (empty(

$node_context) || empty($node_context->data)) {
    return;
  }
// Above we made sure we got the context.
 
$node = $node_context->data;

  ...
}

?>

As you can see, it didn't take much to get the context working, but it gives a lot of power:

  • Plugins without satisfied context will not appear in the plugins list, so a user can't add them by mistake
  • There is one single way for plugins to get a context, unlike "dumb" blocks that each one needs to find out where they are (e.g. menu_get_item() over and over again).

If you listen to talks about Butler module, and having a plugin and context system in D8 - it will probably very similar or at least learn a lot from CTools plugins and context system, so you know you are on the right path.

Dec 03 2010
Dec 03

Although CCK automatically does some basic validation on your fields that you add to your Drupal content types, there are some cases where you'd like to do some additional validation for your site. One use case that I ran into recently was a basic text field that was being used to house hyperlinks for one of my websites. The text field had already been in place and working perfectly for months. Rather than do something drastic like replacing the field altogether with a field provided by the "Link" module, I decided to do a hook_form_alter to add in my own custom validation function.

Basically you just create a custom module for your site called "form_alterations" or whatever you like. Here's the code for adding in this sort of functionality:

<?php
/**
* Implementation of hook_form_alter().
*/
function form_alterations_form_alter(&$form, &$form_state, $form_id) {
  switch (
$form_id) {
    case
'announcement_node_form':
     
// Simply add an additional link validate handler.
     
$first = array_shift($form['#validate']);
     
array_unshift($form['#validate'], $first, 'form_alterations_link_validate');
      break;
  }
}
/**
* FAPI #validate handler. Make sure the hyperlink they used is correctly formatted.
*/
function form_alterations_link_validate($form, &$form_state) {
  if (!empty(
$form_state['values']['field_web_address'][0]['value']) && !strstr($form_state['values']['field_web_address'][0]['value'], 'http://')) {
   
form_set_error('field_web_address', t('Please enter a full web address starting with "http://".'));
  }
}
?>

hook_form_alter allows you to use the Drupal Form API to make changes to existing forms. In the code above, I'm adding in a call to my custom function called "form_alterations_link_validate" after the basic CCK validation takes place. Then, within the function itself, I check to make sure that the user entered a value and that it contains "http://" at the beginning of what was entered. If you use the code above, please just be sure to change the line with "case 'announcement_node_form':" so that it modifies the form for your node type. My node type was called "announcement".

Nov 05 2010
Nov 05

This is Part 2 of a 2-part series examining two different ways to arbitrarily ordering a View. Part 1 can be found here.

In part 2 of our arbitrary views ordering experiment, we're going to take a look at the Module named Nodequeue. Nodequeue is a great way to put your nodes in any order you want, then reference that ordering in a view as a sort criteria. Afterwards we'll compare the two methods, and examine some of the pros and cons of each.

Once again, this tutorial is written for Drupal 6, but from the recent testing I've done with the dev versions in Drupal 7, I don't imagine they will change all that much.

Step 1 - Modules, Content

We'll use the same D6 install from part 1 with Views and Nodequeue installed.

We'll also use the same content. (User profiles for all the employees here at Fuse)

Step 2 - Configuring the Queue

The next step is to setup our nodequeue by going to admin/content/nodequeue/list and clicking 'add_nodequeue'

We'll give it a name and just for fun, let's give it a limited queue size of 7 (the number of people that work here at Fuse)


The 'Reverse in admin view' checkbox is really just a visual setting. Adding items to a queue is done using a autocomplete text field, followed by clicking 'add item'. When you're adding items to a fixed size queue (like ours) items get popped off as you add new ones once you've passed the limit. This option basically lets you choose whether the items you add get inserted at the bottom of the list (and the ones on the top get popped off - this is the default), or vice versa. You can try it out both ways to see what you prefer, but I find I like it the way it is so I'll leave it unchecked.


The next two text fields (Link "add to queue" text, Link "remove from queue" text) allow you to specify links that will appear when viewing nodes to add or remove it from the nodequeue. This is a great feature and can be very handy, but in our case is not necessary so we'll leave them blank.

Lastly, we'll choose Employee as the only node type to appear in the queue. This will basically just filter out all other node types when you're adding nodes to the queue.

Click 'Submit' and congrats! You've created your first nodequeue.


Step 3 - Adding nodes to the queue

Strangely, you'll need to click 'View' in order to start adding nodes to the queue. 'Edit' will actually take you back to the settings page we just filled out.

Adding nodes is very straightforward. We just start typing the title of the node and it will autocomplete, filtering out any nodes that don't match the type we selected earlier. I've gone ahead and added every Employee to the list, and ordered it. (Once again, the order is random other than Greg at the top and the Yeti and I at the bottom) It looks like this:


Step 4 - "Viewing" your queue

So we've created our nodequeue... but where can we actually go to see our nodes listed in that order? Well the module actually created a view for us!

If you click 'Edit' on that View, you'll see that there are some default settings and a Page and Block view already there. Try going to the path specified in the Page view and you'll see a list of all your nodes in the order you specified earlier. (NOTE: I had to actually save the view before this worked for me.. not sure if that's a bug...) Pretty simple, right!

As easy as it is, it may turn out that you don't want to use this automatically generated view. Maybe you have another view set up already and you just want to add your Employees nodequeue as a display to that one instead. Well there are only two things you need to do to get it to work.

If you examine the displays that were automatically generated by the module, you'll see that there are a few nodequeue specific items. In Relationships there is a "Nodequeue: Queue" item. Click on it and take a look at it's settings.

By requiring this relationship, we will filter out items that don't belong to the queue. And the "Limit to one or more queues" option allows us to select which queue we will be using for the relationship. In this case we only made one queue so there is only one listed, but if you had multiple ones they would be listed as well and you could mix and match different queues. We'll name the relationship "queue". Remember that name because we'll be referencing it in the the Sort Criteria (queue) Nodequeue: Position

The only thing to note here is that the "queue" relationship needs to be referenced for the sorting to work properly. If you have these two items in your view, you can config the rest of it however you want.

Conclusion

Both DraggableViews and Nodequeue are great ways to create an arbitrary custom order for your views. So which one is better? Well if I were judging only on the setup process, I think Nodequeue would win. DraggableViews is great once you get it working, but there are some *undocumented* little quirks in the setup process that require a bit of trial and error to figure out (unless you follow this tutorial of course). Tthe first time I used Nodequeue, I just installed it and got it working without documentation. It's that simple!

However, when it comes to use-case, I think this is one of those "right tool for the job" kind of situations. Nodequeue has a more manual approach to selecting which nodes you want to display. You have to add each node one by one to the list before you can use it. For me, this is the main difference between the two Modules and the deciding factor when choosing between them. Do I want to think about which nodes belong in the list or not? If I want not only the ordering, but also the node selection to be arbitrary, Nodequeue is the one to go with. But if the node selection can be automated with views, DraggableViews is the winner.

Nov 03 2010
Nov 03

This is a series of screencasts about how to use the ELIMedia system.  While it is currently a closed system for internal use by the e-Learning Institute, I figured I'd post the tutorials here so you could see some possibilities of Drupal.  This is a Drupal 6.x site with a Flash Media Interactive Server on the back-end, heavy use of Views and JWPlayer Module, and a snazzy theme.  There are two helper modules to connect systems together as well as stream-line different asset types.

Currently, there are tutorials for navigating the site and how to embed flash in a course.  The trackback system is also discussed.  Many, MANY more of these videos will be posted tomorrow and over the next few days.  The Flash tutorial also showcases smart-builder (I accidently call it smart-sheet which is a different product) integration.  After that is a lesson on the importance of using image treatments.  It's a 3 part tutorial on how to use image treatments to replace the need for redundant HTML creation and making images more reusable in courses.  There are also tutorials on uploading video, audio, images and adding youtube videos to the system.

If you have any questions about how different things are done please ask!  I hope this gives you some ideas for asset management in Drupal.

Oct 29 2010
Oct 29

It happens every once in a while that you need to order your nodes arbitrarily. That is, sorted neither "ascending" nor "descending". An example of this might be an employee listing page on a company website. It's likely that the employee listing needs to be sorted by order of importance (for lack of a better term) with the bosses at the top, the interns at the bottom, and everyone else in between.

The *simplest* solution is to just add some sort of weight field to your 'employee' content type and then create a View and sort by this weight field. But then as employees come and go, you'll find that opening and editing every node to adjust for the new company hierarchy will get tedious and frustrating. In comes DraggableViews and Nodequeue to save the day!

The DraggableViews and Nodequeue modules both allow you to arbitrarily/manually order your nodes but with a few slight differences in design and interface. In Part 1 of this tutorial we will create a View for a company's employee listing and order them using DraggableViews. In Part 2 (next week) we will examine using the Nodequeue module to achieve the same results, then compare the two modules' strengths and weaknesses.

This tutorial is specifically for Drupal 6 (but I suspect not much will change when they are released for Drupal 7)

Step 1 - The Employee Content Type:

We'll start with a fresh install of D6 with the following modules downloaded and installed:

  • Views
  • DraggableViews
  • Nodequeue (For part 2)


We'll then create the employee content type with the following fields:

  • Name (Node Title)
  • Job Title
  • Image
  • Bio (Node Body)


I've gone ahead and added all the employees here at Fuse Interactive, so let's jump into Views and create the employee listing page.

Step 2 - The employee listing page in Views:

I've started by creating a simple view with the fields we created for the employee content type and added a filter to show only nodes of the type: Employee.

And the output looks like this (I hid the image and the bio to conserve space):

Right now this doesn't make any sense. Why is the President at the bottom of the list and why am I, a lowly Drupal Developer, at the top? This is because it's ordering them by node nid right now. Since I didn't enter each employee in the *correct* order, I will need to use DraggableViews to re-sort them manually.

The concept here with DraggableViews is to create a view that produces an output of nodes or fields, and then create a separate views display that will act as a sorting interface for the view. The view will be manually re-orderable using the same drag 'n drop interface as other drupal admin tables (e.g. taxonomy terms, menu items, 'manage fields' in cck)

Step 3 - Adding the main page display:

First things first, we're going to create a page display for this employee listing  This will be the page that visitors of the site will see. We'll name it "DV_Page"...

...and assign it a path of employees/draggableviews...

Step 4 - Creating the DraggableViews sorting interface

Now we'll create another page display that with the DraggableViews style plugin enabled that we'll use for the sorting interface. We'll give it a name of "DV_Sort_Page"...

...and a path of employees/draggableviews/sort...


The next few steps need to be done in a specific order to avoid getting errors. Your first instinct may be to change the Style plugin to Draggable Table but there are a few things that need to be done before we enable it.

Firstly, we need to add the 'DraggableViews: Order' field. This is the actual "weight" of the row item that gets automatically changed as we drag 'n drop the rows. (Make sure you don't exclude this field from display, it will be needed a little later!)

Next, we need to add the field 'Node: nid' to the fields. This is because of how DraggableViews stores the order in the database. It creates a map of the Node's nid to a "weight" and saves it so that it can be used as a sorting criteria for ordering the rows.

SIDENOTE: Adding the nid only needs to be done in Views 3. In Views 2 the nid is passed into the result set by default, but it Views 3 you need to explicitly add it as a field!

We are also going to make the nid field excluded from display because we don't really care to see that when I'm sorting the rows later.

We also need to make sure these new fields are at the end of the fields list for DraggableViews to work. This is because DraggableViews will be attaching a little drag n drop handle to each row's first field and errors if the first field is hidden (remember, 'Node: nid' is excluded and 'DraggableViews: Order' will be hidden by default).

Also we can remove fields right now that will clutter up our sorting table. Remember, this is not the actual view, but just a table of the views results that can be re-ordered using Drupal's built in drag 'n drop functionality. In this case, for the purposes of sorting, I really only need to see the employee name and job title, so we'll remove the Body and Image fields.

Lastly, we need to change the sorting of the display to use the DraggableViews: Order (Ascending) rather than alphabetically. Our display at this point should look something like this

Now that our display is all set up, we need to change the style plugin from 'Unformatted' to the "Draggable Table" plugin.

And config the 'Order Field' to 'Order' (this is all you need to configure for the style plugin):

Step 5 - Adjust the Original View

The very last thing before you move on to arbitrary ordering glory is to change the sorting criteria of the original page view (The one we named "DV_Page") to match the DraggableViews order (from Node: Title to DraggableViews: Order)

It's important to note that the view 'preview' output at the bottom of the screen does not process the DraggableViews properly. You will need to visit the actual page (employees/draggableviews/sort) to get the proper functionality


Just Drag 'n Drop your new order and click 'Save Changes', then visit your employees/draggableviews page and voila! An Arbitrary order!

Stay tuned for Part 2 where we will explore the Nodequeue module and compare it to DraggableViews.

Sep 22 2010
Lev
Sep 22

A very common request I get lately is for a banner containing a series of image transitions, often on the home page. Certainly a fairly simple feature, one I've already solved in different ways, E.g., here, here, and here. So when I had a similar need, I decided to finally build a stable and flexible module I could easily reuse. But as any good Drupal citizen knows, better to see what's already out in the contrib space before recreating the wheel. I also like to scour the landscape for any lateset and greatest libraries before a new project, in this case for JQuery image transitions. I came across Nivo Slider, which quickly led me to the corresponding Drupal module. Combine this with CCK (imagefield), Imagecache, and Views, bundle it into a Feature, and voila, a 1 hour flexible and reusable banner transition tool.

Final recipe is as follows.

  • Create a content type with an imagefield and an optional link field. I would default this content type to unpublished since you likely don't want it turning up in search results or elsewhere.
  • Define an imagecache preset that will size images for the banner space in mind. All the images need to be the same size for elegant transitions.
  • Create a view, e.g., header_images and configure as follows:
    • At least filter for the header content type. Make sure to include unpublished content if you're taking the approach suggested above.
    • Add fields for the image, an optional title which is used as the image caption, and an optional link.
    • Set your view style as Nivo Slider and adjust the global transition settings.
    • Set your row style as Nivo Slider and map your image, title, and link fields.
  • Add a block display to the view.
  • Optionally place the block into a Context.
  • Bundle everything into a Feature and deploy. The feature should include the content type, view, imagecache preset, relevant permissions, and optional context.

That's it. You can see this feature in action at the new Portland Design Works site. And next time I need something similar, it will take closer to 15 minutes rather than 60.

UPDATE (9/23/2010): Feature posted for download. I'm still working out the best way to manage my features, aside from running a feature server. Suggestions welcome!

AttachmentSize 13.16 KB
May 27 2010
Lev
May 27

I recently had to implement what seemed like a very simple feature for a client, moving several of the local tasks located on a user's profile page into the site's primary menu. The menu paths in question are dynamic, E.g, /user/%/edit, /user/%/orders, /user/%/notifications, etc., which at first seemed like slight complication. So how to tackle this? At first blush, one might think that you can just use the Menu module to add a dynamic menu item though the GUI or menu API. Well, that won't work. You can only create a menu item that way for an existing path that you have access to. Luckily, Drupal provides a hook that seems like the perfect solution,

<?php
function hook_menu_alter(&$items) {
 
// Example - disable the page at node/add
 
$items['node/add']['access callback'] = FALSE;
}
?>

Great, so I can change the type of the menu items I want to alter by doing something like the code below and problem solved!

<?php
function mymodule_menu_alter(&$items) {
 
$items['user/%user/edit']['type'] = MENU_NORMAL_ITEM;
}
?>

WRONG! At least that's what I realized after much trial and error. Turns out that menu items with wildcards will NOT now show up in the menu tree, and there are no warnings or explanations to that effect. I never found that documented anywhere, only came across it in my trusty copy of Pro Drupal Development, along with following some trails in the code and on another blog post. But that's not where the confusion ends, because menu items with wildcards can appear in the menu tree if the wildcard is a function name that ends with to_arg, e.g., user/%user_uid_optional, and user_uid_optional_to_arg() can be found in user.module. So I'm getting closer, but how do I change those menu items since they don't have one of those nifty to_arg() wildcards? Well, I couldn't think of a way, so in the end, I created my own menu items using user_uid_optional_to_arg() as the placeholder. The code looks something like this,

<?php
function mymodule_menu() {
 
$items['mymodule/orders/%user_uid_optional'] = array(
   
'title' => 'My Orders',
   
'page callback' => '_mymodule_reroute',
   
'page arguments' => array(2, 1),
   
'access callback' => '_mymodule_access_account',
   
'access arguments' => array(2),
   
'type' => MENU_NORMAL_ITEM,
   
'menu_name' => 'primary-links'
 
);
 
$items['mymodule/notifications/%user_uid_optional'] = array(
   
'title' => 'Notifications',
   
'page callback' => '_mymodule_reroute',
   
'page arguments' => array(2, 1),
   
'access callback' => '_mymodule_access_account',
   
'access arguments' => array(2),
   
'type' => MENU_NORMAL_ITEM,
   
'menu_name' => 'primary-links'
 
);
 
$items['mymodule/recurring-fees/%user_uid_optional'] = array(
   
'title' => 'Recurring Fees',
   
'page callback' => '_mymodule_reroute',
   
'page arguments' => array(2, 1),
   
'access callback' => '_mymodule_access_account',
   
'access arguments' => array(2),
   
'type' => MENU_NORMAL_ITEM,
   
'menu_name' => 'primary-links'
 
); 
}function
_mymodule_util_reroute($user, $tab) {
 
drupal_goto('user/'. $user->uid .'/'. $tab, NULL, NULL);
}
?>

Notice the simple and, in my opinion, hack-ish reroute function that actually directs users to the correct destination. In many ways, this is duplicate code, not resilient in the face of changes in other modules, and the href on the new links doesn't match the final destination. Since in this case these links are only available for authenticated users, I'm not worried about SEO implications. So that's my solution to the simple problem of adding some account related links to the primary menu, and I don't like it one bit (even though I burned way too much time on it!). Are there better approaches? I hope so, feedback welcome!

May 13 2010
May 13

I'm a huge fan of the Webform module (and building Drupal forms in general), and I just today noticed a feature I hadn't previously taken advantage of. This is the ability to programmatically add what are called "pre-built" option lists that can be used in your webforms.

Webform pre-built options list in action

Let's say that you want the user to select his/her age from a dropdown. Age possibilities range from 1-100. Without a pre-built option list, you'd have to type in 0|0, 1|1, 2|2, etc. into the "options" field. You can now do this using the webform api and a small custom module for your site.

In your custom module, you have to declare your new options list. Here's an example:

<?php
/**
* Implementation of hook_webform_select_options_info().
* See webform/webform_hooks.php for further information on this hook in the Webform API.
*/
function YOURMODULENAME_webform_select_options_info() {
 
$items = array();
 
$items['one_onehundred'] = array(
   
'title' => t('Numbers 1-100'),
   
'options callback' => 'YOURMODULENAME_options_one_onehundred'
 
);
  return
$items;
}
?>

After that, you need to write a function that spits out the options list. For this example, that looks like:

<?php
/**
* Build an options list for use by webforms.
*/
function YOURMODULENAME_options_one_onehundred() {
 
$options = array();
  for (
$x=1; $x <= 100; $x++) {
   
$options[$x] = $x;
  }
  return
$options;
}
?>

After that, just enable your custom module, and you should be able to use your new pre-built options list in your webforms. Check out the webform_hooks.php file (included in the Webform module directory) for more information.

Note: This is for Drupal 6 and Webform 3.0-beta 5.

Sep 30 2009
Sep 30
Drupal

One of the major problems with Drupal is its learning curve which is quite long. There are many efforts from many poeple for solving this problem. and here is another attempt.

I'm not going to write tutorials here, this research project is going to be jump start for Drupal's newbie developers. it acts more like a hub, people can come here and simple pick their topic and find the most useful resources for starting.

Mastering Drupal | Drupal video tutorials for fast learning (Not reviewed yet)

Apr 19 2009
Apr 19

This video/tutorial was initially started as a presentation for the Denver Open Media Conference this weekend. Since we had a snowstorm on the day of my session, I decided to do this as a screencast here instead. My description of the session and the screencast appear below:

The FlashVideo module provides a simple, yet powerful interface for your users to upload video files of various formats and have them convert and display in a Flash player of your choice. According to the D.O. usage stats, currently over 3,200 Drupal sites are using this module to create YouTube-style sites. In this session, I'll show how to install and configure FlashVideo to build a user-contributed video site.

Oct 07 2008
Oct 07

One of the great improvements in Views 2 is the ability to create various page and block displays for one view. Block views can be linked to a page display when the More link option in the block's settings is enabled.

If there are one or more page displays set up in the view, the page to be linked can be chosen after clicking on the link next to the Page display option. See the screenshot below for an example:

Views 2 Block settings: Link display optionViews 2 Block settings: Link display option

The default anchor text of more links in block views is more which comes with the benefit to be pretty generic but is not good from an SEO point of view.

Anchor texts are very important for search engine rankings as the number one search result for click here demonstrates. Neither the words click nor here actually occur on the Adobe Reader download page, but it is linked with this anchor text very often.

To replace the default anchor text for more links with the title of the linked page, which is hopefully more meaningful, you can override the views-more.tpl.php template file by taking the following steps:

  1. Copy views-more.tpl.php from PATH_TO_VIEWS_MODULE/theme/ to your theme directory
  2. Replace the content of views-more.tpl.php in your theme directory with the code below.
  3. Clear the theme cache. You can achieve this by clicking on Clear cached data at the bottom of the Performance settings form on the admin/settings/performance page. This will clear all cached data, which should be avoided on high traffic sites. Alternatively you can call drupal_rebuild_theme_registry() in your template.php file once after you copied and edited the views-more.tpl.php file or by executing this SQL query
    DELETE FROM `cache` WHERE cid LIKE 'theme_registry%';

Code in views-more.tpl.php

<?php
$offset
= strlen(base_path());
$path = substr($more_url, $offset);
$menu_item = menu_get_item( $path );
$title = check_plain($menu_item['title']);
?>

<div class="more-link">
  <a href="http://www.seo-expert-blog.com/tutorial/how-to-optimize-views-2-more-links-anchor-texts/<?php print $more_url; ?>">
    <?php print $title; ?>
  </a>
</div>

The $more_url variable is made available by the views module in the views-more.tpl.php template file. It contains the absolute path to the linked page view. To get the title of the linked page, the internal Drupal path is extracted from $more_url using PHP's substr() function.

The internal Drupal path is needed to call menu_get_item(), which returns a menu router item that in turn contains the page title in the value for the title array key.

Actually, I am not sure whether calling check_plain() on the title is necessary here, since I assume that this taken care of before the router item is written to the database. Any insight on that is greatly appreciated.

The remainder of the code in the template file consists of the mark up of the more link with the linked page title as the anchor text.

That's it. A simple yet effective method of optimizing the anchor texts of more links in Views 2 blocks. A demonstration of this code in use can be seen in various blocks on linux-netbook.com.

The screenshot of the views settings page and the annotations are done with the awesome Firefox extension FireShot, that I just discoverd today. There is great software outside the Drupal universe ;-)

Mar 29 2008
Mar 29

Requirements: basic css, image editing, basic theming

The fivestar module provides a tidy little voting widget that allows users to vote on nodes. It also provides a CCK field of the type "Fivestar Rating", which can be used to rate a node on multiple criteria.

One of my pet projects, Hill Bomb, requires just this functionality. It's a maps-mashup site for downhill riders, for example skateboarders or mountain bikers, to upload details and maps of awesome hills around the world. Here are the some of the ratings that users can give to hills:

ratings-1.png

As one of my friendly beta testers pointed out to me, it's difficult to judge what "more flames" means in each individual context. For example, does having more flames mean a better road surface, or more danger to the rider?

It would be better to have different image sets for each rating that clearly indicates what more and less actually means. Fivestar module doesn't provide this functionality out of the box - you can only choose one set of icons for all your Fivestar fields. So, I had to do a bit of modification to get it working.

Here's a tutorial with the examples from Hill Bomb. I only add one different image set here, but it can easily be extended for multiple.

Step 1: The images

Create the icon sets for each field. I chose small cars for the traffic fields. It's hopefully obvious that more cars = more traffic!

cars.png

Making this image was easy in the fantastic open-source image manipulation program Paint.NET. It's just a matter of finding a cool icon, extending it's canvas to be 3 times it's original height, then duplicating it twice, and finally editing each one as preferred. The top image is the deactivated state, the second is the activated state, and the third is the roll-over state. Here I just left the roll-over to be identical to the activated state.

I also made this two-state "delete vote" image, the bottom one being the roll-over.

delete-cars.png

Step 2: The custom widget

Next, go to sites/all/modules/fivestar/widgets, where you'll find a directory for each available widget set. Create a new directory that will contain your new image set. In this case, I called it "hillbomb", after the site.

Now copy the contents of another widget directory, that will act as the base or fallback icons. These will be displayed by default, except where you specify your new icon set to be shown. I chose the flames icons, and copied over the three files (two sprite images and a .css). I renamed the .css from flames.css to the name of the folder, hillbomb.css.

Also copy in your new images that were made in step 1.

Step 3: The images in the css

Edit the css file in your new widget directory (in my example, hillbomb.css). Select everything, and copy-paste a duplicate of it at the end of the file. The original section should be left alone - this will ensure that the base icons (the flames) still work everywhere.

In this duplicated css, you need to change all references to the image flames.png to your new image, then do the same for the "delete vote" image (delete.png).

So:
background: url(flame.png) no-repeat 0 0;
becomes
background: url(cars.png) no-repeat 0 0;

Now change all height/width values in the duplicated css to match your new images. It's easy to work out - where the original css file has the width/height of a single sprite from the old image, then use the width/height from the new. If it has double the old image's height, then enter double the new image's height.

Step 4: Specify which CCK Fivestar field has our custom images

In order to make our new image sets display only on certain CCK fields, we need to make sure the scope of the css selector is correct. In my example case, I just add div.field-field-day-traffic before each of the static view selectors, because that's the class given to the div surrounding the field:

/*Override the traffic fields */
/* Static View-only Star Version - TRAFFIC (CARS)*/
div.field-field-day-traffic div.fivestar-widget-static .star {
  width: 20px;
  height: 20px;
  background: url(cars.png) no-repeat 0 0;
}

Note this will only work for the static view (i.e. when a fivestar field is displayed in the node's body), not for the node edit form. We get to that shortly.

Step 5: Give it a quick test

You should now be able to choose your new widget in your Fivestar settings, located at admin/settings/fivestar.

Test to make sure it works - make sure you try it out on a node with a Fivestar CCK field of the same name you specified in the css of Step 4. View the body of the node (static view of the Fivestar rating), and the node edit form (Javascript view).

Of course, the Javascript view of the widget will still display the flames instead of the cars, and also presents a small problem. If you use Firebug to inspect the HTML of the widgets, you'll notice there's no way to discern which CCK field each one belongs to, and thus no way to select just that field for our new images. In my example, there's no way of telling just the Javascript traffic field to display the cars instead of flames - it's all or nothing. Each one is just a <div class="fivestar-form-item fivestar-labels-hover">.

Step 6: Override the theme function for Javascript Fivestar

The simplest solution I found was to override the theme function, enclosing the widget in a div with a more descriptive class. This goes in template.php:

<?php
/**
* Customise the fivestar element, add another div so we know which icons to draw
*/
function phptemplate_fivestar($element) {
 
// Add necessary css.
 
fivestar_add_css();  // Add necessary Javascript.
 
if ($element['#widget'] == 'stars' ) {
   
fivestar_add_js();
  }
  return
'<div class="fivestar-' . $element['#parents'][0] . '">' . theme('form_element', $element, $element['#children']) . '</div>';
}
?>

The only difference from the original function is that instead of returning just theme('form_element', $element, $element['#children']), I enclose it in a div with the name of the field as a class. The name of the field is pulled from $element['#parents'][0]. The result of this for the traffic field will be:

<div class="fivestar-field_day_traffic">...widget...</div>

It's definitely not the most elegant solution, but unfortunately the Fivestar source code is a bit quirky. It doesn't really provide an opportunity to theme the widget's individual bits, as the work gets done in the function fivestar_expand(). I will provide a patch for the function, but until that gets committed, the easiest method is to use the above theme override.

Step 7: Add specific selectors into the css for the Javascript images

Back at the css file, you can now go to the section for the Javascript images, and using the new enclosing

, specify the field that will use the new images. My field was called field_day_traffic, so here's an example of the modifications:

div.fivestar-field_day_traffic div.fivestar-widget .cancel,
div.fivestar-field_day_traffic div.fivestar-widget .star {
  width: 20px;
  height: 20px;
}

And that should do it! Refresh the browser on the node edit page, and you'll see your new images used for the Javascript widget.

Here's screenshot of Hill Bomb's images. I use little road rollers for the road quality rating, as well as the cars and flames:

final-ratings.png

There has been a suggestion that Fivestar be modified to allow this functionality to be set easily for each field, however it was marked as "won't fix" by the maintainers, for fairly sound reasons I think.

You could also argue that the custom CSS and images should be in the theme folder rather than the widget's folder, but I'll leave that issue alone. Just don't forget about the custom widget when you upgrade the Fivestar module... ;)

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web