book.module 38.8 KB
Newer Older
Dries's avatar
 
Dries committed
1
<?php
2
// $Id$
Dries's avatar
 
Dries committed
3

Dries's avatar
 
Dries committed
4 5
/**
 * @file
Dries's avatar
Dries committed
6
 * Allows users to structure the pages of a site in a hierarchy or outline.
Dries's avatar
 
Dries committed
7 8
 */

9
/**
10
 * Implementation of hook_theme().
11 12 13 14
 */
function book_theme() {
  return array(
    'book_navigation' => array(
Dries's avatar
Dries committed
15
      'arguments' => array('book_link' => NULL),
16
      'template' => 'book-navigation',
17 18
    ),
    'book_export_html' => array(
19 20
      'arguments' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL),
      'template' => 'book-export-html',
21 22 23 24
    ),
    'book_admin_table' => array(
      'arguments' => array('form' => NULL),
    ),
Dries's avatar
Dries committed
25 26 27 28 29
    'book_title_link' => array(
      'arguments' => array('link' => NULL),
    ),
    'book_all_books_block' => array(
      'arguments' => array('book_menus' => array()),
30 31 32 33 34
      'template' => 'book-all-books-block',
    ),
    'book_node_export_html' => array(
      'arguments' => array('node' => NULL, 'children' => NULL),
      'template' => 'book-node-export-html',
Dries's avatar
Dries committed
35
    ),
36 37 38
  );
}

39 40 41
/**
 * Implementation of hook_perm().
 */
Dries's avatar
 
Dries committed
42
function book_perm() {
43 44 45
  return array(
    'administer book outlines' => t('Manage books through the administration panel.'),
    'create new books' => t('Add new top-level books.'),
46
    'add content to books' => t('Add new content and child pages to books.'),
47 48
    'access printer-friendly version' => t('View a book page and all of its sub-pages as a single document for ease of printing. Can be performance heavy.'),
  );
Dries's avatar
 
Dries committed
49 50
}

Dries's avatar
 
Dries committed
51 52 53
/**
 * Implementation of hook_link().
 */
54
function book_link($type, $node = NULL, $teaser = FALSE) {
Dries's avatar
 
Dries committed
55 56
  $links = array();

Dries's avatar
Dries committed
57
  if ($type == 'node' && isset($node->book)) {
58
    if (!$teaser) {
Dries's avatar
Dries committed
59 60
      $child_type = variable_get('book_child_type', 'book');
      if ((user_access('add content to books') || user_access('administer book outlines')) && node_access('create', $child_type) && $node->status == 1 && $node->book['depth'] < MENU_MAX_DEPTH) {
61
        $links['book_add_child'] = array(
62
          'title' => t('Add child page'),
63 64
          'href' => "node/add/" . str_replace('_', '-', $child_type),
          'query' => "parent=" . $node->book['mlid'],
65
        );
Dries's avatar
 
Dries committed
66
      }
67

Dries's avatar
Dries committed
68
      if (user_access('access printer-friendly version')) {
69
        $links['book_printer'] = array(
70
          'title' => t('Printer-friendly version'),
71
          'href' => 'book/export/html/' . $node->nid,
72
          'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.'))
73
        );
74
      }
Dries's avatar
 
Dries committed
75
    }
Dries's avatar
 
Dries committed
76
  }
77

Dries's avatar
 
Dries committed
78
  return $links;
Dries's avatar
 
Dries committed
79 80
}

Dries's avatar
 
Dries committed
81 82 83
/**
 * Implementation of hook_menu().
 */
