' . t('About') . ''; $output .= '
' . t('The Book module is used for creating structured, multi-page content, such as site resource guides, manuals, and wikis. It allows you to create content that has chapters, sections, subsections, or any similarly-tiered structure. For more information, see the online handbook entry for Book module.', array('@book' => 'http://drupal.org/documentation/modules/book')) . '
'; $output .= '' . t('The book module offers a means to organize a collection of related content pages, collectively known as a book. When viewed, this content automatically displays links to adjacent book pages, providing a simple navigation system for creating and reviewing structured content.') . '
'; case 'node/%/outline': return '' . t('The outline feature allows you to include pages in the Book hierarchy, as well as move them within the hierarchy or to reorder an entire book.', array('@book' => url('book'), '@book-admin' => url('admin/content/book'))) . '
'; } } /** * Implements hook_theme(). */ function book_theme() { return array( 'book_navigation' => array( 'variables' => array('book_link' => NULL), 'template' => 'book-navigation', ), 'book_export_html' => array( 'variables' => array('title' => NULL, 'contents' => NULL, 'depth' => NULL), 'template' => 'book-export-html', ), 'book_admin_table' => array( 'render element' => 'form', ), 'book_all_books_block' => array( 'render element' => 'book_menus', 'template' => 'book-all-books-block', ), 'book_node_export_html' => array( 'variables' => array('node' => NULL, 'children' => NULL), 'template' => 'book-node-export-html', ), ); } /** * Implements hook_permission(). */ function book_permission() { return array( 'administer book outlines' => array( 'title' => t('Administer book outlines'), ), 'create new books' => array( 'title' => t('Create new books'), ), 'add content to books' => array( 'title' => t('Add content and child pages to books'), ), 'access printer-friendly version' => array( 'title' => t('View printer-friendly books'), 'description' => t('View a book page and all of its sub-pages as a single document for ease of printing. Can be performance heavy.'), ), ); } /** * Adds relevant book links to the node's links. * * @param Drupal\node\Node $node * The book page node to add links to. * @param $view_mode * The view mode of the node. */ function book_node_view_link(Node $node, $view_mode) { $links = array(); if (isset($node->book['depth'])) { if ($view_mode == 'full' && node_is_page($node)) { $child_type = config('book.settings')->get('child_type'); 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) { $links['book_add_child'] = array( 'title' => t('Add child page'), 'href' => 'node/add/' . $child_type, 'query' => array('parent' => $node->book['mlid']), ); } if (user_access('access printer-friendly version')) { $links['book_printer'] = array( 'title' => t('Printer-friendly version'), 'href' => 'book/export/html/' . $node->nid, 'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')) ); } } } if (!empty($links)) { $node->content['links']['book'] = array( '#theme' => 'links__node__book', '#links' => $links, '#attributes' => array('class' => array('links', 'inline')), ); } } /** * Implements hook_menu(). */ function book_menu() { $items['admin/content/book'] = array( 'title' => 'Books', 'description' => "Manage your site's book outlines.", 'page callback' => 'book_admin_overview', 'access arguments' => array('administer book outlines'), 'type' => MENU_LOCAL_TASK, 'file' => 'book.admin.inc', ); $items['admin/content/book/list'] = array( 'title' => 'List', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/content/book/settings'] = array( 'title' => 'Settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('book_admin_settings'), 'access arguments' => array('administer site configuration'), 'type' => MENU_LOCAL_TASK, 'weight' => 8, 'file' => 'book.admin.inc', ); $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, 'file' => 'book.admin.inc', ); $items['book'] = array( 'title' => 'Books', 'page callback' => 'book_render', 'access arguments' => array('access content'), 'type' => MENU_SUGGESTED_ITEM, 'file' => 'book.pages.inc', ); $items['book/export/%/%'] = array( 'page callback' => 'book_export', 'page arguments' => array(2, 3), 'access arguments' => array('access printer-friendly version'), 'type' => MENU_CALLBACK, 'file' => 'book.pages.inc', ); $items['node/%node/outline'] = array( 'title' => 'Outline', 'page callback' => 'book_outline', 'page arguments' => array(1), 'access callback' => '_book_outline_access', 'access arguments' => array(1), 'type' => MENU_LOCAL_TASK, 'weight' => 2, 'file' => 'book.pages.inc', ); $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), 'file' => 'book.pages.inc', ); return $items; } /** * Access callback: Determines if the outline tab is accessible. * * Path: * - admin/content/book/%node * - node/%node/outline * * @param Drupal\node\Node $node * The node whose outline tab is to be viewed. * * @see book_menu() */ function _book_outline_access(Node $node) { return user_access('administer book outlines') && node_access('view', $node); } /** * Access callback: Determines if the user can remove nodes from the outline. * * @param Drupal\node\Node $node * The node to remove from the outline. * * @see book_menu() */ function _book_outline_remove_access(Node $node) { return _book_node_is_removable($node) && _book_outline_access($node); } /** * Determines if a node can be removed from the book. * * A node can be removed from a book if it is actually in a book and it either * is not a top-level page or is a top-level page with no children. */ function _book_node_is_removable($node) { return (!empty($node->book['bid']) && (($node->book['bid'] != $node->nid) || !$node->book['has_children'])); } /** * Implements hook_admin_paths(). */ function book_admin_paths() { if (variable_get('node_admin_theme')) { $paths = array( 'node/*/outline' => TRUE, 'node/*/outline/remove' => TRUE, ); return $paths; } } /** * Implements hook_entity_info(). */ function book_entity_info(&$info) { // Add the 'Print' view mode for nodes. $info['node']['view_modes']['print'] = array( 'label' => t('Print'), 'custom_settings' => FALSE, ); } /** * 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. * * @return * An array of all books. */ function book_get_books() { $all_books = &drupal_static(__FUNCTION__); if (!isset($all_books)) { $all_books = array(); $nids = db_query("SELECT DISTINCT(bid) FROM {book}")->fetchCol(); if ($nids) { $query = db_select('book', 'b', array('fetch' => PDO::FETCH_ASSOC)); $query->join('node', 'n', 'b.nid = n.nid'); $query->join('menu_links', 'ml', 'b.mlid = ml.mlid'); $query->addField('n', 'type', 'type'); $query->addField('n', 'title', 'title'); $query->fields('b'); $query->fields('ml'); $query->condition('n.nid', $nids, 'IN'); $query->condition('n.status', 1); $query->orderBy('ml.weight'); $query->orderBy('ml.link_title'); $query->addTag('node_access'); $result2 = $query->execute(); foreach ($result2 as $link) { $link['href'] = $link['link_path']; $link['options'] = unserialize($link['options']); $all_books[$link['bid']] = $link; } } } return $all_books; } /** * Implements hook_form_BASE_FORM_ID_alter() for node_form(). * * Adds the book form element to the node form. * * @see book_pick_book_nojs_submit() */ function book_form_node_form_alter(&$form, &$form_state, $form_id) { $node = $form_state['controller']->getEntity($form_state); $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))) { // Already in the book hierarchy, or this node type is allowed. $access = TRUE; } } if ($access) { _book_add_form_elements($form, $form_state, $node); // Since the "Book" dropdown can't trigger a form submission when // JavaScript is disabled, add a submit button to do that. book.admin.css hides // this button when JavaScript is enabled. $form['book']['pick-book'] = array( '#type' => 'submit', '#value' => t('Change book (update list of parents)'), '#submit' => array('book_pick_book_nojs_submit'), '#weight' => 20, '#attached' => array( 'css' => array(drupal_get_path('module', 'book') . '/book.admin.css'), ), ); } } /** * Form submission handler for node_form(). * * This handler is run when JavaScript is disabled. It triggers the form to * rebuild so that the "Parent item" options are changed to reflect the newly * selected book. When JavaScript is enabled, the submit button that triggers * this handler is hidden, and the "Book" dropdown directly triggers the * book_form_update() Ajax callback instead. * * @see book_form_update() * @see book_form_node_form_alter() */ function book_pick_book_nojs_submit($form, &$form_state) { $node = $form_state['controller']->getEntity($form_state); $node->book = $form_state['values']['book']; $form_state['rebuild'] = TRUE; } /** * Builds the parent selection form element for the node form or outline tab. * * 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. * * @param $book_link * A fully loaded menu link that is part of the book hierarchy. * * @return * A parent selection form element. */ function _book_parent_select($book_link) { if (config('menu.settings')->get('override_parent_selector')) { return array(); } // Offer a message or a drop-down to choose a different parent page. $form = array( '#type' => 'hidden', '#value' => -1, '#prefix' => '' . 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->label())) . '
', '#weight' => -10, ); } } /** * Returns an array with default values for a book page's menu link. * * @param $nid * The ID of the node whose menu link is being created. * * @return * The default values for the menu 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()); } /** * Implements hook_preprocess_HOOK() for block.tpl.php. */ function book_preprocess_block(&$variables) { if ($variables['block']-> module == 'book') { $variables['attributes']['role'] = 'navigation'; } } /** * Processes variables for book-all-books-block.tpl.php. * * All non-renderable elements are removed so that the template has full access * to the structured data but can also simply iterate over all elements and * render them (as in the default template). * * The $variables array contains the following elements: * - book_menus * * @see book-all-books-block.tpl.php */ function template_preprocess_book_all_books_block(&$variables) { // Remove all non-renderable elements. $elements = $variables['book_menus']; $variables['book_menus'] = array(); foreach (element_children($elements) as $index) { $variables['book_menus'][$index] = $elements[$index]; } } /** * Processes variables for book-navigation.tpl.php. * * The $variables array contains the following elements: * - book_link * * @see book-navigation.tpl.php */ function template_preprocess_book_navigation(&$variables) { drupal_add_css(drupal_get_path('module', 'book') . '/book.theme.css'); $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']); $variables['book_url'] = 'node/' . $book_link['bid']; $variables['current_depth'] = $book_link['depth']; $variables['tree'] = ''; if ($book_link['mlid']) { $variables['tree'] = book_children($book_link); if ($prev = book_prev($book_link)) { $prev_href = url($prev['href']); drupal_add_html_head_link(array('rel' => 'prev', 'href' => $prev_href)); $variables['prev_url'] = $prev_href; $variables['prev_title'] = check_plain($prev['title']); } if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) { $parent_href = url($parent['href']); drupal_add_html_head_link(array('rel' => 'up', 'href' => $parent_href)); $variables['parent_url'] = $parent_href; $variables['parent_title'] = check_plain($parent['title']); } if ($next = book_next($book_link)) { $next_href = url($next['href']); drupal_add_html_head_link(array('rel' => 'next', 'href' => $next_href)); $variables['next_url'] = $next_href; $variables['next_title'] = check_plain($next['title']); } } $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] = ''; } } } /** * Recursively processes and formats menu items for book_toc(). * * This helper function recursively modifies the table of contents array for * each item in the menu tree, ignoring items in the exclude array or at a depth * greater than the limit. Truncates titles over thirty characters and appends * an indentation string incremented by depth. * * @param $tree * The data structure of the book's menu tree. Includes hidden links. * @param $indent * A string appended to each menu item title. Increments by '--' per depth * level. * @param $toc * Reference to the table of contents array. This is modified in place, so the * function does not have a return value. * @param $exclude * Optional array of menu link ID values. Any link whose menu link ID is in * this array will be excluded (along with its children). * @param $depth_limit * Any link deeper than this value will be excluded (along with its children). */ function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) { foreach ($tree as $data) { if ($data['link']['depth'] > $depth_limit) { // Don't iterate through any links on this level. break; } if (!in_array($data['link']['mlid'], $exclude)) { $toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE); if ($data['below']) { _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude, $depth_limit); } } } } /** * 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 $depth_limit * Any link deeper than this value will be excluded (along with its children). * @param $exclude * Optional array of menu link ID values. Any link whose menu link ID is in * this array will be excluded (along with its children). * * @return * An array of (menu link ID, title) pairs for use as options for selecting a * book page. */ function book_toc($bid, $depth_limit, $exclude = array()) { $tree = menu_tree_all_data(book_menu_name($bid)); $toc = array(); _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit); return $toc; } /** * Preprocesses variables for book-export-html.tpl.php. * * The $variables array contains the following elements: * - title * - contents * - depth * * @see book-export-html.tpl.php */ function template_preprocess_book_export_html(&$variables) { global $base_url; $language_interface = language(LANGUAGE_TYPE_INTERFACE); $variables['title'] = check_plain($variables['title']); $variables['base_url'] = $base_url; $variables['language'] = $language_interface; $variables['language_rtl'] = ($language_interface->direction == LANGUAGE_RTL); $variables['head'] = drupal_get_html_head(); // HTML element attributes. $attributes = array(); $attributes['lang'] = $language_interface->langcode; $attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; $variables['html_attributes'] = new Attribute($attributes); } /** * Traverses the book tree to build printable or exportable output. * * During the traversal, the $visit_func() callback is applied to each node and * is called recursively for each child of the node (in weight, title order). * * @param $tree * A subtree of the book menu hierarchy, rooted at the current page. * @param $visit_func * A function callback to be called upon visiting a node in the tree. * * @return * The output generated in visiting each node. */ function book_export_traverse($tree, $visit_func) { $output = ''; foreach ($tree as $data) { // Note- access checking is already performed when building the tree. if ($node = node_load($data['link']['nid'])) { $children = ''; if ($data['below']) { $children = book_export_traverse($data['below'], $visit_func); } if (!empty($visit_func)) { $output .= call_user_func($visit_func, $node, $children); } else { // Use the default function. $output .= book_node_export($node, $children); } } } return $output; } /** * Generates printer-friendly HTML for a node. * * @param Drupal\node\Node $node * The node that will be output. * @param $children * All the rendered child nodes within the current node. * * @return * The HTML generated for the given node. * * @see book_export_traverse() */ function book_node_export(Node $node, $children = '') { $build = node_view($node, 'print'); unset($build['#theme']); // @todo Rendering should happen in the template using render(). $node->rendered = drupal_render($build); return theme('book_node_export_html', array('node' => $node, 'children' => $children)); } /** * Processes variables for book-node-export-html.tpl.php. * * The $variables array contains the following elements: * - node * - children * * @see book-node-export-html.tpl.php */ function template_preprocess_book_node_export_html(&$variables) { $variables['depth'] = $variables['node']->book['depth']; $variables['title'] = check_plain($variables['node']->label()); $variables['content'] = $variables['node']->rendered; } /** * Determines if a given node type is in the list of types allowed for books. */ function book_type_is_allowed($type) { return in_array($type, config('book.settings')->get('allowed_types')); } /** * Implements hook_node_type_update(). * * Updates the Book module's persistent variables if the machine-readable name * of a node type is changed. */ function book_node_type_update($type) { if (!empty($type->old_type) && $type->old_type != $type->type) { $config = config('book.settings'); // Update the list of node types that are allowed to be added to books. $allowed_types = $config->get('allowed_types'); $key = array_search($type->old_type, $allowed_types); if ($key !== FALSE) { $allowed_types[$type->type] = $allowed_types[$key] ? $type->type : 0; unset($allowed_types[$key]); $config->set('allowed_types', $allowed_types); } // Update the setting for the "Add child page" link. if ($config->get('child_type') == $type->old_type) { $config->set('child_type', $type->type); } $config->save(); } } /** * Gets a book menu link by its menu link ID. * * Like menu_link_load(), but adds additional data from the {book} table. * * Do not call when loading a node, since this function may call node_load(). * * @param $mlid * The menu link ID of the menu item. * * @return * A menu link, with the link translated for rendering and data added from the * {book} table. */ function book_link_load($mlid) { if ($item = 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 = :mlid", array( ':mlid' => $mlid, ))->fetchAssoc()) { _menu_link_translate($item); return $item; } return FALSE; } /** * Gets the data representing a subtree of the book hierarchy. * * The root of the subtree will be the link passed as a parameter, so the * returned tree will contain this item and all its descendents in the menu * tree. * * @param $link * 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($link) { $tree = &drupal_static(__FUNCTION__, array()); // Generate a cache ID (cid) specific for this $menu_name and $link. $cid = 'links:' . $link['menu_name'] . ':subtree-cid:' . $link['mlid']; if (!isset($tree[$cid])) { $cache = cache('menu')->get($cid); if ($cache && isset($cache->data)) { // If the cache entry exists, it will just be the cid for the actual data. // This avoids duplication of large amounts of data. $cache = cache('menu')->get($cache->data); if ($cache && isset($cache->data)) { $data = $cache->data; } } // If the subtree data was not in the cache, $data will be NULL. if (!isset($data)) { $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); $query->join('menu_router', 'm', 'm.path = ml.router_path'); $query->join('book', 'b', 'ml.mlid = b.mlid'); $query->fields('b'); $query->fields('m', array('load_functions', 'to_arg_functions', 'access_callback', 'access_arguments', 'page_callback', 'page_arguments', 'title', 'title_callback', 'title_arguments', 'type')); $query->fields('ml'); $query->condition('menu_name', $link['menu_name']); for ($i = 1; $i <= MENU_MAX_DEPTH && $link["p$i"]; ++$i) { $query->condition("p$i", $link["p$i"]); } for ($i = 1; $i <= MENU_MAX_DEPTH; ++$i) { $query->orderBy("p$i"); } $links = array(); foreach ($query->execute() as $item) { $links[] = $item; } $data['tree'] = menu_tree_data($links, array(), $link['depth']); $data['node_links'] = array(); menu_tree_collect_node_links($data['tree'], $data['node_links']); // Compute the real cid for book subtree data. $tree_cid = 'links:' . $item['menu_name'] . ':subtree-data:' . hash('sha256', serialize($data)); // Cache the data, if it is not already in the cache. if (!cache('menu')->get($tree_cid)) { cache('menu')->set($tree_cid, $data); } // Cache the cid of the (shared) data using the menu and item-specific cid. cache('menu')->set($cid, $tree_cid); } // Check access for the current user to each item in the tree. menu_tree_check_access($data['tree'], $data['node_links']); $tree[$cid] = $data['tree']; } return $tree[$cid]; } /** * Implements hook_library_info(). */ function book_library_info() { $libraries['drupal.book'] = array( 'title' => 'Book', 'version' => VERSION, 'js' => array( drupal_get_path('module', 'book') . '/book.js' => array(), ), 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), array('system', 'drupal.form'), ), ); return $libraries; }