How to add a tab to a node's display?

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
kirilius's picture

Hi, I posted this to the forums, but probably this is a better place to place it since it seems to be a problem for the advanced Drupal community...

I have a node type called XXXXX for example. When the node is displayed (by the author) usually there are two default tabs on the top:
View | Edit

Let's say I want to display some information related to the current node and I want to display that on a separate tab on the same level as View and Edit. So the tab line should look like this:
View | Edit | Related Information

The related information could be of any kind: static text, panels, blocks... ultimately I want to embed a view in this tab but the more general question is: How to add this tab for my node type XXXXX only?

Can you suggest some module(s) and settings, which allow for adding tabs without (with minimal) coding?

I know that Javascript Tools module provides tabs, also Panels module can be made to use those but all this is adding artificial visual tabs only. What I need is a real tab whit its own URL.

If a node's URL is http://mysite/node/111, much like the Edit tab loads in http://mysite/node/111/edit, I would like to add a new tab with an URL of type: http://mysite/node/111/new_tab

Thanks!

Comments

Tabs

danaktivix's picture

I did something similar myself this week, and it took some hours to figure it out. Hopefully I have it roughly correct.

There's no non-coding way of doing this that I know of. I suggest making a new module (check drupal site for a starter module tutorial, if you haven't done one before. You'll just need the bare bones - a .module file and a .info file.)

I figured this out in part by adapting code from another module that uses tabs. By the way, could you let me know where you posted this in the forums, and I'll post this there too, see if people can't correct me where I'm going awry. This does work, but there may be an easier way, or problems with my interpretation of what Drupal is doing.

Three things to know before looking at the code:

  • The first thing to realise is that, in drupal code-speak, a tab is a 'MENU_LOCAL_TASK'.
  • Next - and sorry if you know this already - but Drupal works out where to put things / run code based on the path you give it. See the admin/settings/useraccounthelp path below: just by defining that path, Drupal knows to add it to the admin page.
  • Lastly, menu items are cached - so if and when you get your code working, be sure to empty the 'cache_menu' table: Drupal will rebuild it with your changes included. Otherwise you may not see the tab, even when the code's fine. A good frustrating hour spent changing code before I realised that one! (The devel module has a clear cache function, but I can't get it to work. I just use phpmyadmin and a bit of mysql - 'TRUNCATE TABLE cache_menu' will empty the table columns, leaving the table itself intact.)