84 85
function book_menu() {
  $items['admin/content/book'] = array(
86
    'title' => 'Books',
Dries's avatar
Dries committed
87 88 89
    'description' => "Manage your site's book outlines.",
    'page callback' => 'book_admin_overview',
    'access arguments' => array('administer book outlines'),
90 91
  );
  $items['admin/content/book/list'] = array(
92
    'title' => 'List',
93 94
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
Dries's avatar
Dries committed
95 96
  $items['admin/content/book/settings'] = array(
    'title' => 'Settings',
97
    'page callback' => 'drupal_get_form',
Dries's avatar
Dries committed
98 99
    'page arguments' => array('book_admin_settings'),
    'access arguments' => array('administer site configuration'),
100 101 102
    'type' => MENU_LOCAL_TASK,
    'weight' => 8,
  );
Dries's avatar
Dries committed
103 104 105 106 107 108 109 110
  $items['admin/content/book/%node'] = array(
    'title' => 'Re-order book pages and change titles',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('book_admin_edit', 3),
    'access callback' => '_book_outline_access',
    'access arguments' => array(3),
    'type' => MENU_CALLBACK,
  );
111
  $items['book'] = array(
112
    'title' => 'Books',
113 114 115 116 117 118 119
    'page callback' => 'book_render',
    'access arguments' => array('access content'),
    'type' => MENU_SUGGESTED_ITEM,
  );
  $items['book/export/%/%'] = array(
    'page callback' => 'book_export',
    'page arguments' => array(2, 3),
Dries's avatar
Dries committed
120
    'access arguments' => array('access printer-friendly version'),
121 122
    'type' => MENU_CALLBACK,
  );
123
  $items['node/%node/outline'] = array(
124
    'title' => 'Outline',
Dries's avatar
Dries committed
125 126
    'page callback' => 'book_outline',
    'page arguments' => array(1),
127 128 129 130 131
    'access callback' => '_book_outline_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
  );
Dries's avatar
Dries committed
132 133 134 135 136 137 138 139
  $items['node/%node/outline/remove'] = array(
    'title' => 'Remove from outline',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('book_remove_form', 1),
    'access callback' => '_book_outline_remove_access',
    'access arguments' => array(1),
    'type' => MENU_CALLBACK,
  );
140
  $items['book/js/form'] = array(
Dries's avatar
Dries committed
141 142 143 144
    'page callback' => 'book_form_update',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
145

Dries's avatar
 
Dries committed
146 147 148
  return $items;
}

Dries's avatar
Dries committed
149 150 151
/**
 * Menu item access callback - determine if the outline tab is accessible.
 */
152
function _book_outline_access($node) {
Dries's avatar
Dries committed
153 154 155 156 157 158 159 160
  return user_access('administer book outlines') && node_access('view', $node);
}

/**
 * Menu item access callback - determine if the user can remove nodes from the outline.
 */
function _book_outline_remove_access($node) {
  return isset($node->book) && ($node->book['bid'] != $node->nid) && _book_outline_access($node);
161 162
}

Dries's avatar
Dries committed
163
/**
164
 * Implementation of hook_init().
Dries's avatar
Dries committed
165
 */
166
function book_init() {
167
  drupal_add_css(drupal_get_path('module', 'book') . '/book.css');
168 169
}

170 171 172
/**
 * Implementation of hook_block().
 *
Dries's avatar
 
Dries committed
173 174
 * Displays the book table of contents in a block when the current page is a
 * single-node view of a book node.
175
 */
176
function book_block($op = 'list', $delta = '', $edit = array()) {
Dries's avatar
 
Dries committed
177
  $block = array();
Dries's avatar
Dries committed
178 179
  switch ($op) {
    case 'list':
180 181
      $block['navigation']['info'] = t('Book navigation');
      $block['navigation']['cache'] = BLOCK_CACHE_PER_PAGE | BLOCK_CACHE_PER_ROLE;
182

Dries's avatar
Dries committed
183
      return $block;
184

Dries's avatar
Dries committed
185
    case 'view':
186 187 188
      $current_bid = 0;
      if ($node = menu_get_object()) {
        $current_bid = empty($node->book['bid']) ? 0 : $node->book['bid'];
189
      }
190

191
      if (variable_get('book_block_mode', 'all pages') == 'all pages') {
Dries's avatar
Dries committed
192 193
        $block['subject'] = t('Book navigation');
        $book_menus = array();
194
        $pseudo_tree = array(0 => array('below' => FALSE));
195
        foreach (book_get_books() as $book_id => $book) {
196 197 198
          if ($book['bid'] == $current_bid) {
            // If the current page is a node associated with a book, the menu
            // needs to be retrieved.
199
            $book_menus[$book_id] = menu_tree_output(menu_tree_all_data($node->book['menu_name'], $node->book));
200 201 202 203 204 205
          }
          else {
            // Since we know we will only display a link to the top node, there
            // is no reason to run an additional menu tree query for each book.
            $book['in_active_trail'] = FALSE;
            $pseudo_tree[0]['link'] = $book;
206
            $book_menus[$book_id] = menu_tree_output($pseudo_tree);
207
          }
Dries's avatar
 
Dries committed
208
        }
Dries's avatar
Dries committed
209
        $block['content'] = theme('book_all_books_block', $book_menus);
Dries's avatar
 
Dries committed
210
      }
211
      elseif ($current_bid) {
Dries's avatar
Dries committed
212
        // Only display this block when the user is browsing a book.
213 214 215 216 217 218 219 220
        $title = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $node->book['bid']));
        // Only show the block if the user has view access for the top-level node.
        if ($title) {
          $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'] = ($data['below']) ? menu_tree_output($data['below']) : '';
Dries's avatar
Dries committed
221 222
        }
      }
223

Dries's avatar
Dries committed
224
      return $block;
225

Dries's avatar
Dries committed
226 227 228 229 230 231 232 233 234 235
    case 'configure':
      $options = array(
        'all pages' => t('Show block on all pages'),
        'book pages' => t('Show block only on book pages'),
      );
      $form['book_block_mode'] = array(
        '#type' => 'radios',
        '#title' => t('Book navigation block display'),
        '#options' => $options,
        '#default_value' => variable_get('book_block_mode', 'all pages'),
236
        '#description' => t("If <em>Show block on all pages</em> is selected, the block will contain the automatically generated menus for all of the site's books. If <em>Show block only on book pages</em> is selected, the block will contain only the one menu corresponding to the current page's book. In this case, if the current page is not in a book, no block will be displayed. The <em>Page specific visibility settings</em> or other visibility settings can be used in addition to selectively display this block."),
Dries's avatar
Dries committed
237
        );
238

Dries's avatar
Dries committed
239
      return $form;
240

Dries's avatar
Dries committed
241 242 243 244 245
    case 'save':
      variable_set('book_block_mode', $edit['book_block_mode']);
      break;
  }
}
Dries's avatar
 
