=== added file 'modules/book/book.js' --- modules/book/book.js 1970-01-01 00:00:00 +0000 +++ modules/book/book.js 2007-06-20 04:47:39 +0000 @@ -0,0 +1,21 @@ +// $Id$ + +if (Drupal.jsEnabled) { + $(document).ready(function () { + $('#edit-book-pick-book').hide(); + $('#edit-book-plid-wrapper select:disabled').slideUp("fast"); + $('#edit-book-book-id').bind('change', function() { + $.get(Drupal.settings.book.formCallback +'/'+ $('#'+ Drupal.settings.book.formId +' input[@name=form_build_id]').val() +'/'+ $('#edit-book-book-id').val(), {}, function(data) { + parsedData = Drupal.parseJson(data); + $('#edit-book-plid-wrapper').html(parsedData['book']); + $('#edit-book-plid-wrapper select:disabled').slideUp("fast"); + $('#edit-book-plid-wrapper select:enabled').slideDown("fast"); + }); + }); + // We have to start with the fieldset open so the click function can bind. + // Now we can mark it to be closed. + if (Drupal.settings.book.formId == "node-form") { + $('#edit-book-book-id').parents('fieldset.collapsible').addClass("collapsed"); + }; + }); +} === modified file 'modules/book/book.info' --- modules/book/book.info 2007-06-09 12:09:44 +0000 +++ modules/book/book.info 2007-06-20 04:47:39 +0000 @@ -1,6 +1,6 @@ ; $Id: book.info,v 1.4 2007/06/08 05:50:53 dries Exp $ name = Book -description = Allows users to collaboratively author a book. +description = Allows users to structure site pages in a hierarchy or outline. package = Core - optional version = VERSION core = 6.x === modified file 'modules/book/book.install' --- modules/book/book.install 2007-05-25 15:26:33 +0000 +++ modules/book/book.install 2007-06-20 04:53:40 +0000 @@ -13,6 +13,145 @@ function book_install() { * Implementation of hook_uninstall(). */ function book_uninstall() { + // Delete menu links. + db_query("DELETE FROM {menu_links} WHERE module = 'book'"); + menu_cache_clear_all(); // Remove tables. drupal_uninstall_schema('book'); } + + +function book_update_6000() { + $ret = array(); + + // Set up for a multi-part update. + if (!isset($_SESSION['book_update_6000'])) { + + $schema['book'] = array( + 'fields' => array( + 'mlid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'book_id' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + ), + 'indexes' => array( + 'nid' => array('nid'), + 'book_id' => array('book_id') + ), + 'primary key' => array('mlid'), + ); + + // Determine whether there are any existing nodes in the book hierarchy. + if (db_result(db_query("SELECT COUNT(*) FROM {book}"))) { + // Create the legacy node type + $book_node_type = array( + 'type' => 'book', + 'name' => t('Book page'), + 'module' => 'node', + 'description' => t("A static page. These posts (or any other type) may be added to a book outline to create a hierarchical structure for your site."), + 'custom' => TRUE, + 'modified' => TRUE, + 'locked' => FALSE, + ); + + $book_node_type = (object)_node_type_set_defaults($book_node_type); + node_type_save($book_node_type); + node_types_rebuild(); + menu_rebuild(); + + // Temporary table for the old book hierarchy; we'll discard revision info. + $schema['book_temp'] = array( + 'fields' => array( + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'parent' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + 'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny') + ), + 'indexes' => array( + 'parent' => array('parent') + ), + 'primary key' => array('nid'), + ); + + _drupal_initialize_schema('book', $schema); + + db_create_table($ret, $schema['book_temp']); + + // Insert each node in the old table into the temporary table. + $ret[] = update_sql("INSERT INTO {book_temp} (nid, parent, weight) SELECT b.nid, b.parent, b.weight FROM {book} b INNER JOIN {node} n on b.vid = n.vid"); + $ret[] = update_sql("DROP TABLE {book}"); + + db_create_table($ret, $schema['book']); + + $_SESSION['book_update_6000'] = array(); + $r = db_query("SELECT * from {book_temp} WHERE parent = 0"); + + // Collect all books - top-level nodes. + while ($a = db_fetch_array($r)) { + $_SESSION['book_update_6000'][] = $a; + } + $ret['#finished'] = FALSE; + return $ret; + } + else { + // No exising nodes in the hierarchy, so drop the table and re-create it. + $ret[] = update_sql("DROP TABLE {book}"); + _drupal_initialize_schema('book', $schema); + db_create_table($ret, $schema['book']); + return $ret; + } + } + else { + // Do the batched part of the update + $update_count = 50; // Update this many at a time + + while ($update_count && $_SESSION['book_update_6000']) { + // Get the last node off the stack. + $book = array_pop($_SESSION['book_update_6000']); + + $r = db_query("SELECT * FROM {book_temp} WHERE parent = %d", $book['nid']); + while ($a = db_fetch_array($r)) { + $_SESSION['book_update_6000'][] = $a; + } + + if ($book['parent']) { + // If its not a top level page, get its parent's mlid. + $book = array_merge($book, db_fetch_array(db_query("SELECT b.mlid AS plid, b.book_id FROM {book} b WHERE b.nid = %d", $book['parent']))); + } + else { + // There is not a parent - this is a new book. + $book['plid'] = 0; + $book['book_id'] = $book['nid']; + } + + $book += array( + 'module' => 'book', + 'link_path' => 'node/'. $book['nid'], + 'router_path' => 'node/%', + 'menu_name' => book_menu_name($book['book_id']), + ); + $book = array_merge($book, db_fetch_array(db_query("SELECT title AS link_title FROM {node} WHERE nid = %d", $book['nid']))); + + // Items with depth > MENU_MAX_DEPTH cannot be saved. + if (menu_link_save($book)) { + db_query("INSERT INTO {book} (mlid, nid, book_id) VALUES (%d, %d, %d)", $book['mlid'], $book['nid'], $book['book_id']); + } + else { + // The depth was greater then MENU_MAX_DEPTH, so attach it to the + // closest valid parent. + $book['plid'] = db_result(db_query("SELECT plid FROM {menu_links} WHERE mlid = %d", $book['plid'])); + if (menu_link_save($book)) { + db_query("INSERT INTO {book} (mlid, nid, book_id) VALUES (%d, %d, %d, '%s')", $book['mlid'], $book['nid'], $book['book_id']); + } + } + $update_count--; + } + $ret['#finished'] = FALSE; + } + + $ret['#finished'] = empty($_SESSION['book_update_6000']); + if ($ret['#finished']) { + $ret[] = update_sql("DROP TABLE {book_temp}"); + unset($_SESSION['book_update_6000']); + } + + return $ret; +} === modified file 'modules/book/book.module' --- modules/book/book.module 2007-06-11 17:37:11 +0000 +++ modules/book/book.module 2007-06-20 04:47:39 +0000 @@ -3,29 +3,16 @@ /** * @file - * Allows users to collaboratively author a book. + * Allows users to structure the pages of a site in a hierarchy or outline. */ /** - * Implementation of hook_node_info(). - */ -function book_node_info() { - return array( - 'book' => array( - 'name' => t('Book page'), - 'module' => 'book', - 'description' => t("A book is a collaborative writing effort: users can collaborate writing the pages of the book, positioning the pages in the right order, and reviewing or modifying pages previously written. So when you have some information to share or when you read a page of the book and you didn't like it, or if you think a certain page could have been written better, you can do something about it."), - ) - ); -} - -/** * Implementation of hook_theme() */ function book_theme() { return array( 'book_navigation' => array( - 'arguments' => array('node' => NULL), + 'arguments' => array('book_link' => NULL), ), 'book_export_html' => array( 'arguments' => array('title' => NULL, 'content' => NULL), @@ -33,6 +20,9 @@ function book_theme() { 'book_admin_table' => array( 'arguments' => array('form' => NULL), ), + 'book_title_link' => array( + 'arguments' => array('link' => NULL), + ), ); } @@ -40,50 +30,23 @@ function book_theme() { * Implementation of hook_perm(). */ function book_perm() { - return array('outline posts in books', 'create book pages', 'create new books', 'edit book pages', 'edit own book pages', 'see printer-friendly version'); -} - -/** - * Implementation of hook_access(). - */ -function book_access($op, $node) { - global $user; - - if ($op == 'create') { - // Only registered users can create book pages. Given the nature - // of the book module this is considered to be a good/safe idea. - return user_access('create book pages'); - } - - if ($op == 'update') { - // Only registered users can update book pages. Given the nature - // of the book module this is considered to be a good/safe idea. - // One can only update a book page if there are no suggested updates - // of that page waiting for approval. That is, only updates that - // don't overwrite the current or pending information are allowed. - - if (user_access('edit book pages') || ($node->uid == $user->uid && user_access('edit own book pages'))) { - return TRUE; - } - else { - // do nothing. node-access() will determine further access - } - } + return array('outline posts in books', 'create new books', 'see printer-friendly version'); } /** * Implementation of hook_link(). */ function book_link($type, $node = NULL, $teaser = FALSE) { - $links = array(); - if ($type == 'node' && isset($node->parent)) { + if ($type == 'node' && isset($node->book)) { if (!$teaser) { - if (book_access('create', $node) && $node->status == 1) { + $child_type = db_result(db_query(db_rewrite_sql("SELECT type FROM {node} n WHERE n.nid = %d"), $node->book['book_id'])); + if (node_access('create', $child_type) && $node->status == 1 && $node->book['depth'] < MENU_MAX_DEPTH) { $links['book_add_child'] = array( 'title' => t('Add child page'), - 'href' => "node/add/book/parent/$node->nid" + 'href' => "node/add/". str_replace('_', '-', $child_type), + 'query' => "parent=". $node->book['mlid'], ); } if (user_access('see printer-friendly version')) { @@ -95,7 +58,6 @@ function book_link($type, $node = NULL, } } } - return $links; } @@ -105,20 +67,17 @@ function book_link($type, $node = NULL, function book_menu() { $items['admin/content/book'] = array( 'title' => 'Books', - 'description' => "Manage site's books and orphaned book pages.", - 'page callback' => 'book_admin', - 'access arguments' => array('administer nodes'), - ); - $items['admin/content/book/list'] = array( - 'title' => 'List', - 'type' => MENU_DEFAULT_LOCAL_TASK, + 'description' => "Manage your site's book outlines.", + 'page callback' => 'book_admin_overview', + 'access arguments' => array('outline posts in books'), ); - $items['admin/content/book/orphan'] = array( - 'title' => 'Orphan pages', + $items['admin/content/book/%node'] = array( + 'title' => 'Re-order book pages and change titles', 'page callback' => 'drupal_get_form', - 'page arguments' => array('book_admin_orphan'), - 'type' => MENU_LOCAL_TASK, - 'weight' => 8, + 'page arguments' => array('book_admin_edit', 3), + 'access callback' => '_book_outline_access', + 'access arguments' => array(3), + 'type' => MENU_CALLBACK, ); $items['book'] = array( 'title' => 'Books', @@ -129,6 +88,7 @@ function book_menu() { $items['book/export/%/%'] = array( 'page callback' => 'book_export', 'page arguments' => array(2, 3), + 'access arguments' => array('see printer-friendly version'), 'type' => MENU_CALLBACK, ); $items['node/%node/outline'] = array( @@ -140,13 +100,17 @@ function book_menu() { 'type' => MENU_LOCAL_TASK, 'weight' => 2, ); - + $items['book-form-update/%/%'] = array( + 'page callback' => 'book_form_update', + 'page arguments' => array(1, 2), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); return $items; } function _book_outline_access($node) { - // Only add the outline-tab for non-book pages: - return user_access('outline posts in books') && $node && ($node->type != 'book'); + return user_access('outline posts in books') && node_access('view', $node); } function book_init() { @@ -166,338 +130,660 @@ function book_block($op = 'list', $delta return $block; } else if ($op == 'view') { - // Only display this block when the user is browsing a book: + // Only display this block when the user is browsing a book. if (arg(0) == 'node' && is_numeric(arg(1))) { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d'), arg(1)); - if (db_num_rows($result) > 0) { - $node = db_fetch_object($result); - - $path = book_location($node); - $path[] = $node; - - $expand = array(); - foreach ($path as $key => $node) { - $expand[] = $node->nid; + $node = node_load(arg(1)); + if (isset($node->book['book_id'])) { + $title = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $node->book['book_id'])); + if ($title) { + $block['subject'] = check_plain($title); + $tree = menu_tree_all_data($node->book['menu_name'], $node->book); + $output = ''; + foreach ($tree as $data) { + // Should be only one, but loop just in case. + $output .= theme('book_title_link', $data['link']); + $output .= ($data['below']) ? menu_tree_output($data['below']) : ''; + } + $block['content'] = $output; } - - $block['subject'] = check_plain($path[0]->title); - $block['content'] = book_tree($expand[0], 5, $expand); } } - return $block; } } /** - * Implementation of hook_insert(). + * Generate the HTML output for a link to a book title. + * + * @ingroup themeable */ -function book_insert($node) { - db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight); +function theme_book_title_link($link) { + $link['options']['attributes']['class'] = 'book-title'; + $url = l($link['title'], $link['href'], $link['options']); + $class = ($link['has_children'] ? 'expanded' : 'leaf') .' active-trail'; + return ''; } /** - * Implementation of hook_submit(). + * Implementation of hook_submit(). Handles node form submission. */ -function book_submit(&$form_values) { +function book_submit($form, &$form_state) { global $user; // Set default values for non-administrators. if (!user_access('administer nodes')) { - $form_values['revision'] = 1; - $form_values['uid'] = $user->uid; + $form_state['values']['revision'] = 1; + $form_state['values']['uid'] = $user->uid; + } +} + +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( + '#value' => '