Now, this is what I did. I wanted to add a 'help' tab to all profile account pages. I made a module and stuck in the following, (which I'd adapted from the subscriptions module - this also sticks a tab in people's profile. Always look for code to mimic!) (minus the php tags, which'll be in your module...)

The first part - admin/settings/useraccounthelp - just sets up an admin page for setting the URL the tab goes to. The settings function it calls is below, if you're interested. The key part is 'path' => "user/". arg(1) ."/".$helpurl. See here for what the arg function does. For your node problem, I guess you won't need to use the arg function coz you know what the node number is. I can't imagine you'll need to do the "if (arg" test either, though maybe you do - something like "if (arg(0) == 'taxonomy' && arg(2) == whatever your node type is.

It's "'type' => MENU_LOCAL_TASK" that makes the link a tab. Why not "'type' => TAB?" Christ only knows.

'callback' then tells it what function to call. I don't know if this is the quickest way of doing this, but my callback calls my send_to_help_page function, passing in the $helpurl. Then we get to the page I want via the drupalgoto function. Done.

Be interested to hear if that makes any sense and you can adapt it. Thought I'd better write since it took me so long to work out. I can't promise it's the best way of doing it, but it works for me. And don't forget to clear the cache!

<?php
/**
* Implementation of hook_menu().
*/
function accounthelptab_menu($may_cache) {
 
 
$items = array();
  if (
$may_cache) {
   
$items[] = array(
     
'path' => 'admin/settings/useraccounthelp',
     
'title' => t('User Account helppage settings'),
     
'description' => t('Choose the page that the user account help tab link goes to.'),
     
'callback' => 'drupal_get_form',
     
'callback arguments' => array('accounthelptab_admin_settings'),
     
'access' => user_access('administer site configuration')
    );
    }
 
  
//testing to make sure we're in a user profile...     
  
if (arg(0) == 'user' && is_numeric(arg(1))) {
   

     
$helpurl = variable_get('accounthelptab_url', 'userhelp');
    
   
// User help page
    
$items[] = array(
       
'path' => "user/". arg(1) ."/".$helpurl,
       
'title' => t('Help'),
              
'weight' => 8,
               
'callback' => 'send_to_help_page', 'callback arguments' => $helpurl,
       
//'access' => user_access('maintain own subscriptions'),
       
'type' => MENU_LOCAL_TASK,
        );
      
  
   }
 
   return
$items;

}

function
send_to_help_page($helpurl) {
   
  
drupal_goto($path = $helpurl);
    
}

/**
* Define the settings form.
*/
function accounthelptab_admin_settings() {
 
$form['accounthelptab_url'] = array(
   
'#type' => 'textfield',
   
'#title' => t('URL for the account help page'),
   
'#description' => t('Enter the relative path for the user account help page.'),
   
'#default_value' => variable_get('accounthelptab_url', 'userhelp'),
   
'#title' => t('URL'),
    );
      
       return
system_settings_form($form);
}
?>

thanks!

cdjeremy's picture

That sure helps me. You just saved me a solid day of newbie mucking about. Thanks a million!

Now I just need to generalize it. here goes nothing...

Thanks a lot! At this point

kirilius's picture

Thanks a lot! At this point I am not sure if I can use your example directly since I know Drupal only from administration/configuration point of view and I am a complete newbie to module development.

One of the key things I did not get is how to display the tab for a specific node-type only.

You made a comment that probably I won't need the arg(1) function but I think I do. I need that tab displayed for ALL nodes of a given type so the nid should be there.

Here is ultimately what I want to do:

I am using Node Hierarchy module to create a simple parent-child-like structure:

Destination 1
|- Picture1
|- Picture2
|- Picture3

Destination 2
|- Picture4
|- Picture5
|- Picture6
|- Picture7

etc.

So I have 2 node types: Destination and Picture (imagefield + imagecache) and I want to let my users add "pictures" to some "destinations".
Node Hierarchy has a default "Children" page, displayed as a tab in the parent node. Unfortunately that is hard-coded as an extension to the theme and it is simply a list-style page, which is not suitable for showing pictures.
Luckily it also provides views integration so I defined a view, that shows the child-pictures in a nice gallery-style page with thumbnails.

So my real problem is how to add that view to ALL nodes of type "destination"

I tried two similar approaches: (1) using the views only and (2) using Panels 2 (a panel with embedded view).

I prefer Panels 2 slightly because it has the ability to configure a panel in the context of a specific node type only while the view would require some custom coding to do that.

Using both methods I have defined a proper URL: node/arg$/pictures, which works fine for a given destination node. What remains is to somehow add that view or panel to the "destination" node type.

Both Views and Panels provide settings (checkboxes) called "Provide Menu", "Provide Menu as Tab" and "Make Default Menu Tab". No matter how I combine them I am not getting the desired tab. In Panels 2 I actually managed to display a tab in the Preview mode but not in the real node display. So I wonder what these options are for ;-)

Anyway, having read your posting (as far as I understand it), I guess the idea is to create a module that (let's make it more general => usable) will have a configuration page under the Drupal administration, that will allow the administrator to associate node types with some URLs (e.g. node/arg$/pictures) and for each such URL will display a tab in all nodes from the associated type.

I would like to try building such a module but I am missing the structure and really don't know there to begin ;-(

I hope that makes sense to you. Any comments or ideas regarding this problem are welcome!

Thanks!

I spent some more time

kirilius's picture

I spent some more time reading the code and I think I now have some understanding of it... and of course some questions ;-)

There are two main if {...} operators in the accounthelptab_menu function.

The second one is clear - it adds the tab we need to the menu system and sets the proper URL for its contens.

I assume that Drupal scans all modules for XXX_menu functions and that's how it is rendering the menus. Please correct me if I am wrong.

I don't fully understand the meaning of the code within the first if {...} operator. From the values added to the $items array, I see that this is probably creating a settings page under the Drupal administration. What I don't understand is why is this code inside the accounthelptab_menu function? What is it doing there?

I would assume that creating an administration form and creating a menu (tab), which reads from that form are two separate tasks. Please correct me if I am wrong.

I would also assume that I'll have to replace the line:

"if (arg(0) == 'user' && is_numeric(arg(1))) {"

...with something that checks if we are in the context of a node of type XXX. What would such a check look like?

Thanks a lot for your help!

I spent a few more hours and

kirilius's picture

I spent a few more hours and I think I did it!

I had to give my view the same path as the new menu/tab's path so that they did't mix up to much. Here is the code:

<?php
function dest_pics_menu($may_cache) {
  
 
$items = array();
 
  if (
arg(0) == 'node' && is_numeric(arg(1))) {
     
$node = node_load(arg(1));
      if (
$node->nid && $node->type=="destination") {
        
$items[]= array(
         
'path' => 'node/'. arg(1) .'/dest_pics',
         
'title' => t('Destination Pictures'),
         
'callback' => 'send_to_dest_pic_page',
         
'callback arguments' => array(arg(1)),
         
'type' => MENU_LOCAL_TASK,
         
'weight' => 10
       
);
      }
    }

    return
$items;
  
}


function
send_to_dest_pic_page($parent_nid) {
  
   
drupal_goto($path = "node/". $parent_nid ."/dest_pics");
      
}
?>

One thing that I noticed in the sample code flying around was the if ($may_cache) {...} construct. I looked in the documentation and it literally says: "$may_cache A boolean indicating whether cacheable menu items should be returned."

What exactly is a "cacheable menu item" and who defines which menu is cacheable and which is not?

Anyway, now I have other walls to break (like how to configure a view to get its title from the parameter node ID...)

Thanks a lot for your help!

Panels

RobLoach's picture

Is there anyway we could get this working with Panels? Panels Tabs, for example, does this sort of thing, except not within the View/Edit tab menu.

I got that working using

kirilius's picture

I got that working using Panes Tabs. The problem there is that the tabs there don't have their own URLs and are independent from the main menu/tabs system in Drupal. While that is not a big problem in itself, having tabs related to a single node in two lines (or groups) with different behavior looks like a bad interface design idea.

That is why I am looking for a single solution for that problem. If only the "Provide Menu as Tab" option is Panels 2 worked the way I thought it should work... ;-)

bug fixed

dwees's picture

I think this bug got fixed in Panels Pages RC2. Should be able to actually see the tabs now instead of having them be invisible until you hit the url directly.

Dave

for Drupal 6 users...

rbishop's picture

for Drupal 6 users... seems you need to use %node instead of arg(1). Probably would help ive fully read the new menus for D6 first but hope that saves someone sometime.

$items['node/%node/new_tab'] = array(
...
)

but for only certain node types...?

ryan_courtnage's picture

<?php
$items
['node/%node/new_tab'] = array(
   
'title' => 'New Tab',
   
'page callback' => 'mycallback',
   
'page arguments' => array(1),
   
'access callback'   => TRUE,
   
'type' => MENU_LOCAL_TASK
)
?>

will work for adding a tab to a node in D6. However, to make this apply to only certain node types is beyond me.

The D6 menu migration pages at http://drupal.org/node/103114 state that:

Non menu code that was placed in hook_menu !$may_cache so that it could be run during initialisation, should now be moved to hook_init.

But what to do in hook_init escapes me. Does anyone have any idea what to do?

Ryan

loader function

ryan_courtnage's picture

best way to do this is with a loader function. easy peasy! See here: http://drupal.org/node/307510

If you want to add a tab only

phpdiva's picture

If you want to add a tab only to specific node types, the easiest way to go is add your own access function for that tab. Inside the access function you can check whether node is of certain type, and any other permissions you want, and return true or false based on that.

For example:

/
* Implementation of hook_menu().
*/
function mymodule_menu() {
  $items = array();
 
  $items['node/%node/mypath'] = array(
    'title' => 'Tab Title',
    'page callback' => 'mymodule_mytab',
    'page arguments' => array(1),
    'access callback' => _mymodule_check_tab_permission,
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );
 
  return $items;
}


/

* Check whether to display "my tab".
*/
function _mymodule_check_tab_permission($node) {
  if ($node->type == 'my_type') { // you can check other permissions here, like "view content", etc.
    return TRUE;
  }
  else {
    return FALSE;
  }
}

array(1) -> arg(1) ?

nally's picture

Thanks for the snippet.

There are a couple instances of array(1) in there. Should those be arg(1) instead?

No, it's actually array(1).

phpdiva's picture

No, it's actually array(1). Array stands for the array of parameters that are being passed in to the callback function, and 1 stands for which argument to grab from the URL string.
In the URL string each segment is an argument, and it's zero-based. So if you wanted to pass in the very first argument from the URL to your callback function, you would say array(0), etc. We want the second argument, that's why it is array(1).

POST arguments to a fnction from another module

EDDYL's picture