Dries committed
246

247
/**
Dries's avatar
Dries committed
248 249 250
 * Generate the HTML output for a link to a book title when used as a block title.
 *
 * @ingroup themeable
251
 */
Dries's avatar
Dries committed
252 253
function theme_book_title_link($link) {
  $link['options']['attributes']['class'] =  'book-title';
254

Dries's avatar
Dries committed
255
  return l($link['title'], $link['href'], $link['options']);
Dries's avatar
 
Dries committed
256
}
Dries's avatar
 
Dries committed
257

258
/**
Dries's avatar
Dries committed
259 260 261 262
 * Returns an array of all books.
 *
 * This list may be used for generating a list of all the books, or for building
 * the options for a form select.
263
 */
Dries's avatar
Dries committed
264 265 266 267 268 269 270 271 272 273
function book_get_books() {
  static $all_books;

  if (!isset($all_books)) {
    $all_books = array();
    $result = db_query("SELECT DISTINCT(bid) FROM {book}");
    $nids = array();
    while ($book = db_fetch_array($result)) {
      $nids[] = $book['bid'];
    }
274

Dries's avatar
Dries committed
275
    if ($nids) {
276
      $result2 = db_query(db_rewrite_sql("SELECT n.type, n.title, b.*, ml.* FROM {book} b INNER JOIN {node} n on b.nid = n.nid INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE n.nid IN (" . implode(',', $nids) . ") AND n.status = 1 ORDER BY ml.weight, ml.link_title"));
Dries's avatar
Dries committed
277 278 279
      while ($link = db_fetch_array($result2)) {
        $link['href'] = $link['link_path'];
        $link['options'] = unserialize($link['options']);
280
        $all_books[$link['bid']] = $link;
Dries's avatar
Dries committed
281 282
      }
    }
283
  }
284

Dries's avatar
Dries committed
285
  return $all_books;
286
}
287

Dries's avatar
Dries committed
288
/**
289 290 291
 * Implementation of hook_form_alter().
 *
 * Adds the book fieldset to the node form.
Dries's avatar
Dries committed
292 293 294 295 296 297
 *
 * @see book_pick_book_submit()
 * @see book_submit()
 */
function book_form_alter(&$form, $form_state, $form_id) {

298
  if (!empty($form['#node_edit_form'])) {
299
    // Add elements to the node form.
Dries's avatar
Dries committed
300 301 302 303 304
    $node = $form['#node'];

    $access = user_access('administer book outlines');
    if (!$access) {
      if (user_access('add content to books') && ((!empty($node->book['mlid']) && !empty($node->nid)) || book_type_is_allowed($node->type))) {
305
        // Already in the book hierarchy, or this node type is allowed.
Dries's avatar
Dries committed
306 307 308 309 310 311 312 313 314
        $access = TRUE;
      }
    }

    if ($access) {
      _book_add_form_elements($form, $node);
      $form['book']['pick-book'] = array(
        '#type' => 'submit',
        '#value' => t('Change book (update list of parents)'),
315
         // Submit the node form so the parent select options get updated.
316
         // This is typically only used when JS is disabled. Since the parent options
317 318
         // won't be changed via AJAX, a button is provided in the node form to submit
         // the form and generate options in the parent select corresponding to the
319
         // selected book. This is similar to what happens during a node preview.
320
        '#submit' => array('node_form_submit_build_node'),
Dries's avatar
Dries committed
321 322 323
        '#weight' => 20,
      );
    }
324
  }
Dries's avatar
Dries committed
325
}
326

Dries's avatar
Dries committed
327
/**
328
 * Build the parent selection form element for the node form or outline tab.
Dries's avatar
Dries committed
329 330 331 332 333 334
 *
 * This function is also called when generating a new set of options during the
 * AJAX callback, so an array is returned that can be used to replace an existing
 * form element.
 */
