drupal

Theming Local Task Tabs in Drupal 6

In one of the primary applications on our intranet, we use small icons in the process to help our Agents and Home office users through the process.

Agents see something like this:

While our employees see something more along the lines of this:

There are various combinations but the point that I’d like to impress is that a use-case exists for theming Drupal’s MENU_LOCAL_TASK tabs.

In Drupal 5, you could get away with doing stuff like this in hook_menu?:

    $items[] = array(
      ‘path’ => ‘node/’.arg(1).‘/reject’,
      ‘title’ => ‘Reject’,
      ‘type’ => MENU_LOCAL_TASK, 
      ‘callback’ => ‘drupal_get_form’,
      ‘callback arguments’ => array(‘pcf_casetracker_form_reject’,$node),
      ‘access’ => $finish_access,
      ‘weight’ => 3,
      ‘class’ => ‘hasicon reject’); // Special class for my my tab.

Then, a simple theme override:

function garland_menu_local_task($mid, $active, $primary) {
  $item = menu_get_item($mid);
  if ($active) {
    return ‘<li class="active’.($item[‘class’]?‘ ‘.$item[‘class’]:‘’).‘">’. menu_item_link($mid) ."</li>\n";
  }
  else {
    return ‘<li’.($item[‘class’]?‘ class="’.$item[‘class’].‘"’:‘’).‘>’. menu_item_link($mid) ."</li>\n";
  }
}

Would give you the intended results.

This, however, does not work in Drupal 6. There are two reasons:

Firstly, in Drupal 6, two theme functions are used to build links to menu tabs:

 theme(‘menu_item_link’, $link)
 theme(‘menu_local_task’, $link, $active = FALSE)

menu_item_link takes the actual menu router item as a parameter. It returns an HTML link. menu_local_task takes just the link, wraps it with an <li> tag, and adds the appropriate class if it is $active. At no time does the $menu_router item get passed to the function where it could affect the display of the <li> tag.

Secondly, the menu router system stores all of its values in a table called… menu_router. Writing entries to this table strips them of any values which are not in the table to begin with. So adding  ‘class’ => ‘css_class’ in the menu’s item in hook_menu() does nothing.

So how do we do this? I’ve got a hack, and a possible “fix.”

The Hack

In the menu system,  ‘page arguments’ and  ‘access arguments’ can be utilized to pass extra parameters to your page and access callbacks. These arguments get serialized before they get sent to the database. So you can actually stick a bunch of stuff in here. So, if you write your own access callback to only utilize the first param, you can stick extra information on those callback arguments like so:

  $items[‘node/%pcf_node/reject’] = array(
    ‘title’ => ‘Reject’,
    ‘type’ => MENU_LOCAL_TASK,
    ...
    ‘access callback’ => ‘pcf_casetracker_can_finish’,
    ‘access arguments’ => array(1, array(‘class’ => ‘hasicon reject’)),
  );

And then simply override your theme callbacks to do some trickery. Basically, test for that extra set of classes and build the link and the item entry in theme(‘menu_item_link’) instead of building it in theme(‘menu_local_task’). Then, if menu_local_task detects the ‘<li’ at the beginning, it will just let it pass through. Now your <li> tags can have extra css or attributes passed to them.

function garland_menu_local_task($link, $active = FALSE) {
  if (substr($link, 0, 3) != ‘<li’)
    return ‘<li ‘. ($active ? ‘class="active" ‘ : ‘’) .‘>’. $link ."</li>\n";
 
  return $active ? str_replace(‘class="’, ‘class="active ‘, $link) : $link;
}
 
