' . 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 documentation for the Book module.', array('!book' => 'https://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' => \Drupal::url('book.render'), '!book-admin' => \Drupal::url('book.admin'))) . '
'; } } /** * Implements hook_entity_bundle_info(). */ function book_entity_bundle_info() { $bundles['menu_link']['book-toc'] = array( 'label' => t('Book'), 'translatable' => FALSE, ); return $bundles; } /** * Implements hook_TYPE_load(). */ function book_menu_link_load($entities) { foreach ($entities as $entity) { // Change the bundle of menu links related to a book. if (strpos($entity->menu_name, 'book-toc-') === 0) { $entity->bundle = 'book-toc'; } } } /** * 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', 'file' => 'book.admin.inc', ), '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.'), ), ); } /** * Implements hook_entity_info(). */ function book_entity_info(&$entity_info) { /** @var $entity_info \Drupal\Core\Entity\EntityTypeInterface[] */ $entity_info['node']->setForm('book_outline', 'Drupal\book\Form\BookOutlineForm'); } /** * Implements hook_node_links_alter(). */ function book_node_links_alter(array &$node_links, NodeInterface $node, array &$context) { if ($context['view_mode'] != 'rss') { $account = \Drupal::currentUser(); if (isset($node->book['depth'])) { if ($context['view_mode'] == 'full' && node_is_page($node)) { $child_type = \Drupal::config('book.settings')->get('child_type'); $access_controller = \Drupal::entityManager()->getAccessController('node'); if (($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines')) && $access_controller->createAccess($child_type) && $node->isPublished() && $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 ($account->hasPermission('access printer-friendly version')) { $links['book_printer'] = array( 'title' => t('Printer-friendly version'), 'href' => 'book/export/html/' . $node->id(), 'attributes' => array('title' => t('Show a printer-friendly version of this book page and its sub-pages.')) ); } } } if (!empty($links)) { $node_links['book'] = array( '#theme' => 'links__node__book', '#links' => $links, '#attributes' => array('class' => array('links', 'inline')), ); } } } /** * Implements hook_menu(). */ function book_menu() { $items['admin/structure/book'] = array( 'title' => 'Books', 'description' => "Manage your site's book outlines.", 'route_name' => 'book.admin', ); $items['book'] = array( 'title' => 'Books', 'route_name' => 'book.render', 'type' => MENU_SUGGESTED_ITEM, ); $items['node/%node/outline/remove'] = array( 'title' => 'Remove from outline', 'route_name' => 'book.remove', ); return $items; } /** * Implements hook_admin_paths(). */ function book_admin_paths() { if (\Drupal::config('node.settings')->get('use_admin_theme')) { $paths = array( 'node/*/outline' => TRUE, 'node/*/outline/remove' => TRUE, ); return $paths; } } /** * Returns an array of all books. * * @todo Remove in favor of BookManager Service. http://drupal.org/node/1963894 * * 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() { return \Drupal::service('book.manager')->getAllBooks(); } /** * 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(); $account = \Drupal::currentUser(); $access = $account->hasPermission('administer book outlines'); if (!$access) { if ($account->hasPermission('add content to books') && ((!empty($node->book['mlid']) && !$node->isNew()) || book_type_is_allowed($node->getType()))) { // Already in the book hierarchy, or this node type is allowed. $access = TRUE; } } if ($access) { $form = \Drupal::service('book.manager')->addFormElements($form, $form_state, $node, $account); // 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') . '/css/book.admin.css'), ), ); $form['#entity_builders'][] = 'book_node_builder'; } } /** * Entity form builder to add the book information to the node. * * @todo: Remove this in favor of an entity field. */ function book_node_builder($entity_type, $entity, &$form, &$form_state) { $entity->book = $form_state['values']['book']; } /** * 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(); $node->book = $form_state['values']['book']; $form_state['rebuild'] = TRUE; } /** * Renders a new parent page select element when the book selection changes. * * This function is called via Ajax when the selected book is changed on a node * or book outline form. * * @return * The rendered parent page select element. */ function book_form_update($form, $form_state) { return $form['book']['plid']; } /** * Gets the book menu tree for a page and returns it as a linear array. * * @param $book_link * A fully loaded menu link that is part of the book hierarchy. * * @return * 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 the current node. The children of the current node * (if any) will come immediately after it in the array, and links will only * be fetched as deep as one level deeper than $book_link. */ function book_get_flat_menu($book_link) { $flat = &drupal_static(__FUNCTION__, array()); if (!isset($flat[$book_link['mlid']])) { // Call bookTreeAllData() to take advantage of the menu system's caching. $tree = \Drupal::service('book.manager')->bookTreeAllData($book_link['menu_name'], $book_link, $book_link['depth'] + 1); $flat[$book_link['mlid']] = array(); _book_flatten_menu($tree, $flat[$book_link['mlid']]); } return $flat[$book_link['mlid']]; } /** * Recursively converts a tree of menu links to a flat array. * * @param $tree * A tree of menu links in an array. * @param $flat * A flat array of the menu links from $tree, passed by reference. * * @see book_get_flat_menu(). */ 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); } } } } /** * Fetches the menu link for the previous page of the book. * * @param $book_link * A fully loaded menu link that is part of the book hierarchy. * * @return * A fully loaded menu link for the page before the one represented in * $book_link. */ 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; } $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. $tree = \Drupal::service('book.manager')->bookMenuSubtreeData($prev); $data = array_shift($tree); // The link of interest is the last child - iterate to find the deepest one. while ($data['below']) { $data = end($data['below']); } return $data['link']; } else { return $prev; } } } /** * Fetches the menu link for the next page of the book. * * @param $book_link * A fully loaded menu link that is part of the book hierarchy. * * @return * A fully loaded menu link for the page after the one represented in * $book_link. */ 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, ) = each($flat); } while ($key && $key != $book_link['mlid']); if ($key == $book_link['mlid']) { return current($flat); } } /** * Formats the menu links for the child pages of the current page. * * @param $book_link * A fully loaded menu link that is part of the book hierarchy. * * @return * HTML for the links to the child pages of the current page. */ function book_children($book_link) { $flat = book_get_flat_menu($book_link); $children = array(); 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; } } if ($children) { $elements = \Drupal::service('book.manager')->bookTreeOutput($children); return drupal_render($elements); } return ''; } /** * Implements hook_node_load(). */ function book_node_load($nodes) { $result = db_query("SELECT * FROM {book} b INNER JOIN {menu_links} ml ON b.mlid = ml.mlid WHERE b.nid IN (:nids)", array(':nids' => array_keys($nodes)), array('fetch' => PDO::FETCH_ASSOC)); foreach ($result as $record) { $nodes[$record['nid']]->book = $record; $nodes[$record['nid']]->book['href'] = $record['link_path']; $nodes[$record['nid']]->book['title'] = $record['link_title']; $nodes[$record['nid']]->book['options'] = unserialize($record['options']); } } /** * Implements hook_node_view(). */ function book_node_view(EntityInterface $node, EntityViewDisplayInterface $display, $view_mode) { if ($view_mode == 'full') { if (!empty($node->book['bid']) && empty($node->in_preview)) { $book_navigation = array( '#theme' => 'book_navigation', '#book_link' => $node->book); $node->content['book_navigation'] = array( '#markup' => drupal_render($book_navigation), '#weight' => 100, '#attached' => array( 'css' => array( drupal_get_path('module', 'book') . '/css/book.theme.css', ), ), ); } } } /** * Implements hook_node_presave(). */ function book_node_presave(EntityInterface $node) { // Always save a revision for non-administrators. if (!empty($node->book['bid']) && !\Drupal::currentUser()->hasPermission('administer nodes')) { $node->setNewRevision(); } // Make sure a new node gets a new menu link. if ($node->isNew()) { $node->book['mlid'] = NULL; } } /** * Implements hook_node_insert(). */ function book_node_insert(EntityInterface $node) { $book_manager = \Drupal::service('book.manager'); if (!empty($node->book['bid'])) { if ($node->book['bid'] == 'new') { // New nodes that are their own book. $node->book['bid'] = $node->id(); } $node->book['nid'] = $node->id(); $node->book['menu_name'] = $book_manager->createMenuName($node->book['bid']); $book_manager->updateOutline($node); } } /** * Implements hook_node_update(). */ function book_node_update(EntityInterface $node) { $book_manager = \Drupal::service('book.manager'); if (!empty($node->book['bid'])) { if ($node->book['bid'] == 'new') { // New nodes that are their own book. $node->book['bid'] = $node->id(); } $node->book['nid'] = $node->id(); $node->book['menu_name'] = $book_manager->createMenuName($node->book['bid']); $book_manager->updateOutline($node); } } /** * Implements hook_node_predelete(). */ function book_node_predelete(EntityInterface $node) { if (!empty($node->book['bid'])) { if ($node->id() == $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 = :plid", array( ':plid' => $node->book['mlid'] )); foreach ($result as $child) { $child_node = node_load($child->id()); $child_node->book['bid'] = $child_node->id(); \Drupal::service('book.manager')->updateOutline($child_node); } } // @todo - remove this call when we change the schema. menu_link_delete($node->book['mlid']); db_delete('book') ->condition('mlid', $node->book['mlid']) ->execute(); drupal_static_reset('book_get_books'); } } /** * Implements hook_node_prepare_form(). */ function book_node_prepare_form(NodeInterface $node, $form_display, $operation, array &$form_state) { // Get BookManager service $book_manager = \Drupal::service('book.manager'); // Prepare defaults for the add/edit form. $account = \Drupal::currentUser(); if (empty($node->book) && ($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines'))) { $node->book = array(); $query = \Drupal::request()->query; if ($node->isNew() && !is_null($query->get('parent')) && is_numeric($query->get('parent'))) { // Handle "Add child page" links: $parent = book_link_load($query->get('parent')); if ($parent && $parent['access']) { $node->book['bid'] = $parent['bid']; $node->book['plid'] = $parent['mlid']; $node->book['menu_name'] = $parent['menu_name']; } } // Set defaults. $node_ref = !$node->isNew() ? $node->id() : 'new'; $node->book += $book_manager->getLinkDefaults($node_ref); } else { if (isset($node->book['bid']) && !isset($node->book['original_bid'])) { $node->book['original_bid'] = $node->book['bid']; } } // 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_manager->getParentDepthLimit($node->book); } } /** * Implements hook_form_FORM_ID_alter() for node_delete_confirm(). * * Alters the confirm form for a single node deletion. * * @see node_delete_confirm() */ 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( '#markup' => '' . 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, ); } } /** * Implements hook_preprocess_HOOK() for block templates. */ function book_preprocess_block(&$variables) { if ($variables['configuration']['module'] == 'book') { $variables['attributes']['role'] = 'navigation'; } } /** * Prepares variables for book listing block templates. * * Default template: book-all-books-block.html.twig. * * 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). * * @param array $variables * An associative array containing the following key: * - book_menus: An associative array containing renderable menu links for all * book menus. */ 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]; } } /** * Prepares variables for book navigation templates. * * Default template: book-navigation.html.twig. * * @param array $variables * An associative array containing the following key: * - book_link: An associative array of book link properties. * Properties used: bid, link_title, depth, plid, mlid. */ 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']); $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); $build = array(); if ($prev = book_prev($book_link)) { $prev_href = url($prev['href']); $build['#attached']['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['link_path']); $build['#attached']['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']); $build['#attached']['drupal_add_html_head_link'][][] = array( 'rel' => 'next', 'href' => $next_href, ); $variables['next_url'] = $next_href; $variables['next_title'] = check_plain($next['title']); } } if (!empty($build)) { drupal_render($build); } $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] = ''; } } } /** * Prepares variables for book export templates. * * Default template: book-export-html.html.twig. * * @param array $variables * An associative array containing: * - title: The title of the book. * - contents: Output of each book page. * - depth: The max depth of the book. */ 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::DIRECTION_RTL); $variables['head'] = drupal_get_html_head(); // HTML element attributes. $attributes = array(); $attributes['lang'] = $language_interface->id; $attributes['dir'] = $language_interface->direction ? 'rtl' : 'ltr'; $variables['html_attributes'] = new Attribute($attributes); } /** * Prepares variables for single node export templates. * * Default template: book-node-export-html.html.twig. * * @param array $variables * An associative array containing the following keys: * - node: The node that will be output. * - children: All the rendered child nodes within the current node. Defaults * to an empty string. */ 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. * * @param string $type * A node type. * * @return bool * A Boolean TRUE if the node type can be included in books; otherwise, FALSE. */ function book_type_is_allowed($type) { return in_array($type, \Drupal::config('book.settings')->get('allowed_types')); } /** * Implements hook_node_type_update(). * * Updates book.settings configuration object if the machine-readable name of a * node type is changed. */ function book_node_type_update(NodeTypeInterface $type) { if ($type->getOriginalId() != $type->id()) { $config = \Drupal::config('book.settings'); // Update the list of node types that are allowed to be added to books. $allowed_types = $config->get('allowed_types'); $old_key = array_search($type->getOriginalId(), $allowed_types); if ($old_key !== FALSE) { $allowed_types[$old_key] = $type->id(); // Ensure that the allowed_types array is sorted consistently. // @see BookSettingsForm::submitForm() sort($allowed_types); $config->set('allowed_types', $allowed_types); } // Update the setting for the "Add child page" link. if ($config->get('child_type') == $type->getOriginalId()) { $config->set('child_type', $type->id()); } $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. FALSE if there is an error. */ function book_link_load($mlid) { return entity_load('menu_link', $mlid); } /** * Implements hook_library_info(). */ function book_library_info() { $libraries['drupal.book'] = array( 'title' => 'Book', 'version' => \Drupal::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; }