function _book_parent_select($book_link) {
335 336 337
  if (variable_get('menu_override_parent_selector', FALSE)) {
    return array();
  }
Dries's avatar
Dries committed
338 339 340 341 342 343 344
  // Offer a message or a drop-down to choose a different parent page.
  $form = array(
    '#type' => 'hidden',
    '#value' => -1,
    '#prefix' => '<div id="edit-book-plid-wrapper">',
    '#suffix' => '</div>',
  );
345

Dries's avatar
Dries committed
346 347 348
  if ($book_link['nid'] === $book_link['bid']) {
    // This is a book - at the top level.
    if ($book_link['original_bid'] === $book_link['bid']) {
349
      $form['#prefix'] .= '<em>' . t('This is the top-level page in this book.') . '</em>';
Dries's avatar
Dries committed
350 351
    }
    else {
352
      $form['#prefix'] .= '<em>' . t('This will be the top-level page in this book.') . '</em>';
Dries's avatar
Dries committed
353 354 355
    }
  }
  elseif (!$book_link['bid']) {
356
    $form['#prefix'] .= '<em>' . t('No book selected.') . '</em>';
357 358
  }
  else {
Dries's avatar
Dries committed
359 360 361 362
    $form = array(
      '#type' => 'select',
      '#title' => t('Parent item'),
      '#default_value' => $book_link['plid'],
363
      '#description' => t('The parent page in the book. The maximum depth for a book and all child pages is !maxdepth. Some pages in the selected book may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
364
      '#options' => book_toc($book_link['bid'], array($book_link['mlid']), $book_link['parent_depth_limit']),
365
      '#attributes' => array('class' => 'book-title-select'),
366
    );
Dries's avatar
 
Dries committed
367
  }
368

369
  return $form;
Dries's avatar
 
Dries committed
370 371
}

372
/**
Dries's avatar
Dries committed
373
 * Build the common elements of the book form for the node and outline forms.
374
 */
Dries's avatar
Dries committed
375 376 377
function _book_add_form_elements(&$form, $node) {
  // Need this for AJAX.
  $form['#cache'] = TRUE;
378
  drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() { $('#edit-book-pick-book').css('display', 'none'); }); }", 'inline');
Dries's avatar
Dries committed
379 380 381 382 383 384 385 386

  $form['book'] = array(
    '#type' => 'fieldset',
    '#title' => t('Book outline'),
    '#weight' => 10,
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#tree' => TRUE,
387
    '#attributes' => array('class' => 'book-outline-form'),
388
  );
389
  foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'original_bid', 'parent_depth_limit') as $key) {
Dries's avatar
Dries committed
390 391 392 393 394 395 396 397 398 399
    $form['book'][$key] = array(
      '#type' => 'value',
      '#value' => $node->book[$key],
    );
  }

  $form['book']['plid'] = _book_parent_select($node->book);

  $form['book']['weight'] = array(
    '#type' => 'weight',
400
    '#title' => t('Weight'),
Dries's avatar
Dries committed
401
    '#default_value' => $node->book['weight'],
402
    '#delta' => 15,
Dries's avatar
Dries committed
403
    '#weight' => 5,
404 405
    '#description' => t('Pages at a given level are ordered first by weight and then by title.'),
  );
Dries's avatar
Dries committed
406 407
  $options = array();
  $nid = isset($node->nid) ? $node->nid : 'new';
408

409
  if (isset($node->nid) && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) {
410 411 412 413 414 415 416 417 418
    // This is the top level node in a maximum depth book and thus cannot be moved.
    $options[$node->nid] = $node->title;
  }
  else {
    foreach (book_get_books() as $book) {
      $options[$book['nid']] = $book['title'];
    }
  }

Dries's avatar
Dries committed
419 420
  if (user_access('create new books') && ($nid == 'new' || ($nid != $node->book['original_bid']))) {
    // The node can become a new book, if it is not one already.
421
    $options = array($nid => '<' . t('create a new book') . '>') + $options;
Dries's avatar
Dries committed
422 423
  }
  if (!$node->book['mlid']) {
424
    // The node is not currently in the hierarchy.
425
    $options = array(0 => '<' . t('none') . '>') + $options;
Dries's avatar
Dries committed
426 427 428 429 430 431 432 433 434 435 436
  }

  // Add a drop-down to select the destination book.
  $form['book']['bid'] = array(
    '#type' => 'select',
    '#title' => t('Book'),
    '#default_value' => $node->book['bid'],
    '#options' => $options,
    '#access' => (bool)$options,
    '#description' => t('Your page will be a part of the selected book.'),
    '#weight' => -5,
437
    '#attributes' => array('class' => 'book-title-select'),
438 439 440 441 442
    '#ahah' => array(
      'path' => 'book/js/form',
      'wrapper' => 'edit-book-plid-wrapper',
      'effect' => 'slide',
    ),
443
  );
Dries's avatar
Dries committed
444
}
Dries's avatar
 
Dries committed
445

446
/**
Dries's avatar
Dries committed
447
 * Common helper function to handles additions and updates to the book outline.
Dries's avatar
Dries committed
448
 *
Dries's avatar
Dries committed
449 450 451 452 453 454 455 456 457
 * Performs all additions and updates to the book outline through node addition,
 * node editing, node deletion, or the outline tab.
 */