function garland_menu_item_link($link) {
  if (empty($link[‘localized_options’])) {
    $link[‘localized_options’] = array();
  }
 
  if ($link[‘access_arguments’] && ($stuff = unserialize($link[‘access_arguments’])) && is_array($stuff) && ($b = array_pop($stuff)) && is_array($b)) {
    if ($b[‘class’]) {
      $link[‘class’] = $b[‘class’];
    }
  }
 
  if ($link[‘class’]) {
    return ‘<li class="’.($link[‘class’] ? ‘ ‘.$link[‘class’] : ‘’).‘">’. l($link[‘title’], $link[‘href’], $link[‘localized_options’]) ."</li>\n";
  }
 
  return l($link[‘title’], $link[‘href’], $link[‘localized_options’]);
}

The “Fix”

Since Drupal 6 isn’t taking any new features, it is highly unlikely that this will get fixed. At any rate, by modifying core to add two fields ‘theme callback’ and ‘theme arguments’, the menu system can be modified to add support for theming the individual items as they come out. From there, it is easy. One particular function, menu_local_tasks is responsible for actually rendering the links.

By modifying the function to look for the theme function and call it if it exits, we can do all sorts of cool things. The patch is down a the bottom of this post. If there is no  ‘theme callback’, it will fall back to the current method it uses.

It might be more worthwhile to split the actual rendering and collection of the tab information into two separate functions. This is probably the better way to do it. Also, there might be a better way to do it in D7.

Also, if you are using the Chaos tool suite you’d need to patch it as well (if you are using Garland).

There is also probably a way to do this that involves overriding the menu theme function just like ctools does it. The only problem that still remains is making sure that the menu tabs get the proper data associated with it. There doesn’t seem to be a no-brainer to attach that data after the fact. Could be wrong, though!

This is what the code in the new solution looks like (in your module, that is.)

The menu item itself:

  $items[‘node/%pcf_node/void’] = array(
    ‘title’ => ‘Void’,
    ‘type’ => MENU_LOCAL_TASK,
    ‘page callback’ => ‘drupal_get_form’,
    ‘page arguments’ => array(‘pcf_casetracker_form_void’, 1),
    ‘access callback’ => ‘pcf_casetracker_can_void’,
    ‘access arguments’ => array(1, array(‘class’ => ‘hasicon void’,)),
    ‘theme callback’ => ‘pcf_casetracker_tab’,
    ‘theme arguments’ => array(‘class’ => ‘hasicon void’),
    ‘weight’ => 9,);

The callback, which is basically theme(‘menu_item_link’) embedded in a tweaked copy of theme(‘menu_local_task’).

function theme_pcf_casetracker_tab($menu_item, $options, $active = FALSE) {
  if (empty($menu_item[‘localized_options’])) {
    $menu_item[‘localized_options’] = array();
  }
 
  $classes = array();
 
  if ($active)
    $classes[] = ‘active’;
 
  if (is_string($options[‘class’]))
    $classes[] = $options[‘class’];
 
  return ‘<li’. (count($classes) > 0 ? ‘ class="’. implode(‘ ‘, $classes) .‘"’ : ‘’) .‘>’. l($menu_item[‘title’], $menu_item[‘href’], $menu_item[‘localized_options’]) ."</li>\n";
}

Anyway, hope this helps someone.

dgo.to: A URL shortener (and smartener) for drupal.org

The last day of DrupalCon, after talking to several people about drupal.org URLs, dgo.to was born. It’s goal is to make drupal.org URLs easy and smarter. For instance, where else can see someone’s profile by just typing their name?

dgo.to/@nvahalik

Anyhoo, check out the site itself: dgo.to or this post on d.o for more information.

Any comments & suggestions are welcome!

Finally: Drupal 5 to Drupal 6 Upgrade of our Production Intranet

Since getting to RVOS, a lot of my focus has been on improving things. Improving the network (new APs, redundant switches), infrastructure downtime (40+ hours a week to <2hrs a week), improving applications (bugfixes on custom apps), and in general, the way business is done.

Our intranet went live in Q3 of 2007, when Drupal 6 was still -beta1. Right after we went live, I tried to upgrade and failed miserably. Ultimately, the idea got pushed to the back burner over and over for several years until roughly 2 weeks ago.