'. t('This page is part of a book outline, and has other pages associated as child pages.') .'

', + '#weight' => -10, + ); } } /** - * Implementation of hook_form(). + * Returns an array of all books. */ -function book_form(&$node) { - $type = node_get_types('type', $node); - if (!empty($node->nid) && !$node->parent && !user_access('create new books')) { - $form['parent'] = array('#type' => 'value', '#value' => $node->parent); +function book_get_books() { + static $books; + + if (!isset($books)) { + $books = array(); + $result = db_query("SELECT DISTINCT(book_id) FROM {book}"); + $nids = array(); + while ($b = db_fetch_array($result)) { + $nids[] = $b['book_id']; + } + if ($nids) { + $result2 = db_query(db_rewrite_sql("SELECT n.type, 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")); + while ($b = db_fetch_array($result2)) { + $books[] = $b; + } + } } - else { - $form['parent'] = array('#type' => 'select', - '#title' => t('Parent'), - '#default_value' => (isset($node->parent) ? $node->parent : arg(4)), - '#options' => book_toc(isset($node->nid) ? $node->nid : 0), - '#weight' => -4, - '#description' => user_access('create new books') ? t('The parent section in which to place this page. Note that each page whose parent is <top-level> is an independent, top-level book.') : t('The parent that this page belongs in.'), - ); + return $books; +} + +/** + * Returns an array of node types that are the basis of books that the user can access. + */ +function book_node_types() { + static $types; + + if (!isset($types)) { + $types = array(); + foreach (book_get_books() as $b) { + $types[] = $b['type']; + } + $types = array_unique($types); } + return $types; +} - $form['title'] = array('#type' => 'textfield', - '#title' => check_plain($type->title_label), - '#required' => TRUE, - '#default_value' => $node->title, - '#weight' => -5, - ); - - $form['body_field'] = node_body_field($node, $type->body_label, 1); - - if (user_access('administer nodes')) { - $form['weight'] = array('#type' => 'weight', - '#title' => t('Weight'), - '#default_value' => isset($node->weight) ? $node->weight : 0, - '#delta' => 15, - '#weight' => 5, - '#description' => t('Pages at a given level are ordered first by weight and then by title.'), - ); + +/** + * AJAX callback to repalce the book parent select. + * + * @param $build_id + * The form's build_id. + * @param $book_id + * A book_id form from among those on the form. + * @return + * Prints the replacement HTML + */ +function book_form_update($build_id, $book_id) { + + $cid = 'form_'. $build_id; + $cache = cache_get($cid, 'cache_form'); + if ($cache) { + $form = $cache->data; + + // Validate the book_id. + if (isset($form['book']['book_id']['#options'][$book_id])) { + $book_link = $form['#node']->book; + $book_link['book_id'] = $book_id; + // Get the new options and update the cache. + $form['book']['plid'] = _book_parent_select($book_link); + $expire = max(ini_get('session.cookie_lifetime'), 86400); + cache_set($cid, $form, 'cache_form', $expire); + + $form_state = array(); + $form['#post'] = array(); + $form = form_builder($form['form_id']['#value'] , $form, $form_state); + $output = drupal_render($form['book']['plid']); + print(drupal_to_js(array('book' => $output))); + } + } + exit(); +} + +/** + * Implementation of hook_form_alter(). + */ +function book_form_alter(&$form, $form_state, $form_id) { + + if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) { + // Add elements to the node form + $node = $form['#node']; + + $access = user_access('outline posts in books') || user_access('create new books'); + if (!$access) { + if ((!empty($node->book['mlid']) && !empty($node->nid)) || in_array($node->type, book_node_types())) { + // Already in the book hierarchy or there are existing books based on this node type.. + $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)'), + '#submit' => array('book_pick_book_submit'), + '#weight' => 20, + ); + $form['#submit'][] = 'book_submit'; + } + } +} + +/** + * Like a node preview, to update options in the parent select. + */ +function book_pick_book_submit($form, &$form_state) { + // We do not want to execute button level handlers, we want the form level + // handlers to go in and change the submitted values. + unset($form_state['submit_handlers']); + form_execute_handlers('submit', $form, $form_state); + $form_state['rebuild'] = TRUE; + $form_state['node'] = $form_state['values']; +} + +function _book_parent_select($book_link) { + // Offer a drop-down to choose a different parent page. + $form = array('#type' => 'select', '#options' => array(-1 => ''), '#disabled' => TRUE); + + if ($book_link['nid'] === $book_link['book_id']) { + // This is a book - at the top level. + $form['#title'] = ''. t('This is the top-level page in this book') .''; + } + elseif (!$book_link['book_id']) { + $form['#title'] = ''. t('No book selected') .''; } else { - // If a regular user updates a book page, we preserve the node weight; otherwise - // we use 0 as the default for new pages - $form['weight'] = array( - '#type' => 'value', - '#value' => isset($node->weight) ? $node->weight : 0, + $form = array( + '#type' => 'select', + '#title' => t('Parent item'), + '#default_value' => $book_link['plid'], + '#description' => t('The parent page in the book.'), + '#options' => book_toc($book_link['book_id'], array($book_link['mlid'])), ); } - return $form; } -/** - * Implementation of function book_outline() - * Handles all book outline operations. - */ -function book_outline($form_state, $node) { - $form['parent'] = array('#type' => 'select', - '#title' => t('Parent'), - '#default_value' => isset($node->parent) ? $node->parent : 0, - '#options' => book_toc($node->nid), - '#description' => t('The parent page in the book.'), +function _book_add_form_elements(&$form, $node) { + $settings['book']['formCallback'] = url('book-form-update' , array()); + $settings['book']['formId'] = $form['#id']; + drupal_add_js($settings, 'setting'); + drupal_add_js(drupal_get_path('module', 'book'). '/book.js'); + + // Need this for AJAX. + $form['#cache'] = TRUE; + + $form['book'] = array( + '#type' => 'fieldset', + '#title' => t('Book outline options'), + '#weight' => -1, + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#tree' => TRUE, ); - $form['weight'] = array('#type' => 'weight', + foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'orig_book_id') as $key) { + $form['book'][$key] = array( + '#type' => 'value', + '#value' => $node->book[$key], + ); + } + + $form['book']['plid'] = _book_parent_select($node->book); + + $form['book']['weight'] = array( + '#type' => 'weight', '#title' => t('Weight'), - '#default_value' => isset($node->weight) ? $node->weight : 0, + '#default_value' => $node->book['weight'], '#delta' => 15, + '#weight' => 1, '#description' => t('Pages at a given level are ordered first by weight and then by title.'), ); - $form['log'] = array('#type' => 'textarea', - '#title' => t('Log message'), - '#description' => t('An explanation to help other authors understand your motivations to put this post into the book.'), + $options = array(); + if (!$node->book['mlid']) { + $options[0] = '<'. t('none') .'>'; + } + + foreach (book_get_books() as $book) { + if ($book['type'] == $node->type || user_access('outline posts in books') || $node->book['orig_book_id'] == $book['nid']) { + $options[$book['nid']] = check_plain($book['link_title']); + } + } + $nid = isset($node->nid) ? $node->nid : 'new'; + if (user_access('create new books') && ($nid == 'new' || ($nid != $node->book['orig_book_id']))) { + // The node can become a new book, if it is not one already. + $options = array($nid => '<'. t('create a new book') .'>') + $options; + } + + if ($node->book['book_id']) { + $title = t('Select a different book to move this post to'); + } + else { + $title = t('Select a book to add this post to'); + } + // Add a drop-down to select the destination book. + $form['book']['book_id'] = array( + '#type' => 'select', + '#title' => $title, + '#default_value' => (isset($node->book['book_id']) ? $node->book['book_id'] : 1), + '#options' => $options, + '#access' => (bool)$options, + '#description' => t('Select the book you wish to add/move your page to.'), + '#weight' => 15, ); +} - $form['nid'] = array('#type' => 'value', '#value' => isset($node->nid) ? $node->nid : 0); - if (isset($node->parent)) { - $form['update'] = array('#type' => 'submit', - '#value' => t('Update book outline'), - ); - $form['remove'] = array('#type' => 'submit', - '#value' => t('Remove from book outline'), - ); +/** + * Build the form to handle all book outline operations. + */ +function book_outline(&$form_state, $node) { + + if (!isset($node->book)) { + // The node is not part of any book yet - set default options. + $node->book = _book_link_defaults($node->nid); + } + else { + $node->book['orig_book_id'] = $node->book['book_id']; + } + $form['#node'] = $node; + $form['#id'] = 'book-outline'; + _book_add_form_elements($form, $node); + + $form['book']['#collapsible'] = FALSE; + + $form['book']['update'] = array( + '#type' => 'submit', + '#value' => t('Update book outline'), + '#weight' => 5, + ); + + $form['book']['remove'] = array( + '#type' => 'submit', + '#value' => t('Remove from book outline'), + '#access' => $node->nid != $node->book['book_id'] && $node->book['book_id'], + '#weight' => 10, + ); + + if ($node->book['book_id']) { + $action = t('Move to a different book'); } else { - $form['add'] = array('#type' => 'submit', '#value' => t('Add to book outline')); + $action = t('Add to book outline'); } + $form['book']['pick-book'] = array('#type' => 'submit', '#value' => $action, '#weight' => 20); + drupal_set_title(check_plain($node->title)); return $form; } +function _book_update_outline(&$node) { + if (empty($node->book['book_id'])) { + return FALSE; + } + $new = empty($node->book['mlid']); + + $node->book['link_path'] = 'node/'. $node->nid; + $node->book['link_title'] = $node->title; + + if ($node->book['book_id'] == $node->nid) { + $node->book['plid'] = 0; + } + else { + // Check in case parent is not is this book - 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['book_id'] != $node->book['book_id']) { + $node->book['plid'] = db_result(db_query("SELECT mlid FROM {book} WHERE nid = %d", $node->book['book_id'])); + } + } + if (menu_link_save($node->book)) { + if ($new) { + // Insert new. + db_query("INSERT INTO {book} (nid, mlid, book_id) VALUES (%d, %d, %d)", $node->nid, $node->book['mlid'], $node->book['book_id']); + } + else { + if ($node->book['book_id'] != db_result(db_query("SELECT book_id FROM {book} WHERE nid = %d", $node->nid))) { + // Update the book_id for this page and all children. + book_update_book_id($node->book); + } + } + return TRUE; + } + return FALSE; +} + /** * Handles book outline form submissions. */ function book_outline_submit($form, &$form_state) { - $op = $form_state['values']['op']; - $node = node_load($form_state['values']['nid']); + $node = $form['#node']; + $form_state['redirect'] = "node/". $node->nid; + $link = $form_state['values']['book']; + if (!$link['book_id']) { + return; + } + + $link['menu_name'] = book_menu_name($link['book_id']); - switch ($op) { + switch ($form_state['values']['op']) { case t('Add to book outline'): - db_query('INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)', $node->nid, $node->vid, $form_state['values']['parent'], $form_state['values']['weight']); - db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_state['values']['log'], $node->vid); - drupal_set_message(t('The post has been added to the book.')); + case t('Move to a different book'): + if ($link['book_id'] != $node->book['book_id']) { + // Only act if the destination book is different from the current book. + $node->book = $link; + if (_book_update_outline($node)) { + if ($node->book['plid']) { + drupal_set_message(t('The post has been added to the book. You may now position it relative to other pages.')); + $form_state['redirect'] = "node/". $node->nid ."/outline"; + } + else { + drupal_set_message(t('The post is now a separate book.')); + } + } + else { + drupal_set_message(t('There was an error adding the post to the book.')); + } + } + else { + drupal_set_message(t('No changes were made')); + } break; case t('Update book outline'): - db_query('UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d', $form_state['values']['parent'], $form_state['values']['weight'], $node->vid); - db_query("UPDATE {node_revisions} SET log = '%s' WHERE vid = %d", $form_state['values']['log'], $node->vid); - drupal_set_message(t('The book outline has been updated.')); + $node->book = $link; + if (_book_update_outline($node)) { + drupal_set_message(t('The book outline has been updated.')); + } break; case t('Remove from book outline'): - db_query('DELETE FROM {book} WHERE nid = %d', $node->nid); - drupal_set_message(t('The post has been removed from the book.')); + if ($node->nid != $node->book['book_id']) { + // Only allowed when this is not a book (top-level page). + menu_link_delete($node->book['mlid']); + db_query('DELETE FROM {book} WHERE nid = %d', $node->nid); + drupal_set_message(t('The post has been removed from the book.')); + } break; } - $form_state['redirect'] = "node/$node->nid"; - return; } /** - * Given a node, this function returns an array of 'book node' objects - * representing the path in the book tree from the root to the - * parent of the given node. - * - * @param $node - * A book node object for which to compute the path. - * - * @return - * An array of book node objects representing the path nodes root to - * parent of the given node. Returns an empty array if the node does - * not exist or is not part of a book hierarchy. + * Update the book_id for a page and its children when we move it to a new book. */ -function book_location($node, $nodes = array()) { - $parent = db_fetch_object(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.nid = %d'), $node->parent)); - if (isset($parent->title)) { - $nodes = book_location($parent, $nodes); - $nodes[] = $parent; +function book_update_book_id($link) { + + for ($i = 1; $i <= MENU_MAX_DEPTH && $link["p$i"]; $i++) { + $match[] = "p$i = %d"; + $args[] = $link["p$i"]; + } + $result = db_query("SELECT mlid FROM {menu_links} WHERE ". implode(' AND ', $match), $args); + + $mlids = array(); + while ($a = db_fetch_array($result)) { + $mlids[] = $a['mlid']; + } + if ($mlids) { + db_query("UPDATE {book} SET book_id = %d WHERE mlid IN (". implode(',', $mlids) .")", $link['book_id']); } - return $nodes; } /** - * Given a node, this function returns an array of 'book node' objects - * representing the path in the book tree from the given node down to - * the last sibling of it. - * - * @param $node - * A book node object where the path starts. + * Get the book menu tree for a page, and return it as a linear array. * + * @param $book_link + * A menu link that is part of the book hierarchy. * @return - * An array of book node objects representing the path nodes from the - * given node. Returns an empty array if the node does not exist or - * is not part of a book hierarchy or there are no siblings. - */ -function book_location_down($node, $nodes = array()) { - $last_direct_child = db_fetch_object(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 AND b.parent = %d ORDER BY b.weight DESC, n.title DESC'), $node->nid)); - if ($last_direct_child) { - $nodes[] = $last_direct_child; - $nodes = book_location_down($last_direct_child, $nodes); + * 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. + */ +function book_get_flat_menu($book_link) { + static $flat = array(); + + if (!isset($flat[$book_link['mlid']])) { + $tree = menu_tree_all_data($book_link['menu_name'], $book_link); + $flat[$book_link['mlid']] = array(); + _book_flatten_menu($tree, $flat[$book_link['mlid']]); + } + return $flat[$book_link['mlid']]; +} + +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); + } + } } - return $nodes; } /** - * Fetches the node object of the previous page of the book. + * Fetches the menu link for the previous page of the book. */ -function book_prev($node) { - // If the parent is zero, we are at the start of a book so there is no previous. - if ($node->parent == 0) { +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; } - - // Previous on the same level: - $direct_above = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = %d AND n.status = 1 AND (b.weight < %d OR (b.weight = %d AND n.title < '%s')) ORDER BY b.weight DESC, n.title DESC"), $node->parent, $node->weight, $node->weight, $node->title)); - if ($direct_above) { - // Get last leaf of $above. - $path = book_location_down($direct_above); - - return $path ? (count($path) > 0 ? array_pop($path) : NULL) : $direct_above; - } - else { - // Direct parent: - $prev = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d AND n.status = 1'), $node->parent)); + $flat = book_get_flat_menu($book_link); + $curr = NULL; + do { + $prev = $curr; + list($key, $curr) = each($flat); + } while ($key && $key != $book_link['mlid']); + if ($key == $book_link['mlid']) { return $prev; } } /** - * Fetches the node object of the next page of the book. + * Fetches the menu link for the next page of the book. */ -function book_next($node) { - // get first direct child - $child = db_fetch_object(db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = %d AND n.status = 1 ORDER BY b.weight ASC, n.title ASC'), $node->nid)); - if ($child) { - return $child; +function book_next($book_link) { + $flat = book_get_flat_menu($book_link); + do { + list($key, $curr) = each($flat); + } while ($key && $key != $book_link['mlid']); + if ($key == $book_link['mlid']) { + return current($flat); } +} - // No direct child: get next for this level or any parent in this book. - $path = book_location($node); // Path to top-level node including this one. - $path[] = $node; +/** + * Format the menu links for the child pages of the current page. + */ +function book_children($book_link) { + $flat = book_get_flat_menu($book_link); + + $children = array(); - while (($leaf = array_pop($path)) && count($path)) { - $next = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = %d AND n.status = 1 AND (b.weight > %d OR (b.weight = %d AND n.title > '%s')) ORDER BY b.weight ASC, n.title ASC"), $leaf->parent, $leaf->weight, $leaf->weight, $leaf->title)); - if ($next) { - return $next; + if ($book_link['has_children']) { + // Walk through the array until we find the current page. + do { + $link = array_shift($flat); + } while ($link && ($link['mlid'] != $book_link['mlid'])); + // 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; } } + return $children ? menu_tree_output($children) : ''; +} + +/** + * Generate the corresponding menu name from a book ID. + */ +function book_menu_name($book_id) { + return 'book-toc-'. $book_id; } /** - * Returns the content of a given node. If $teaser if TRUE, returns - * the teaser rather than full content. Displays the most recently - * approved revision of a node (if any) unless we have to display this - * page in the context of the moderation queue. + * Build an active trail to show in the breadcrumb. */ -function book_content($node, $teaser = FALSE) { - // Return the page body. - return node_prepare($node, $teaser); +function book_build_active_trail($book_link) { + static $trail; + + if (!isset($trail)) { + $trail = array(); + $trail[] = array('title' => t('Home'), 'href' => '', 'options' => array()); + + $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); + } + } + } + return $trail; } /** * Implementation of hook_nodeapi(). * - * Appends book navigation to all nodes in the book. + * Appends book navigation to all nodes in the book, and handles book outline + * insertions and updates via the node form.. */ function book_nodeapi(&$node, $op, $teaser, $page) { switch ($op) { case 'load': - return db_fetch_array(db_query('SELECT parent, weight FROM {book} WHERE vid = %d', $node->vid)); + // 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)); + if ($info['book']) { + $info['book']['href'] = $info['book']['link_path']; + $info['book']['title'] = $info['book']['link_title']; + $info['book']['options'] = unserialize($info['book']['options']); + return $info; + } break; case 'view': if (!$teaser) { - if (isset($node->parent)) { - $path = book_location($node); - // Construct the breadcrumb: - $node->breadcrumb = array(); // Overwrite the trail with a book trail. - foreach ($path as $level) { - $node->breadcrumb[] = array('path' => 'node/'. $level->nid, 'title' => $level->title); - } - $node->breadcrumb[] = array('path' => 'node/'. $node->nid); + if (isset($node->book)) { $node->content['book_navigation'] = array( - '#value' => theme('book_navigation', $node), + '#value' => theme('book_navigation', $node->book), '#weight' => 100, ); if ($page) { - menu_set_location($node->breadcrumb); + menu_set_active_trail(book_build_active_trail($node->book)); + menu_set_active_menu_name($node->book['menu_name']); } } } break; + case 'insert': case 'update': - if (isset($node->parent)) { - if (!empty($node->revision)) { - db_query("INSERT INTO {book} (nid, vid, parent, weight) VALUES (%d, %d, %d, %d)", $node->nid, $node->vid, $node->parent, $node->weight); - } - else { - db_query("UPDATE {book} SET parent = %d, weight = %d WHERE vid = %d", $node->parent, $node->weight, $node->vid); + if (isset($node->book) && $node->book['book_id']) { + if ($node->book['book_id'] == 'new') { + // New nodes that are their own book. + $node->book['book_id'] = $node->nid; } + $node->book['menu_name'] = book_menu_name($node->book['book_id']); + _book_update_outline($node); } break; - case 'delete revision': - db_query('DELETE FROM {book} WHERE vid = %d', $node->vid); - break; case 'delete': - db_query('DELETE FROM {book} WHERE nid = %d', $node->nid); + if (isset($node->book)) { + // TODO handle the case where a book is deleted. + menu_link_delete($node->book['mlid']); + db_query('DELETE FROM {book} WHERE mlid = %d', $node->book['mlid']); + } + break; + case 'prepare': + // Prepare defaults for the add/edit form. + if (empty($node->book)) { + $node->book = array(); + if (empty($node->nid) && isset($_GET['parent']) && is_numeric($_GET['parent'])) { + // Handle "Add child page" links: + $parent = book_link_load($_GET['parent']); + if ($parent && $parent['access']) { + // Already checked access, don't need rewrite sql. + $child_type = db_result(db_query("SELECT type FROM {node} n WHERE n.nid = %d", $parent['book_id'])); + // Restrict adding via this link to nodes that match the type of the top node. + if ($child_type == $node->type || user_access('outline posts in books')) { + $node->book['book_id'] = $parent['book_id']; + $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 { + $node->book['orig_book_id'] = $node->book['book_id']; + } break; } } +function _book_link_defaults($nid) { + return array('orig_book_id' => 0, 'menu_name' => '', 'nid' => $nid, 'book_id' => 0, 'router_path' => 'node/%', 'plid' => 0, 'mlid' => 0, 'has_children' => 0, 'weight' => 0, 'module' => 'book', 'options' => array()); +} + /** - * Prepares the links to children (TOC) and forward/backward - * navigation for a node presented as a book page. + * Prepares the links to the children of the page and the previous/up/next + * navigation for a node in the book outline. * * @ingroup themeable */ -function theme_book_navigation($node) { +function theme_book_navigation($book_link) { $output = ''; $links = ''; - if ($node->nid) { - $tree = book_tree($node->nid); + if ($book_link['mlid']) { + $tree = book_children($book_link); - if ($prev = book_prev($node)) { - drupal_add_link(array('rel' => 'prev', 'href' => url('node/'. $prev->nid))); - $links .= l(t('‹ ') . $prev->title, 'node/'. $prev->nid, array('class' => 'page-previous', 'title' => t('Go to previous page'))); - } - if ($node->parent) { - drupal_add_link(array('rel' => 'up', 'href' => url('node/'. $node->parent))); - $links .= l(t('up'), 'node/'. $node->parent, array('class' => 'page-up', 'title' => t('Go to parent page'))); - } - if ($next = book_next($node)) { - drupal_add_link(array('rel' => 'next', 'href' => url('node/'. $next->nid))); - $links .= l($next->title . t(' ›'), 'node/'. $next->nid, array('class' => 'page-next', 'title' => t('Go to next page'))); + if ($prev = book_prev($book_link)) { + drupal_add_link(array('rel' => 'prev', 'href' => url($prev['href']))); + $links .= l(t('‹ ') . $prev['title'], $prev['href'], array('attributes' => array('class' => 'page-previous', 'title' => t('Go to previous page')))); + } + if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) { + drupal_add_link(array('rel' => 'up', 'href' => url($parent['href']))); + $links .= l(t('up'), $parent['href'], array('attributes' => array('class' => 'page-up', 'title' => t('Go to parent page')))); + } + if ($next = book_next($book_link)) { + drupal_add_link(array('rel' => 'next', 'href' => url($next['href']))); + $links .= l($next['title'] . t(' ›'), $next['href'], array('attributes' => array('class' => 'page-next', 'title' => t('Go to next page')))); } if (isset($tree) || isset($links)) { @@ -518,105 +804,44 @@ function theme_book_navigation($node) { /** * This is a helper function for book_toc(). */ -function book_toc_recurse($nid, $indent, $toc, $children, $exclude) { - if (!empty($children[$nid])) { - foreach ($children[$nid] as $foo => $node) { - if (!$exclude || $exclude != $node->nid) { - $toc[$node->nid] = $indent .' '. $node->title; - $toc = book_toc_recurse($node->nid, $indent .'--', $toc, $children, $exclude); +function _book_toc_recurse($tree, $indent, &$toc, $exclude) { + foreach ($tree as $data) { + if (!in_array($data['link']['mlid'], $exclude)) { + $toc[$data['link']['mlid']] = $indent .' '. truncate_utf8($data['link']['title'], 30, TRUE, TRUE) ; + if ($data['below'] && $data['link']['depth'] < MENU_MAX_DEPTH - 1) { + _book_toc_recurse($data['below'], $indent .'--', $toc, $exclude); } } } - - return $toc; } /** - * Returns an array of titles and nid entries of book pages in table of contents order. + * Returns an array of titles and mlid entries of book pages in table of + * contents order for use in selecting a book page. + * + * @param $book_id + * 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). + * @return + * An array of mlid, title pairs. */ -function book_toc($exclude = 0) { - $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')); - - $children = array(); - while ($node = db_fetch_object($result)) { - if (empty($children[$node->parent])) { - $children[$node->parent] = array(); - } - $children[$node->parent][] = $node; - } +function book_toc($book_id, $exclude = array()) { + $tree = menu_tree_all_data(book_menu_name($book_id)); $toc = array(); - // If the user has permission to create new books, add the top-level book page to the menu; - if (user_access('create new books')) { - $toc[0] = '<'. t('top-level') .'>'; - } - - $toc = book_toc_recurse(0, '', $toc, $children, $exclude); + _book_toc_recurse($tree, '', $toc, $exclude); return $toc; } /** - * This is a helper function for book_tree() - */ -function book_tree_recurse($nid, $depth, $children, $unfold = array()) { - $output = ''; - if ($depth > 0) { - if (isset($children[$nid])) { - foreach ($children[$nid] as $foo => $node) { - if (in_array($node->nid, $unfold)) { - if ($tree = book_tree_recurse($node->nid, $depth - 1, $children, $unfold)) { - $output .= '
  • '; - $output .= l($node->title, 'node/'. $node->nid); - $output .= ''; - $output .= '
  • '; - } - else { - $output .= '
  • '. l($node->title, 'node/'. $node->nid) .'
  • '; - } - } - else { - if ($tree = book_tree_recurse($node->nid, 1, $children)) { - $output .= ''; - } - else { - $output .= '
  • '. l($node->title, 'node/'. $node->nid) .'
  • '; - } - } - } - } - } - - return $output; -} - -/** - * Returns an HTML nested list (wrapped in a menu-class div) representing the book nodes - * as a tree. - */ -function book_tree($parent = 0, $depth = 3, $unfold = array()) { - $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)) { - $list = isset($children[$node->parent]) ? $children[$node->parent] : array(); - $list[] = $node; - $children[$node->parent] = $list; - } - - if ($tree = book_tree_recurse($parent, $depth, $children, $unfold)) { - return ''; - } -} - -/** * Menu callback; prints a listing of all books. */ function book_render() { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = 0 AND n.status = 1 ORDER BY b.weight, n.title')); - - $books = array(); - while ($node = db_fetch_object($result)) { - $books[] = l($node->title, 'node/'. $node->nid); + foreach (book_get_books() as $b) { + $books[] = l($b['link_title'], $b['link_path']); } return theme('item_list', $books); @@ -631,26 +856,27 @@ function book_render() { * given output type. So, e.g., a type of 'html' results in a call to * the function book_export_html(). * - * @param type - * - a string encoding the type of output requested. - * The following types are currently supported in book module - * html: HTML (printer friendly output) - * Other types are supported in contributed modules. - * @param nid - * - an integer representing the node id (nid) of the node to export + * @param $type + * A string encoding the type of output requested. The following + * types are currently supported in book module: + * + * - html: HTML (printer friendly output) * + * Other types may be supported in contributed modules. + * @param $nid + * An integer representing the node id (nid) of the node to export + * @return + * A string representing the node and its children in the book hierarchy + * in a format determined by the $type parameter. */ function book_export($type, $nid) { + $type = drupal_strtolower($type); - $node_result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.nid = %d'), $nid); - if (db_num_rows($node_result) > 0) { - $node = db_fetch_object($node_result); - } - $depth = count(book_location($node)) + 1; + $export_function = 'book_export_'. $type; if (function_exists($export_function)) { - print call_user_func($export_function, $nid, $depth); + print call_user_func($export_function, $nid); } else { drupal_set_message(t('Unknown export format.')); @@ -669,25 +895,27 @@ function book_export($type, $nid) { * sections, no matter their depth relative to the node selected to be * exported as printer-friendly HTML. * - * @param nid - * - an integer representing the node id (nid) of the node to export - * @param depth - * - an integer giving the depth in the book hierarchy of the node - * which is to be exported - * + * @param $nid + * An integer representing the node id (nid) of the node to export. * @return - * - string containing HTML representing the node and its children in - * the book hierarchy + * A string containing HTML representing the node and its children in + * the book hierarchy. */ -function book_export_html($nid, $depth) { +function book_export_html($nid) { if (user_access('see printer-friendly version')) { + $content = ''; $node = node_load($nid); - for ($i = 1; $i < $depth; $i++) { - $content .= "
    \n"; - } - $content .= book_recurse($nid, $depth, 'book_node_visitor_html_pre', 'book_node_visitor_html_post'); - for ($i = 1; $i < $depth; $i++) { - $content .= "
    \n"; + if (isset($node->book)) { + $depth = $node->book['depth']; + for ($i = 1; $i < $depth; $i++) { + $content .= "
    \n"; + } + $tree = book_menu_subtree_data($node->book); + $content .= book_export_traverse($tree, 'book_node_visitor_html_pre', 'book_node_visitor_html_post'); + + for ($i = 1; $i < $depth; $i++) { + $content .= "
    \n"; + } } return theme('book_export_html', check_plain($node->title), $content); } @@ -709,7 +937,7 @@ function theme_book_export_html($title, $html .= ''; $html .= ''."\n"; $html .= "\n"; @@ -725,49 +953,44 @@ function theme_book_export_html($title, * * @todo This is duplicitous with node_build_content(). * - * @param nid - * - the node id (nid) of the root node of the book hierarchy. - * @param depth - * - the depth of the given node in the book hierarchy. - * @param visit_pre - * - a function callback to be called upon visiting a node in the tree - * @param visit_post - * - a function callback to be called after visiting a node in the tree, - * but before recursively visiting children. + * @param $tree + * A subtree of the book menu hierarchy, rooted at the current page. + * @param $visit_pre + * A function callback to be called upon visiting a node in the tree + * @param $visit_post + * A function callback to be called after visiting a node in the tree, + * but before recursively visiting children. * @return - * - the output generated in visiting each node + * The output generated in visiting each node */ -function book_recurse($nid = 0, $depth = 1, $visit_pre, $visit_post) { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 AND n.nid = %d ORDER BY b.weight, n.title'), $nid); - while ($page = db_fetch_object($result)) { - // Load the node: - $node = node_load($page->nid); +function book_export_traverse($tree, $visit_pre, $visit_post) { + + foreach ($tree as $data) { + // Note- access checking is already performed when building the tree + $node = node_load($data['link']['nid'], FALSE); if ($node) { + $depth = $node->book['depth']; if (function_exists($visit_pre)) { - $output .= call_user_func($visit_pre, $node, $depth, $nid); + $output = call_user_func($visit_pre, $node, $depth); } else { - $output .= book_node_visitor_html_pre($node, $depth, $nid); + $output = book_node_visitor_html_pre($node, $depth); } - - $children = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE n.status = 1 AND b.parent = %d ORDER BY b.weight, n.title'), $node->nid); - while ($childpage = db_fetch_object($children)) { - $childnode = node_load($childpage->nid); - if ($childnode->nid != $node->nid) { - $output .= book_recurse($childnode->nid, $depth + 1, $visit_pre, $visit_post); - } + + if ($data['below']) { + $output .= book_export_traverse($data['below'], $visit_pre, $visit_post); } + if (function_exists($visit_post)) { $output .= call_user_func($visit_post, $node, $depth); } else { - # default + // default $output .= book_node_visitor_html_post($node, $depth); } } } - return $output; } @@ -776,17 +999,14 @@ function book_recurse($nid = 0, $depth = * is a 'pre-node' visitor function for book_recurse(). * * @param $node - * - the node to generate output for. + * The node to generate output for. * @param $depth - * - the depth of the given node in the hierarchy. This - * is used only for generating output. - * @param $nid - * - the node id (nid) of the given node. This + * The depth of the given node in the hierarchy. This * is used only for generating output. * @return - * - the HTML generated for the given node. + * The HTML generated for the given node. */ -function book_node_visitor_html_pre($node, $depth, $nid) { +function book_node_visitor_html_pre($node, $depth) { // Remove the delimiter (if any) that separates the teaser from the body. $node->body = str_replace('', '', $node->body); @@ -802,7 +1022,7 @@ function book_node_visitor_html_pre($nod // Allow modules to make their own additions to the node. node_invoke_nodeapi($node, 'print'); - $output .= "
    nid ."\" class=\"section-$depth\">\n"; + $output = "
    nid ."\" class=\"section-$depth\">\n"; $output .= "

    ". check_plain($node->title) ."

    \n"; $output .= drupal_render($node->content); @@ -811,65 +1031,65 @@ function book_node_visitor_html_pre($nod /** * Finishes up generation of printer-friendly HTML after visiting a - * node. This function is a 'post-node' visitor function for - * book_recurse(). + * node. This function is a 'post-node' visitor function for + * book_export_traverse(). */ function book_node_visitor_html_post($node, $depth) { return "
    \n"; } -function _book_admin_table($nodes = array()) { +function _book_admin_table($node) { $form = array( '#theme' => 'book_admin_table', '#tree' => TRUE, ); - foreach ($nodes as $node) { - $form = array_merge($form, _book_admin_table_tree($node, 0)); - } - + $tree = book_menu_subtree_data($node->book); + _book_admin_table_tree($tree, $form); return $form; } -function _book_admin_table_tree($node, $depth) { - $form = array(); - - $form[] = array( - 'nid' => array('#type' => 'value', '#value' => $node->nid), - 'depth' => array('#type' => 'value', '#value' => $depth), - 'title' => array( - '#type' => 'textfield', - '#default_value' => $node->title, - '#maxlength' => 255, - ), - 'weight' => array( - '#type' => 'weight', - '#default_value' => $node->weight, - '#delta' => 15, - ), - ); - - $children = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = %d ORDER BY b.weight, n.title'), $node->nid); - while ($child = db_fetch_object($children)) { - $form = array_merge($form, _book_admin_table_tree(node_load($child->nid), $depth + 1)); +function _book_admin_table_tree($tree, &$form) { + foreach ($tree as $data) { + $form[] = array( + 'nid' => array('#type' => 'value', '#value' => $data['link']['nid']), + 'depth' => array('#type' => 'value', '#value' => $data['link']['depth']), + 'href' => array('#type' => 'value', '#value' => $data['link']['href']), + 'title' => array( + '#type' => 'textfield', + '#default_value' => $data['link']['link_title'], + '#maxlength' => 255, + ), + 'weight' => array( + '#type' => 'weight', + '#default_value' => ($data['link']['weight'] - 50000), + '#delta' => 15, + ), + ); + if ($data['below']) { + _book_admin_table_tree($data['below'], &$form); + } } return $form; } function theme_book_admin_table($form) { + $header = array(t('Title'), t('Weight'), array('data' => t('Operations'), 'colspan' => '3')); $rows = array(); + $destination = drupal_get_destination(); + $access = user_access('administer nodes'); foreach (element_children($form) as $key) { $nid = $form[$key]['nid']['#value']; - $pid = $form[0]['nid']['#value']; + $href = $form[$key]['href']['#value']; $rows[] = array( '
    '. drupal_render($form[$key]['title']) .'
    ', drupal_render($form[$key]['weight']), - l(t('view'), 'node/'. $nid), - l(t('edit'), 'node/'. $nid .'/edit'), - l(t('delete'), 'node/'. $nid .'/delete', NULL, 'destination=admin/content/book'. (arg(3) == 'orphan' ? '/orphan' : '') . ($pid != $nid ? '/'. $pid : '')) + l(t('view'), $href), + $access ? l(t('edit'), 'node/'. $nid .'/edit', array('query' => $destination)) : ' ', + $access ? l(t('delete'), 'node/'. $nid .'/delete', array('query' => $destination) ) : ' ', ); } @@ -879,106 +1099,49 @@ function theme_book_admin_table($form) { /** * Display an administrative view of the hierarchy of a book. */ -function book_admin_edit($form_state, $nid) { - $node = node_load($nid); - if ($node->nid) { +function book_admin_edit($form_state, $node) { + drupal_set_title(check_plain($node->title)); $form = array(); - $form['table'] = _book_admin_table(array($node)); + $form['#node'] = $node; + $form['table'] = _book_admin_table($node); $form['save'] = array( '#type' => 'submit', '#value' => t('Save book pages'), ); - - return $form; - } - else { - drupal_not_found(); - } + return $form; } /** - * Menu callback; displays a listing of all orphaned book pages. + * Handle submission of book administrative page form. */ -function book_admin_orphan() { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, n.status, b.parent FROM {node} n INNER JOIN {book} b ON n.vid = b.vid')); - - $pages = array(); - while ($page = db_fetch_object($result)) { - $pages[$page->nid] = $page; - } - - $orphans = array(); - if (count($pages)) { - foreach ($pages as $page) { - if ($page->parent && empty($pages[$page->parent])) { - $orphans[] = node_load($page->nid); - } - } - } - - if (count($orphans)) { - $form['table'] = _book_admin_table($orphans); - $form['save'] = array( - '#type' => 'submit', - '#value' => t('Save book pages'), - ); - - } - else { - $form['error'] = array('#value' => '

    '. t('There are no orphan pages.') .'

    '); - } - $form['#submit'][] = 'book_admin_edit_submit'; - $form['#validate'][] = 'book_admin_edit_validate'; - $form['#theme'] = 'book_admin_edit'; - return $form; -} - function book_admin_edit_submit($form, &$form_state) { foreach ($form_state['values']['table'] as $row) { $node = node_load($row['nid']); - if ($row['title'] != $node->title || $row['weight'] != $node->weight) { + if ($row['title'] != $node->title || $row['weight'] != $node->book['weight']) { $node->title = $row['title']; - $node->weight = $row['weight']; + $node->book['link_title'] = $row['title']; + $node->book['weight'] = $row['weight']; + $node->revision = 1; node_save($node); watchdog('content', 'book: updated %title.', array('%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid)); } } - if (is_numeric(arg(3))) { - // Updating pages in a single book. - $book = node_load(arg(3)); - drupal_set_message(t('Updated book %title.', array('%title' => $book->title))); - } - else { - // Updating the orphan pages. - drupal_set_message(t('Updated orphan book pages.')); - } -} - -/** - * Menu callback; displays the book administration page. - */ -function book_admin($nid = 0) { - if ($nid) { - return drupal_get_form('book_admin_edit', $nid); - } - else { - return book_admin_overview(); - } + drupal_set_message(t('Updated book %title.', array('%title' => $form['#node']->title))); } /** * Returns an administrative overview of all books. */ function book_admin_overview() { - $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = 0 ORDER BY b.weight, n.title')); + $result = db_query(db_rewrite_sql("SELECT n.title, b.* FROM {book} b INNER JOIN {node} n ON n.nid = b.nid WHERE b.nid = b.book_id ORDER BY n.title")); $rows = array(); while ($book = db_fetch_object($result)) { - $rows[] = array(l($book->title, "node/$book->nid"), l(t('outline'), "admin/content/book/$book->nid")); + $rows[] = array(l($book->title, "node/$book->nid"), l(t('edit order and titles'), "admin/content/book/$book->nid")); } $headers = array(t('Book'), t('Operations')); @@ -1008,3 +1171,65 @@ function book_help($section) { return '

    '. t('The outline feature allows you to include posts in the book hierarchy.', array('@book' => url('book'))) .'

    '; } } + +/** + * Like menu_link_load(), but get extra data from the {book} table. + * + * Do not call when loading a node, since this function may call node_load(). + */ +function book_link_load($mlid) { + if ($item = db_fetch_array(db_query("SELECT * FROM {menu_links} ml INNER JOIN {book} b ON b.mlid = ml.mlid LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid))) { + _menu_link_translate($item); + return $item; + } + return FALSE; +} + +/** + * Get the data structure representing a subtree. the root item will be the + * link passed as a parameter, and the returned data will contain all the + * links that are below it in the menu tree.. + * + * @param $menu_name + * The named menu links to return + * @param $item + * A fully loaded menu link, + * @return + * An subtree of menu links in an array, in the order they should be rendered. + */ +function book_menu_subtree_data($item) { + static $tree = array(); + + $mlid = $item['mlid']; + $menu_name = $item['menu_name']; + $cid = 'links:'. $menu_name .':subtree:'. $mlid; + + if (!isset($tree[$cid])) { + $cache = cache_get($cid, 'cache_menu'); + if ($cache && isset($cache->data)) { + $tree[$cid] = $cache->data; + } + else { + $i = 1; + $match = array(); + while ($i <= MENU_MAX_DEPTH && $item["p$i"]) { + $match[] = "p$i = %d"; + $args[] = $item["p$i"]; + $i++; + } + $sql = " + SELECT b.*, m.*, ml.menu_name, ml.mlid, ml.plid, ml.link_path, ml.router_path, ml.hidden, ml.external, ml.has_children, ml.expanded, ml.weight + 50000 AS weight, ml.depth, ml.p1, ml.p2, ml.p3, ml.p4, ml.p5, ml.p6, ml.module, ml.link_title, ml.options + FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path + INNER JOIN {book} b ON ml.mlid = b.mlid + WHERE ". implode(' AND ', $match) ." + ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC"; + + list(, $tree[$cid]) = _menu_tree_data(db_query($sql, $args), array(), $item['depth']); + cache_set($cid, $tree[$cid], 'cache_menu'); + } + _menu_tree_check_access($tree[$cid]); + } + + return $tree[$cid]; +} + === modified file 'modules/book/book.schema' --- modules/book/book.schema 2007-05-25 15:26:33 +0000 +++ modules/book/book.schema 2007-06-20 04:47:39 +0000 @@ -4,16 +4,15 @@ function book_schema() { $schema['book'] = array( 'fields' => array( - 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), - 'parent' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), - 'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny') + 'mlid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), + 'book_id' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0), ), 'indexes' => array( 'nid' => array('nid'), - 'parent' => array('parent') + 'book_id' => array('book_id') ), - 'primary key' => array('vid'), + 'primary key' => array('mlid'), ); return $schema;