function _book_update_outline(&$node) {
  if (empty($node->book['bid'])) {
    return FALSE;
  }
  $new = empty($node->book['mlid']);

458
  $node->book['link_path'] = 'node/' . $node->nid;
Dries's avatar
Dries committed
459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
  $node->book['link_title'] = $node->title;
  $node->book['parent_mismatch'] = FALSE; // The normal case.

  if ($node->book['bid'] == $node->nid) {
    $node->book['plid'] = 0;
    $node->book['menu_name'] = book_menu_name($node->nid);
  }
  else {
    // Check in case the parent is not is this book; the book takes precedence.
    if (!empty($node->book['plid'])) {
      $parent = db_fetch_array(db_query("SELECT * FROM {book} WHERE mlid = %d", $node->book['plid']));
    }
    if (empty($node->book['plid']) || !$parent || $parent['bid'] != $node->book['bid']) {
      $node->book['plid'] = db_result(db_query("SELECT mlid FROM {book} WHERE nid = %d", $node->book['bid']));
      $node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled.
    }
  }
476

Dries's avatar
Dries committed
477 478 479 480 481 482 483 484 485 486 487
  if (menu_link_save($node->book)) {
    if ($new) {
      // Insert new.
      db_query("INSERT INTO {book} (nid, mlid, bid) VALUES (%d, %d, %d)", $node->nid, $node->book['mlid'], $node->book['bid']);
    }
    else {
      if ($node->book['bid'] != db_result(db_query("SELECT bid FROM {book} WHERE nid = %d", $node->nid))) {
        // Update the bid for this page and all children.
        book_update_bid($node->book);
      }
    }
488

Dries's avatar
Dries committed
489 490
    return TRUE;
  }
491 492

  // Failed to save the menu link.
Dries's avatar
Dries committed
493 494 495
  return FALSE;
}

496
/**
Dries's avatar
Dries committed
497
 * Update the bid for a page and its children when it is moved to a new book.
498
 *
Dries's avatar
Dries committed
499 500 501 502 503 504 505 506
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
 */
function book_update_bid($book_link) {
  for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) {
    $match[] = "p$i = %d";
    $args[] = $book_link["p$i"];
  }
507
  $result = db_query("SELECT mlid FROM {menu_links} WHERE " . implode(' AND ', $match), $args);
Dries's avatar
Dries committed
508 509 510 511 512

  $mlids = array();
  while ($a = db_fetch_array($result)) {
    $mlids[] = $a['mlid'];
  }
513

Dries's avatar
Dries committed
514
  if ($mlids) {
515
    db_query("UPDATE {book} SET bid = %d WHERE mlid IN (" . implode(',', $mlids) . ")", $book_link['bid']);
Dries's avatar
Dries committed
516 517 518 519 520
  }
}

/**
 * Get the book menu tree for a page, and return it as a linear array.
521
 *
Dries's avatar
Dries committed
522 523
 * @param $book_link
 *   A fully loaded menu link that is part of the book hierarchy.
524
 * @return
Dries's avatar
Dries committed
525 526 527 528
 *   A linear array of menu links in the order that the links are shown in the
 *   menu, so the previous and next pages are the elements before and after the
 *   element corresponding to $node.  The children of $node (if any) will come
 *   immediately after it in the array.
529
 */
Dries's avatar
Dries committed
530 531 532 533
function book_get_flat_menu($book_link) {
  static $flat = array();

  if (!isset($flat[$book_link['mlid']])) {
534
    // Call menu_tree_all_data() to take advantage of the menu system's caching.
Dries's avatar
Dries committed
535 536 537
    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
    $flat[$book_link['mlid']] = array();
    _book_flatten_menu($tree, $flat[$book_link['mlid']]);
Dries's avatar
 
Dries committed
538
  }
539

Dries's avatar
Dries committed
540
  return $flat[$book_link['mlid']];
Dries's avatar
 
Dries committed
541 542
}

543
/**
Dries's avatar
Dries committed
544
 * Recursive helper function for book_get_flat_menu().
545
 */
Dries's avatar
Dries committed
546 547 548 549 550 551 552 553
function _book_flatten_menu($tree, &$flat) {
  foreach ($tree as $data) {
    if (!$data['link']['hidden']) {
      $flat[$data['link']['mlid']] = $data['link'];
      if ($data['below']) {
        _book_flatten_menu($data['below'], $flat);
      }
    }
Dries's avatar
 
Dries committed
554
  }
Dries's avatar
Dries committed
555
}
Dries's avatar
 
Dries committed
556

Dries's avatar
Dries committed
557 558 559 560 561 562 563
/**
 * Fetches the menu link for the previous page of the book.
 */
function book_prev($book_link) {
  // If the parent is zero, we are at the start of a book.
  if ($book_link['plid'] == 0) {
    return NULL;
Dries's avatar
 
Dries committed
564
  }
Dries's avatar
Dries committed
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
  $flat = book_get_flat_menu($book_link);
  // Assigning the array to $flat resets the array pointer for use with each().
  $curr = NULL;
  do {
    $prev = $curr;
    list($key, $curr) = each($flat);
  } while ($key && $key != $book_link['mlid']);

  if ($key == $book_link['mlid']) {
    // The previous page in the book may be a child of the previous visible link.
    if ($prev['depth'] == $book_link['depth'] && $prev['has_children']) {
      // The subtree will have only one link at the top level - get its data.
      $data = array_shift(book_menu_subtree_data($prev));
      // The link of interest is the last child - iterate to find the deepest one.
      while ($data['below']) {
        $data = end($data['below']);
      }
582

Dries's avatar
Dries committed
583 584 585 586 587
      return $data['link'];
    }
    else {
      return $prev;
    }
Dries's avatar
 
Dries committed
588 589 590
  }
}

