Book Module: How to make a book navigation block appear on all pages.

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:

  1. $book_top_page is 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.
  2. $levels_deep is the number of levels you want to show in the initial navigation. "0" will result in nothing being shown. "1" will show one level, "2" will show two levels deep, etc.
  3. $emulate_book_block determines whether or not the code will emulate the book menu. For example, if $emulate_book_block is set to FALSE, then the menu will not expand to show child pages beyond the number of levels specified by $levels_deep. If $emulate_book_block is set to TRUE, then the menu will expand to show the necessary pages.

To behave exactly like the book block, set $levels_deep to "1", and $emulate_book_block to TRUE.

usage

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.

<?php
$book_top_page
= 159;
$levels_deep = 2;
$emulate_book_block = true;

if (!
function_exists('book_struct_recurse')){
  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>';
      foreach (
$children[$nid] as $key => $node) {
        if (
$tree = book_struct_recurse($node->nid, $levels_deep - 1, $children, $current_lineage, $emulate_book_block)) {
         
$struct .= '<li class="expanded">';
         
$struct .= l($node->title, 'node/'. $node->nid);
         
$struct .= $tree;
         
$struct .= '</li>';
        }
        else {
          if (
$children[$node->nid]){
           
$struct .= '<li class="collapsed">'. l($node->title, 'node/'. $node->nid) .'</li>';
          }
          else {
           
$struct .= '<li class="leaf">'. l($node->title, 'node/'. $node->nid) .'</li>';
          }
        }
      }
     
$struct .= '</ul>';
      return
$struct;
    }
  }
}

$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.nid = b.nid AND 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);
?>

Note: this snippet may not quite display correctly in Garland (Drupal 5.x).

Changing line 10 $struct = '<ul>'; to $struct = '<ul class="menu">'; fixes the problem.

Is this right for adding the block to all pages?

mnoyes - May 30, 2007 - 04:00

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?

<?php
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);
  }
?>

(I'm a rank amateur, so don't try this at home!)

Efficiency in SQL query

jcolbyk - January 13, 2008 - 19:11

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

btopro - April 8, 2008 - 18:45

$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

Display problem also in Marinelli

mapiedra - April 9, 2008 - 03:38

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...

webchick - May 15, 2008 - 14:41

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:

<?php
print book_tree(123);
?>

(where 123 is the node ID of the top-level book page.)

 
 

Drupal is a registered trademark of Dries Buytaert.