' . 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. Enabling the module creates a new content type Book page. For more information, see the online documentation for the Book module.', [':book' => 'https://www.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 'entity.node.book_outline_form': 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.', [':book' => \Drupal::url('book.render'), ':book-admin' => \Drupal::url('book.admin')]) . '
'; } } /** * Implements hook_theme(). */ function book_theme() { return [ 'book_navigation' => [ 'variables' => ['book_link' => NULL], ], 'book_tree' => [ 'variables' => ['items' => [], 'attributes' => []], ], 'book_export_html' => [ 'variables' => ['title' => NULL, 'contents' => NULL, 'depth' => NULL], ], 'book_all_books_block' => [ 'render element' => 'book_menus', ], 'book_node_export_html' => [ 'variables' => ['node' => NULL, 'content' => NULL, 'children' => NULL], ], ]; } /** * Implements hook_entity_type_build(). */ function book_entity_type_build(array &$entity_types) { /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ $entity_types['node'] ->setFormClass('book_outline', 'Drupal\book\Form\BookOutlineForm') ->setLinkTemplate('book-outline-form', '/node/{node}/outline') ->setLinkTemplate('book-remove-form', '/node/{node}/outline/remove') ->addConstraint('BookOutline', []); } /** * Implements hook_node_links_alter(). */ function book_node_links_alter(array &$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_control_handler = \Drupal::entityManager()->getAccessControlHandler('node'); if (($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines')) && $access_control_handler->createAccess($child_type) && $node->isPublished() && $node->book['depth'] < BookManager::BOOK_MAX_DEPTH) { $book_links['book_add_child'] = [ 'title' => t('Add child page'), 'url' => Url::fromRoute('node.add', ['node_type' => $child_type], ['query' => ['parent' => $node->id()]]), ]; } if ($account->hasPermission('access printer-friendly version')) { $book_links['book_printer'] = [ 'title' => t('Printer-friendly version'), 'url' => Url::fromRoute('book.export', [ 'type' => 'html', 'node' => $node->id(), ]), 'attributes' => ['title' => t('Show a printer-friendly version of this book page and its sub-pages.')] ]; } } } if (!empty($book_links)) { $links['book'] = [ '#theme' => 'links__node__book', '#links' => $book_links, '#attributes' => ['class' => ['links', 'inline']], ]; } } } /** * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm. * * Adds the book form element to the node form. * * @see book_pick_book_nojs_submit() */ function book_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { $node = $form_state->getFormObject()->getEntity(); $account = \Drupal::currentUser(); $access = $account->hasPermission('administer book outlines'); if (!$access) { if ($account->hasPermission('add content to books') && ((!empty($node->book['bid']) && !$node->isNew()) || book_type_is_allowed($node->getType()))) { // Already in the book hierarchy, or this node type is allowed. $access = TRUE; } } if ($access) { $collapsed = !($node->isNew() && !empty($node->book['pid'])); $form = \Drupal::service('book.manager')->addFormElements($form, $form_state, $node, $account, $collapsed); // The "js-hide" class hides submit button when Javascript is enabled. $form['book']['pick-book'] = [ '#type' => 'submit', '#value' => t('Change book (update list of parents)'), '#submit' => ['book_pick_book_nojs_submit'], '#weight' => 20, '#attributes' => [ 'class' => [ 'js-hide', ], ], ]; $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, NodeInterface $entity, &$form, FormStateInterface $form_state) { $entity->book = $form_state->getValue('book'); // Always save a revision for non-administrators. if (!empty($entity->book['bid']) && !\Drupal::currentUser()->hasPermission('administer nodes')) { $entity->setNewRevision(); } } /** * 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, FormStateInterface $form_state) { $node = $form_state->getFormObject()->getEntity(); $node->book = $form_state->getValue('book'); $form_state->setRebuild(); } /** * 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, FormStateInterface $form_state) { return $form['book']['pid']; } /** * Implements hook_ENTITY_TYPE_load() for node entities. */ function book_node_load($nodes) { /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); $links = $book_manager->loadBookLinks(array_keys($nodes), FALSE); foreach ($links as $record) { $nodes[$record['nid']]->book = $record; $nodes[$record['nid']]->book['link_path'] = 'node/' . $record['nid']; $nodes[$record['nid']]->book['link_title'] = $nodes[$record['nid']]->label(); } } /** * Implements hook_ENTITY_TYPE_view() for node entities. */ function book_node_view(array &$build, EntityInterface $node, EntityViewDisplayInterface $display, $view_mode) { if ($view_mode == 'full') { if (!empty($node->book['bid']) && empty($node->in_preview)) { $book_node = Node::load($node->book['bid']); if (!$book_node->access()) { return; } $build['book_navigation'] = [ '#theme' => 'book_navigation', '#book_link' => $node->book, '#weight' => 100, // The book navigation is a listing of Node entities, so associate its // list cache tag for correct invalidation. '#cache' => [ 'tags' => $node->getEntityType()->getListCacheTags(), ], ]; } } } /** * Implements hook_ENTITY_TYPE_presave() for node entities. */ function book_node_presave(EntityInterface $node) { // Make sure a new node gets a new menu link. if ($node->isNew()) { $node->book['nid'] = NULL; } } /** * Implements hook_ENTITY_TYPE_insert() for node entities. */ function book_node_insert(EntityInterface $node) { /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); $book_manager->updateOutline($node); } /** * Implements hook_ENTITY_TYPE_update() for node entities. */ function book_node_update(EntityInterface $node) { /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); $book_manager->updateOutline($node); } /** * Implements hook_ENTITY_TYPE_predelete() for node entities. */ function book_node_predelete(EntityInterface $node) { if (!empty($node->book['bid'])) { /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); $book_manager->deleteFromBook($node->book['nid']); } } /** * Implements hook_ENTITY_TYPE_prepare_form() for node entities. */ function book_node_prepare_form(NodeInterface $node, $operation, FormStateInterface $form_state) { /** @var \Drupal\book\BookManagerInterface $book_manager */ $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 = []; $query = \Drupal::request()->query; if ($node->isNew() && !is_null($query->get('parent')) && is_numeric($query->get('parent'))) { // Handle "Add child page" links: $parent = $book_manager->loadBookLink($query->get('parent'), TRUE); if ($parent && $parent['access']) { $node->book['bid'] = $parent['bid']; $node->book['pid'] = $parent['nid']; } } // 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_BASE_FORM_ID_alter() for \Drupal\node\Form\NodeDeleteForm. * * Alters the confirm form for a single node deletion. */ function book_form_node_confirm_form_alter(&$form, FormStateInterface $form_state) { // Only need to alter the delete operation form. if ($form_state->getFormObject()->getOperation() !== 'delete') { return; } /** @var \Drupal\node\NodeInterface $node */ $node = $form_state->getFormObject()->getEntity(); if (!book_type_is_allowed($node->getType())) { // Not a book node. return; } if (isset($node->book) && $node->book['has_children']) { $form['book_warning'] = [ '#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.', ['%title' => $node->label()]) . '
', '#weight' => -10, ]; } } /** * 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'] = []; foreach (Element::children($elements) as $index) { $variables['book_menus'][] = [ 'id' => $index, 'menu' => $elements[$index], 'title' => $elements[$index]['#book_title'], ]; } } /** * 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, pid, nid. */ 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'] = $book_link['link_title']; $variables['book_url'] = \Drupal::url('entity.node.canonical', ['node' => $book_link['bid']]); $variables['current_depth'] = $book_link['depth']; $variables['tree'] = ''; /** @var \Drupal\book\BookOutline $book_outline */ $book_outline = \Drupal::service('book.outline'); if ($book_link['nid']) { $variables['tree'] = $book_outline->childrenLinks($book_link); $build = []; if ($prev = $book_outline->prevLink($book_link)) { $prev_href = \Drupal::url('entity.node.canonical', ['node' => $prev['nid']]); $build['#attached']['html_head_link'][][] = [ 'rel' => 'prev', 'href' => $prev_href, ]; $variables['prev_url'] = $prev_href; $variables['prev_title'] = $prev['title']; } /** @var \Drupal\book\BookManagerInterface $book_manager */ $book_manager = \Drupal::service('book.manager'); if ($book_link['pid'] && $parent = $book_manager->loadBookLink($book_link['pid'])) { $parent_href = \Drupal::url('entity.node.canonical', ['node' => $book_link['pid']]); $build['#attached']['html_head_link'][][] = [ 'rel' => 'up', 'href' => $parent_href, ]; $variables['parent_url'] = $parent_href; $variables['parent_title'] = $parent['title']; } if ($next = $book_outline->nextLink($book_link)) { $next_href = \Drupal::url('entity.node.canonical', ['node' => $next['nid']]); $build['#attached']['html_head_link'][][] = [ 'rel' => 'next', 'href' => $next_href, ]; $variables['next_url'] = $next_href; $variables['next_title'] = $next['title']; } } if (!empty($build)) { \Drupal::service('renderer')->render($build); } $variables['has_links'] = FALSE; // Link variables to filter for values and set state of the flag variable. $links = ['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 = \Drupal::languageManager()->getCurrentLanguage(); $variables['base_url'] = $base_url; $variables['language'] = $language_interface; $variables['language_rtl'] = ($language_interface->getDirection() == LanguageInterface::DIRECTION_RTL); // HTML element attributes. $attributes = []; $attributes['lang'] = $language_interface->getId(); $attributes['dir'] = $language_interface->getDirection(); $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'] = $variables['node']->label(); } /** * 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_ENTITY_TYPE_update() for node_type entities. * * 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::configFactory()->getEditable('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(); } }