591
/**
Dries's avatar
Dries committed
592
 * Fetches the menu link for the next page of the book.
593
 */
Dries's avatar
Dries committed
594 595 596 597 598
function book_next($book_link) {
  $flat = book_get_flat_menu($book_link);
  // Assigning the array to $flat resets the array pointer for use with each().
  do {
    list($key, $curr) = each($flat);
599 600 601
  }
  while ($key && $key != $book_link['mlid']);

Dries's avatar
Dries committed
602 603
  if ($key == $book_link['mlid']) {
    return current($flat);
Dries's avatar
 
Dries committed
604
  }
Dries's avatar
Dries committed
605
}
Dries's avatar
 
Dries committed
606

Dries's avatar
Dries committed
607 608 609 610 611
/**
 * Format the menu links for the child pages of the current page.
 */
function book_children($book_link) {
  $flat = book_get_flat_menu($book_link);
612

Dries's avatar
Dries committed
613 614 615 616 617 618
  $children = array();

  if ($book_link['has_children']) {
    // Walk through the array until we find the current page.
    do {
      $link = array_shift($flat);
619 620
    }
    while ($link && ($link['mlid'] != $book_link['mlid']));
Dries's avatar
Dries committed
621 622 623 624 625
    // Continue though the array and collect the links whose parent is this page.
    while (($link = array_shift($flat)) && $link['plid'] == $book_link['mlid']) {
      $data['link'] = $link;
      $data['below'] = '';
      $children[] = $data;
Dries's avatar
 
Dries committed
626 627
    }
  }
628

Dries's avatar
Dries committed
629
  return $children ? menu_tree_output($children) : '';
Dries's avatar
 
Dries committed
630 631
}

632
/**
Dries's avatar
Dries committed
633
 * Generate the corresponding menu name from a book ID.
634
 */
Dries's avatar
Dries committed
635
function book_menu_name($bid) {
636
  return 'book-toc-' . $bid;
Dries's avatar
Dries committed
637 638 639 640 641 642 643 644 645 646
}

/**
 * Build an active trail to show in the breadcrumb.
 */
function book_build_active_trail($book_link) {
  static $trail;

  if (!isset($trail)) {
    $trail = array();
647
    $trail[] = array('title' => t('Home'), 'href' => '<front>', 'localized_options' => array());
Dries's avatar
Dries committed
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665

    $tree = menu_tree_all_data($book_link['menu_name'], $book_link);
    $curr = array_shift($tree);

    while ($curr) {
      if ($curr['link']['href'] == $book_link['href']) {
        $trail[] = $curr['link'];
        $curr = FALSE;
      }
      else {
        if ($curr['below'] && $curr['link']['in_active_trail']) {
          $trail[] = $curr['link'];
          $tree = $curr['below'];
        }
        $curr = array_shift($tree);
      }
    }
  }
666

Dries's avatar
Dries committed
667
  return $trail;
Dries's avatar
 
Dries committed
668 669
}

670
/**
Dries's avatar
 
Dries committed
671 672
 * Implementation of hook_nodeapi().
 *
Dries's avatar
Dries committed
673 674
 * Appends book navigation to all nodes in the book, and handles book outline
 * insertions and updates via the node form.
675
 */
Dries's avatar
 
Dries committed
676 677
function book_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
678
    case 'load':
Dries's avatar
Dries committed
679 680
      // Note - we cannot use book_link_load() because it will call node_load().
      $info['book'] = db_fetch_array(db_query('SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid = %d', $node->nid));
681

Dries's avatar
Dries committed
682 683 684 685
      if ($info['book']) {
        $info['book']['href'] = $info['book']['link_path'];
        $info['book']['title'] = $info['book']['link_title'];
        $info['book']['options'] = unserialize($info['book']['options']);
686

Dries's avatar
Dries committed
687
        return $info;
Dries's avatar
Dries committed
688
      }
689
      break;
690

Dries's avatar
 
Dries committed
691
    case 'view':
Dries's avatar
Dries committed
692 693
    if (!$teaser) {
        if (!empty($node->book['bid']) && $node->build_mode == NODE_BUILD_NORMAL) {
694
          $node->content['book_navigation'] = array(
695
            '#markup' => theme('book_navigation', $node->book),
696 697
            '#weight' => 100,
          );
698

699
          if ($page) {
Dries's avatar
Dries committed
700 701
            menu_set_active_trail(book_build_active_trail($node->book));
            menu_set_active_menu_name($node->book['menu_name']);
702
          }
Dries's avatar
 
Dries committed
703
        }
Dries's avatar
 
Dries committed
704
      }
Dries's avatar
 
Dries committed
705
      break;
706

Dries's avatar
Dries committed
707 708 709 710 711
    case 'presave':
      // Always save a revision for non-administrators.
      if (!empty($node->book['bid']) && !user_access('administer nodes')) {
        $node->revision = 1;
      }