Hi,
I have managed to add a tab and understand how it's page callback / access callback functions work but still cannot achieve what I want...

I would like my tab to POST the nid (my tab is next to view and edit tabs) to a form function from another module.

I tried drupal_execute, but this does not render the form ; drupal_get_form seems not to be able to POST arguments,...

Please help.
Edouard

"L'exces de modération nuit gravement à la consommation"


Materiel et services agrométéorologiques : http://www.promete.fr

hook_menu and path components

Jeraarts's picture

From the API docs (http://api.drupal.org/api/drupal/modules--system--system.api.php/functio...):

"In addition to optional path arguments, the page callback and other callback functions may specify argument lists as arrays. These argument lists may contain both fixed/hard-coded argument values and integers that correspond to path components. When integers are used and the callback function is called, the corresponding path components will be substituted for the integers. That is, the integer 0 in an argument list will be replaced with the first path component, integer 1 with the second, and so on (path components are numbered starting from zero)"

if values of the array are

asad.hasan's picture

if values of the array are numeric they are passed to arg, and other types of inputs like strings are passed to the callback as is.

"If you have to do a sequence of actions more than twice, they are in the wrong abstraction layer :D" - Learned from drupal

Thanks!

oknate's picture

The other answers seemed to be doing conditional code in the hook_menu. This is awesome because the access function is separate from the menu cache!

Thanks!

Jeroen Aarts's picture

I just stumbled upon this post, and it was exactly what I needed - but didn't know yet how to do: conditionally display tabs in D6. Thanks for sharing this!

specific page

salientknight's picture

Is there a way to make this tab show a specific page?

It depends on what you mean

phpdiva's picture

It depends on what you mean by "page"... If it's just a node type page, in your access function check for specific node ID.

I'm trying to add a tab to

salientknight's picture

I am starting to see the problem... I have more reading to do :) thanks

Thanks

sivakrishna yarls's picture

thank you so munch

make the change in the theme?

simonaustin's picture

I have done this by making the change in the theme. Although probably not the best way to do it, I found it to be quite an easy workaround.

E.g. I wanted to add a "New" tab to enable the content poster to quickly add more content. I searched for a more elegant way of doing this first but couldn't find a solution so I modified my theme:

<?php
$nodetype
= $node->type;
$newnode = "<li ><a href=\"/node/add/$nodetype\">New</a></li>";
if (
$tabs): print '<ul class="tabs primary">'. $tabs . $newnode . '</ul></div>'; endif;
?>

You could fairly easily apply this to specific node types by populating $nodetype only if $node->type was equal to a specific set.

How to add node/add

reachkb's picture

Hi All,

I am very new to web development and Just now I have started using Drupal.

Currently I have created list of items on left sidebar using Navigation and these are displaying and working perfect.
Now I struck while adding path. It says like this "The path this menu item links to. This can be an internal Drupal path such as node/add or an external URL such as http://drupal.org. Enter to link to the front page."

But I would like to implement this with similar to "My account" for single and static page and "Administer" node for more then one item and dynamic page.

In my case "My team" is a single static page, and as soon as user click this link, team details has to displayed
And "Projects" is the multiple links, these has to expand and bring the dynamic page, when user click.

Can some body help me here.

making node/add available through node tabs

davey853's picture

thanks for the discussion above, gave me some ideas to try, my problem was I wanted to make the node/add function available by adding an "Add" tab next to the usual "View" and "Edit" tabs you get when viewing a single node (and have appropriate permissions).

deciding to add the following code to the page template page.tbl.php

<?php
 
if (($tabs) && ($node->type)) {$tabs .= "<li>" .l("Add","node/add/".str_replace("_", "-", $node->type)) ."</li>"; } 
?>

Thanks

aya.y's picture

Thank you all, i got what i need .

Aya Younis

Add ADD (node) tab

greta_drupal's picture

For adding a node/add tab, use the "Add Another" module. It will automatically add the tab/link for the specific node type being used. One of my favorite sleeper modules. The lastest version allows you to add it as a link or tab.

How about this?

How about access callbacks?

t1gor's picture

How about access callbacks?

    // alter node edit page - add tab
$items['node/%/settings'] = array(
    'title'             => 'Some settings',
    'page callback'     => 'somemodule_node_settings',
    'page arguments'        => array(1),
    'access callback'       => 'somemodule_node_settings_access',
    'access arguments'      => array(1),
    'type'              => MENU_LOCAL_TASK,
);