Last updated December 22, 2009. Created by coreyp_1 on January 14, 2006.
Edited by kenorb, sepeck, cel4145, Dublin Drupaller. Log in to edit this page.
description
This snippet will create a book navigation menu that can be present on your site any time, not just when navigating a book. It has three configurable features:
$book_top_pageis the node number which represents the top level of your book. Since Drupal and the book module support multiple books, you must supply the desired node number for the script to follow. You can set this to any book page number, even nodes in the middle of a book.$levels_deepis the number of levels you want to show (pre-expanded, so to speak) in the menu. "0" will result in nothing being shown, so don't use it. "1" will show one level, "2" will show two levels deep, etc.$emulate_book_blockdetermines how the code will emulate the book menu.- If
$emulate_book_blockis set toFALSE, then the menu will not expand to show child pages beyond the number of levels specified by$levels_deep. - If
$emulate_book_blockis set toTRUE, then the menu will expand to show the necessary pages. - If
$emulate_book_blockis set to a number (ex. 1, 2, 3, etc.), then the block will expand beyond$levels_deep, but it will limit itself accordingly.
- If
To behave exactly like the book block, set $levels_deep to "1", and $emulate_book_block to TRUE.
To have a book block that will only expand to the second level, set $emulate_book_block to 2.
usage suggestions
If you put this into a custom block, be sure to disable the default book block. Nothing bad will happen if you don't, but it might look odd to have two book navigation menus showing at the same time!
Placed in a page, this could easily provide a complete look at a book's entire hierarchy.
code
Because of significant changes to core APIs, the script is different for different versions of Drupal.
start here
All versions begin with this portion of code:
<?php
$book_top_page = 1;
$levels_deep = 1;
$emulate_book_block = true;
if (!function_exists('book_struct_recurse')){
// we wrap the function in this if() statment to avoid PHP errors
// when this is used in more than one block
function book_struct_recurse($nid, $levels_deep, $children, $current_lineage = array(), $emulate_book_block = true) {
$struct = '';
if ($children[$nid] && ($levels_deep > 0 || ($emulate_book_block && in_array($nid, $current_lineage)))) {
$struct = '<ul class="menu">';
foreach ($children[$nid] as $key => $node) {
if ($tree = book_struct_recurse($node->nid, $levels_deep - 1, $children, $current_lineage, is_numeric($emulate_book_block) ? ($emulate_book_block - 1 ? $emulate_book_block - 1 : false) : $emulate_book_block )) {
// a submenu will be printed below this item
$struct .= '<li class="expanded">';
$struct .= l($node->title, 'node/'. $node->nid);
$struct .= $tree;
$struct .= '</li>';
}
else {
// no submenu will be printed below this item, but let's see if we
// should call this <li> "collapsed" or just a "leaf"
// yes, it's complex logic
$class = ($emulate_book_block === true || (($emulate_book_block - 1) && $emulate_book_block > $levels_deep)) && $children[$node->nid] ? 'collapsed' : 'leaf';
$struct .= '<li class="' . $class . '">' . l($node->title, 'node/'. $node->nid) . '</li>';
}
}
$struct .= '</ul>';
return $struct;
}
}
}
?>Drupal 4.6, Drupal 4.7, and Drupal 5
For Drupal 4.6.x, 4.7.x, and 5.x, you should continue with this code (please note the difference in SQL for 4.6.x):
<?php
$current_lineage = array();
// use this version for Drupal 4.6
// $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid WHERE n.status = 1 ORDER BY b.weight, n.title'));
// use this version for Drupal 4.7 and Drupal 5
$result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 ORDER BY b.weight, n.title'));
while ($node = db_fetch_object($result)) {
if (!$children[$node->parent]) {
$children[$node->parent] = array();
}
array_push($children[$node->parent], $node);
if (arg(0) == 'node' && is_numeric(arg(1)) && arg(1) == $node->nid) {
$_temp = book_location($node);
foreach ($_temp as $key => $val){
$current_lineage[] = $val->nid;
}
$current_lineage[] = arg(1);
}
}
echo book_struct_recurse($book_top_page, $levels_deep, $children, $current_lineage, $emulate_book_block);
?>Drupal 6
For Drupal 6.x, use this portion of code instead:
<?php
$book_top_page= YOUR_NID;
$tree = menu_tree_all_data(book_menu_name($book_top_page));
print menu_tree_output($tree);
?>
Comments
Is this right for adding the block to all pages?
I put the suggested code into a custom block which I hoped to use for navigation; but it seems the block only shows up on the book pages. Removing this bit seemed to solve the problem? Is there a better way?
<?phpif (arg(0) == 'node' && is_numeric(arg(1)) && arg(1) == $node->nid) {
$_temp = book_location($node);
foreach ($_temp as $key => $val){
$current_lineage[] = $val->nid;
}
$current_lineage[] = arg(1);
}
?>
(I'm a rank amateur, so don't try this at home!)
Efficiency in SQL query
The sql query in this block will return *all* book nodes for php processing. If you have hundreds/thousands of book nodes, this will lead to all book nodes being processed by the interpreted PHP (not good). Since you've already got the limiting nodeid listed as book_top_page, I think it makes sense to limit the sql query to only returning those nodes which have book_top_page as their parent, e.g.:
...
$bookNodeQuery = 'SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid AND n.vid = b.vid AND b.parent = ' . $book_top_page . ' WHERE n.status = 1 ORDER BY b.weight, n.title';
$result = db_query(db_rewrite_sql($bookNodeQuery));
...
This adds another benefit, that you can now limit the returned set as well inside the SQL query:
...
$bookNodeQuery = 'SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid AND n.vid = b.vid AND b.parent = ' . $book_top_page . ' WHERE n.status = 1 ORDER BY b.weight, n.title LIMIT 5';
$result = db_query(db_rewrite_sql($bookNodeQuery));
...
And so on...
Query change
$bookNodeQuery = 'SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid AND n.vid = b.vid AND b.parent = ' . $book_top_page . ' WHERE n.status = 1 ORDER BY b.weight, n.title';
$result = db_query(db_rewrite_sql($bookNodeQuery));
This allows for injection and should be replaced with the following:
$bookNodeQuery = 'SELECT n.nid, n.title, b.parent, b.weight FROM {node} n INNER JOIN {book} b ON n.nid = b.nid AND n.vid = b.vid AND b.parent = %d WHERE n.status = 1 ORDER BY b.weight, n.title';
$result = db_query($bookNodeQuery,$book_top_page);
Minor thing but helps keep things closer to standards, esp. since db_query requires this in 6.x and recommends it in 5.x
Build a better tomorrow. Stop complaining about, stop talking about and do it already.
I agree with the potential
I agree with the potential problem with the SQL, but this solution breaks the script. It does not allow proper creation of the $children array, because of how D5 stores the book information. Limiting the SQL to the top level book results in only one level being printed, ever.
D6 is a little better, in that you can limit it to a specific book, but it is still a lot of information to process with PHP.
The only alternative I can see would be multiple smaller queries with a whole lot of PHP to figure out how to find the right nodes and put them into the array.
Maybe I'll figure out an alternative next time. On the bright side, block caching should be very helpful.
- Corey
- Corey
http://www.thefreecollege.com
Display problem also in Marinelli
Thank you for this useful snippet. Publishing the sections of a site as a book is much more efficient (for a web site editor) than using the 'page' content type.
As for the line 10 fix, it also applies to the Marinelli theme (http://drupal.org/project/marinelli).
If you don't care about recursion...
In Drupal 5, if all you really want is a list of the top-level sub-pages of a particular book, you can just do this:
<?phpprint book_tree(123);
?>
(where 123 is the node ID of the top-level book page.)
custom block for Drupal 6
Hi!
Unfortunately the suggested code does not work for Drupal 6.8
I have written a small custom block that displays the fully expanded menu for the specified book. I'm not sure about checking permissions, though.
<?php$book_top_page=YOURDATA;
$tree = menu_tree_all_data(book_menu_name($book_top_page));
print menu_tree_output($tree);
?>
How to add a link to add child from menu
Do you think there is a good way to add a link to add a child page to the item in the navigation. That is, from the menu, have the link got to the add new child? I tried to do this in 6.8 using this code:
$options=array();$options['query']['parent'] .= $node->nid;
$struct .= '<li class="' . $class . '">' . l($node->title, 'node/add/chapter' , $options) . '</li>';
The code looks right and the URL structure is the same as the actual add child. However, the parent nid is way off. Any suggestions?
Difference Between Snippet and API Function
New Drupal user here:
May I ask what the difference is between the contributed snippet and the following?
<?phpecho menu_tree( book_menu_name( $book_top_page ) );
?>
Is it more or less efficient?
Another approach
Here is another way to do it -- code adapted from book.module. I wanted to copy book module's style of making the book subject the first item and linking it to the first node of the book.
I created a tiny custom module to hold this function.
I welcome your code improvement suggestions.
<?php/**
* Displays a specific book navigation block when not in the book
*/
function YOUR_UNIQUE_MODULE_NAME_HERE_block($op = 'list', $delta = 0, $edit = array()) {
switch ($op) {
case 'list':
$block[0]['info'] = t('YOUR_BOOK_BLOCK_NAME_HERE');
$block[0]['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE;
return $block;
case 'view':
$block = array();
$tree = menu_tree_all_data(book_menu_name( YOUR_BOOK_ID_HERE ));
$data = array_shift($tree);
$block['subject'] = theme('book_title_link', $data['link']);
$block['content'] = ($data['below']) ? menu_tree_output($data['below']) : '';
return $block;
}
}
?>
unpublished nodes + private module
In Drupal6, the code above still shows unpublished nodes in the menu. Also, I am using the Private module and since the above code creates the tree directly from {menu_links} it doesn't pass through the necessary authentication. Below is my hack to resolve this.
Basically, I create an array of $nid to exclude, then don't populate the $menu array with any of these $nid's.
This isn't very flexible, but maybe it will help someone... or inspire someone to come up with a better solution :-)
To use, basically replace this section of code:
<?php$menu = array();
while ($m = db_fetch_object($result)) {
$menu[$m->mlid] = $m;
$menu[$m->mlid]->nid = $nid = (int)substr($m->link_path, 5);
}
?>
with this:
<?php
// don't show non-published nodes
$result1 = db_query('SELECT nid FROM {node} n WHERE n.status = 0');
$exclude_nid = array();
while ($id = db_fetch_object($result1)) {
$exclude_nid[] = (int)$id->nid;
}
// don't show private content to anonymous users
if (!user_is_logged_in()) {
$result2 = db_query('SELECT nid FROM {private} p WHERE p.private = 1');
while ($id = db_fetch_object($result2)) {
$exclude_nid[] = (int)$id->nid;
}
}
$menu = array();
while ($m = db_fetch_object($result)) {
$nid = (int)substr($m->link_path, 5);
if (!in_array($nid, $exclude_nid, false)) {
$menu[$m->mlid] = $m;
$menu[$m->mlid]->nid = $nid;
}
}
?>
Where is
Where is book_struct_recurse() defined? I can't find it in 6.15
Default behaviour in D6?
I'm confused about this snippet; I understand it's purpose in D5, but not for D6. If I enable the Book navigation block in D6 at
./admin/build/block/configure/book/0, it's default behaviour is to show on all pages anyway, so what's the point of the D6 version of this code snippet?There's even a configuration option in the block configuration page which allows to "display the block on every page", or to "display the block only on book pages" (not exactly like documented at api.drupal.org, but the settings are there anyway).
Greetings, -asb
Kefk | CineDat | Fotonexus | Encycan | Encymus | Taxidi
It will show the whole book
It will show the whole book tree in a block.
I want to know how to have the same behavior for D6 like the snippet does for D5. For D6 I cannot show only 1 level deep, it will always show the whole book tree.
It can also be used in a
It can also be used in a template to display the book nav of a book of your choice. I just now did just that with a views template - because the book nav isn't printed in D6/Views2 (and I couldn't figure out any other way to make that happen).
it doesn't work in 6.15
The code which started this whole thread would be awesome except it does not work in 6.15.
Looking at the code, I can't figure out where the function actually gets called.
Drupal seems to be using some completely unrelated function for printing the book menu. Thus, changing settings like $levels_deep and $emulate_book_block has no effect on the output. Too bad.
Anyone got a hint?
Here's my two cents
I was struggling with book navigation and came up with this:
<?php
$book_top_page = 8;
$nid = $book_top_page;
if (arg(0) == 'node' && is_numeric(arg(1))) {
$nid = arg(1);
$node = node_load($nid);
if (!$node->book) {
$nid = $book_top_page;
}
}
$node = node_load($nid);
$tree = menu_tree_all_data($node->book['menu_name'], $node->book);
// There should only be one element at the top level.
$data = array_shift($tree);
$block['subject'] = theme('book_title_link', $data['link']);
$block['content'] = '<div>';
$block['content'] .= ($data['below']) ? menu_tree_output($data['below']) : '';
$block['content'] .= '</div>';
print $block['content'];
?>
A few additions
I made a few simple additions so that I was able to view the contents of book pages below the current node
here is what I had done
<?php
$nid = $book_top_page;
if (arg(0) == 'node' && is_numeric(arg(1))) {
$nid = arg(1);
$node = node_load($nid);
if (!$node->book) {
$nid = $book_top_page;
}
}
$node = node_load($nid);
$tree = book_menu_subtree_data($node->book);
// There should only be one element at the top level.
$data = array_shift($tree);
$block['subject'] = theme('book_title_link', $data['link']);
$block['content'] = '<div>';
$block['content'] .= ($data['below']) ? menu_tree_output($data['below']) : '';
$block['content'] .= '</div>';
print $block['content'];
?>
It works... but...
Hi,
I really liked how the snippet you have above creates a block below my text on the page. But, the book outline still shows. So, basically, there are two book outlines on the page, one can show all the child pages, the other doesn't. Would you know how to hide/delete the book outline that doesn't show child pages without altering the book outline?
Here's basically what the page would look like:
- CONTENT
- BOOK OUTLINE (using your snippet) - shows child pages
- BOOK OUTLINE (using the book module) - doesn't show child pages
Thank you for your help!
This module may help some
http://drupal.org/project/bookblock
It allows you to display any individual book menu as a standard block that you can configure to appear on any pages you choose.
Any plans to update this for
Any plans to update this for D7?
For Drupal 7
It's a pretty minor change -- you just just need to pass the output of menu_tree_output() into drupal_render().
So:
<?php$book_top_page= YOUR_NID;
$tree = menu_tree_all_data(book_menu_name($book_top_page));
print drupal_render(menu_tree_output($tree));
?>
Documenting my own drupal experience: http://toddgee.com/
Helping the two-wheeled love: http://bikegeeks.org/
Thanks toddgee, that worked
Thanks toddgee, that worked perfectly.
Reggie W