712 713 714 715
      // Make sure a new node gets a new menu link.
      if (empty($node->nid)) {
        $node->book['mlid'] = NULL;
      }
Dries's avatar
Dries committed
716
      break;
717

Dries's avatar
Dries committed
718
    case 'insert':
719
    case 'update':
Dries's avatar
Dries committed
720 721 722 723
      if (!empty($node->book['bid'])) {
        if ($node->book['bid'] == 'new') {
          // New nodes that are their own book.
          $node->book['bid'] = $node->nid;
724
        }
Dries's avatar
Dries committed
725 726 727
        $node->book['nid'] = $node->nid;
        $node->book['menu_name'] = book_menu_name($node->book['bid']);
        _book_update_outline($node);
728 729
      }
      break;
730

731
    case 'delete':
Dries's avatar
Dries committed
732 733 734 735 736 737 738 739 740 741 742 743 744
      if (!empty($node->book['bid'])) {
        if ($node->nid == $node->book['bid']) {
          // Handle deletion of a top-level post.
          $result = db_query("SELECT b.nid FROM {menu_links} ml INNER JOIN {book} b on b.mlid = ml.mlid WHERE ml.plid = %d", $node->book['mlid']);
          while ($child = db_fetch_array($result)) {
            $child_node = node_load($child['nid']);
            $child_node->book['bid'] = $child_node->nid;
            _book_update_outline($child_node);
          }
        }
        menu_link_delete($node->book['mlid']);
        db_query('DELETE FROM {book} WHERE mlid = %d', $node->book['mlid']);
      }
745
      break;
746

Dries's avatar
Dries committed
747 748 749 750
    case 'prepare':
      // Prepare defaults for the add/edit form.
      if (empty($node->book) && (user_access('add content to books') || user_access('administer book outlines'))) {
        $node->book = array();
751

Dries's avatar
Dries committed
752 753 754
        if (empty($node->nid) && isset($_GET['parent']) && is_numeric($_GET['parent'])) {
          // Handle "Add child page" links:
          $parent = book_link_load($_GET['parent']);
755

Dries's avatar
Dries committed
756 757 758 759 760 761 762 763 764 765 766 767 768 769
          if ($parent && $parent['access']) {
            $node->book['bid'] = $parent['bid'];
            $node->book['plid'] = $parent['mlid'];
            $node->book['menu_name'] = $parent['menu_name'];
          }
        }
        // Set defaults.
        $node->book += _book_link_defaults(!empty($node->nid) ? $node->nid : 'new');
      }
      else {
        if (isset($node->book['bid']) && !isset($node->book['original_bid'])) {
          $node->book['original_bid'] = $node->book['bid'];
        }
      }
770 771 772 773
      // Find the depth limit for the parent select.
      if (isset($node->book['bid']) && !isset($node->book['parent_depth_limit'])) {
        $node->book['parent_depth_limit'] = _book_parent_depth_limit($node->book);
      }
Dries's avatar
Dries committed
774 775 776 777
      break;
  }
}

778 779 780 781 782 783 784
/**
 * Find the depth limit for items in the parent select.
 */
function _book_parent_depth_limit($book_link) {
  return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? menu_link_children_relative_depth($book_link) : 0);
}

Dries's avatar
Dries committed
785 786 787 788 789 790 791 792
/**
 * Form altering function for the confirm form for a single node deletion.
 */
function book_form_node_delete_confirm_alter(&$form, $form_state) {
  $node = node_load($form['nid']['#value']);

  if (isset($node->book) && $node->book['has_children']) {
    $form['book_warning'] = array(
793
      '#markup' => '<p>' . t('%title is part of a book outline, and has associated child pages. If you proceed with deletion, the child pages will be relocated automatically.', array('%title' => $node->title)) . '</p>',
Dries's avatar
Dries committed
794 795
      '#weight' => -10,
    );
Dries's avatar
 
Dries committed
796
  }
Dries's avatar
 
Dries committed
797
}
Dries's avatar
 
Dries committed
798

799
/**
Dries's avatar
Dries committed
800 801 802 803 804 805 806
 * Return an array with default values for a book link.
 */
function _book_link_defaults($nid) {
  return array('original_bid' => 0, 'menu_name' => '', 'nid' => $nid, 'bid' => 0, 'router_path' => 'node/%', 'plid' => 0, 'mlid' => 0, 'has_children' => 0, 'weight' => 0, 'module' => 'book', 'options' => array());
}

/**
807
 * Process variables for book-navigation.tpl.php.
Dries's avatar
Dries committed
808
 *
809 810
 * The $variables array contains the following arguments:
 * - $book_link
811
 *
812
 * @see book-navigation.tpl.php
813
 */