I figured this would be somewhat easy since most of the modules now have 6.x counterparts. This assumption probably would have been true if I was updating a simple Drupal site. However, Views, Panels, and all of our custom modules have been throwing me for a loop.

Here’s just a snapshot of what I’ve found so far:

  1. Upgraded Panels that had views had to be rebuilt after cough manually converting the Views. This doesn’t bother me so much, but it’s still painful on over 40 custom views.
  2. All of my blocks were automatically disabled after the upgrade.
  3. The groups part of LDAP integration module doesn’t upgrade properly. The names of the columns in 5.x start with ldap_groups_ but in 6.x they start with ldapgroups_. I’d work on a patch, but I’m just not sure how to actually ‘rename’ a column with the whole Schema API in Drupal 6.
  4. LDAP integration also breaks customizations. To do custom mapping of OUs to Roles, you had to edit a config file in the 5.x version. In 6.x, it’s a configuration parameter in the admin area. That’s actually great, but pretty much ruins any chance of automating it besides writing a custom module to stick that value in there (which I’m doing.)
  5. Errors, errors everywhere. Most of them appeared to be benign, but seeing several hundred errors flood out of an update script can be very unnerving.
  6. Running the update script felt like Windows Update. Had to re-run it five or six times before it finally got everything. Probably due to the above.

Anyhoo, now that I’ve vented, let the progress commence again!

My First (contributed) Drupal Module: Permission Report

Very proud to announce my first contributed drupal module! Permission Report:

Permission Report calculates and displays permissions a user has and shows which roles grant those permissions. It also provides ability to list users in a role, look at role membership information, view which users have a particular permission, and dig down into complicated role and permission problems.

Hope that this is the first of many contributions to come!

Avoid Using The Drupal "path" Module To Create Clean Paths In Your Module

I’ve been doing a lot of cleanup of certain modules at the office. Two in particular are heavily used apps that incorporate pre-made default views with screens to add nodes or do information lookups.

Part of the problem with this is that although I had a decent URL structure (many paths were aliased), my breadcrumbs were never right. I usually had to override them to make them work the way I wanted to (Drupal 5 on my end):

drupal_set_breadcrumb(array(...))

This is a hack! path.module is great for aliasing content paths. But stay away from it for stuff in your module.

Drupal’s drupal_get_breadcrumb actually calls menu_get_active_breadcrumb to figure out what to put in the breadcrumb list. Essentially, this function goes through the url, and looks at each element, gets the title of them if they have the MENU_VISIBLE_IN_BREADCRUMB flag on them. Both MENU_NORMAL_ITEM and MENU_ITEM_GROUPING have that flag by default.

Basically, if you write your menu hook properly, your breadcrumbs will automatically populate for you without any extra work.

The key here is putting this in your menu hook. If you use path.module’s path_set_alias or you add it manually via the interface, it won’t work! The reason is due to the fact that this is an alias. If you do this, menu_get_active_breadcrumb will see the “true” path and not the “aliased” path.

For example: Let’s say you have a node form that you want to put inside your path somewhere. If you do:

path_set_alias('node/add/claim','agents/claims/create')

Your breadcrumb for “agents/claims/create” will show the breadcrumb you will see on “node/add/claim.” (probably just “Home”)

However, if you do this:

$items[] = array( 'path' => 'agents/claims/create', 'type' => 'callback', 'callback' => 'node_add', 'callback arguments' => array('claim') );

Your breadcrumb will display something along of:

Home » Agents » Claims

Which is exactly what you want!

There is always a way to make the menu do what you want it to do. This includes using node_add to put node creation forms where you want them or using views_view_page for displaying views. It will make your breadcrumbs work right and make your code cleaner since you won’t be putting urls and menu information in your default views hook!


Theme & Icons by N.Design Studio
© 2010 Nicholas Vahalik
Syndicate content