814 815 816 817 818 819
function template_preprocess_book_navigation(&$variables) {
  $book_link = $variables['book_link'];

  // Provide extra variables for themers. Not needed by default.
  $variables['book_id'] = $book_link['bid'];
  $variables['book_title'] = check_plain($book_link['link_title']);
820
  $variables['book_url'] = 'node/' . $book_link['bid'];
821 822
  $variables['current_depth'] = $book_link['depth'];
  $variables['tree'] = '';
823

Dries's avatar
Dries committed
824
  if ($book_link['mlid']) {
825
    $variables['tree'] = book_children($book_link);
Dries's avatar
 
Dries committed
826

Dries's avatar
Dries committed
827
    if ($prev = book_prev($book_link)) {
828 829 830 831
      $prev_href = url($prev['href']);
      drupal_add_link(array('rel' => 'prev', 'href' => $prev_href));
      $variables['prev_url'] = $prev_href;
      $variables['prev_title'] = check_plain($prev['title']);
Dries's avatar
 
Dries committed
832
    }
833

Dries's avatar
Dries committed
834
    if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) {
835 836 837 838
      $parent_href = url($parent['href']);
      drupal_add_link(array('rel' => 'up', 'href' => $parent_href));
      $variables['parent_url'] = $parent_href;
      $variables['parent_title'] = check_plain($parent['title']);
Dries's avatar
 
Dries committed
839
    }
840

Dries's avatar
Dries committed
841
    if ($next = book_next($book_link)) {
842 843 844 845
      $next_href = url($next['href']);
      drupal_add_link(array('rel' => 'next', 'href' => $next_href));
      $variables['next_url'] = $next_href;
      $variables['next_title'] = check_plain($next['title']);
Dries's avatar
 
Dries committed
846
    }
847
  }
Dries's avatar
 
Dries committed
848

849 850 851 852 853 854 855 856 857 858 859
  $variables['has_links'] = FALSE;
  // Link variables to filter for values and set state of the flag variable.
  $links = array('prev_url', 'prev_title', 'parent_url', 'parent_title', 'next_url', 'next_title');
  foreach ($links as $link) {
    if (isset($variables[$link])) {
      // Flag when there is a value.
      $variables['has_links'] = TRUE;
    }
    else {
      // Set empty to prevent notices.
      $variables[$link] = '';
860
    }
Dries's avatar
 
Dries committed
861
  }
Dries's avatar
 
Dries committed
862
}
Dries's avatar
 
Dries committed
863

864
/**
Dries's avatar
Dries committed
865
 * A recursive helper function for book_toc().
866
 */
867
function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) {
Dries's avatar
Dries committed
868
  foreach ($tree as $data) {
869 870 871 872
    if ($data['link']['depth'] > $depth_limit) {
      // Don't iterate through any links on this level.
      break;
    }
873

Dries's avatar
Dries committed
874
    if (!in_array($data['link']['mlid'], $exclude)) {
875
      $toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
876
      if ($data['below']) {
877
        _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
878
      }
Dries's avatar
 
Dries committed
879 880 881 882
    }
  }
}

883
/**
Dries's avatar
Dries committed
884 885 886 887 888 889 890
 * Returns an array of book pages in table of contents order.
 *
 * @param $bid
 *   The ID of the book whose pages are to be listed.
 * @param $exclude
 *   Optional array of mlid values.  Any link whose mlid is in this array
 *   will be excluded (along with its children).
891 892
 * @param $depth_limit
 *   Any link deeper than this value will be excluded (along with its children).
Dries's avatar
Dries committed
893 894
 * @return
 *   An array of mlid, title pairs for use as options for selecting a book page.
895
 */
896
function book_toc($bid, $exclude = array(), $depth_limit) {
Dries's avatar
Dries committed
897
  $tree = menu_tree_all_data(book_menu_name($bid));
898
  $toc = array();
899
  _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit);
Dries's avatar
 
Dries committed
900

Dries's avatar
 
Dries committed
901 902 903
  return $toc;
}

904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923
/**
 * Process variables for book-export-html.tpl.php.
 *
 * The $variables array contains the following arguments:
 * - $title
 * - $contents
 * - $depth
 *
 * @see book-export-html.tpl.php
 */
function template_preprocess_book_export_html(&$variables) {
  global $base_url, $language;

  $variables['title'] = check_plain($variables['title']);
  $variables['base_url'] = $base_url;
  $variables['language'] = $language;
  $variables['language_rtl'] = (defined('LANGUAGE_RTL') && $language->direction == LANGUAGE_RTL) ? TRUE : FALSE;
  $variables['head'] = drupal_get_html_head();
}

924
/**
Dries's avatar
Dries committed
925
 * Traverse the book tree to build printable or exportable output.
926
 *
927
 * During the traversal, the $visit_func() callback is applied to each
Dries's avatar
Dries committed
928
 * node, and is called recursively for each child of the node (in weight,
929
 * title order).
930
 *
Dries's avatar
Dries committed
931 932
 * @param $tree
 *   A subtree of the book menu hierarchy, rooted at the current page.
933
 * @param $visit_func
Dries's avatar
Dries committed
934
 *   A function callback to be called upon visiting a node in the tree.
935
 * @return
Dries's avatar
Dries committed
936
 *   The output generated in visiting each node.
937
 */
Gábor Hojtsy's avatar