Chris@0: ' . t('About') . ''; Chris@0: $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']) . '
'; Chris@0: $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.') . '
'; Chris@0: Chris@0: case 'entity.node.book_outline_form': Chris@18: 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' => Url::fromRoute('book.render')->toString(), ':book-admin' => Url::fromRoute('book.admin')->toString()]) . '
'; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_theme(). Chris@0: */ Chris@0: function book_theme() { Chris@0: return [ Chris@0: 'book_navigation' => [ Chris@0: 'variables' => ['book_link' => NULL], Chris@0: ], Chris@0: 'book_tree' => [ Chris@0: 'variables' => ['items' => [], 'attributes' => []], Chris@0: ], Chris@0: 'book_export_html' => [ Chris@0: 'variables' => ['title' => NULL, 'contents' => NULL, 'depth' => NULL], Chris@0: ], Chris@0: 'book_all_books_block' => [ Chris@0: 'render element' => 'book_menus', Chris@0: ], Chris@0: 'book_node_export_html' => [ Chris@0: 'variables' => ['node' => NULL, 'content' => NULL, 'children' => NULL], Chris@0: ], Chris@0: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_entity_type_build(). Chris@0: */ Chris@0: function book_entity_type_build(array &$entity_types) { Chris@0: /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ Chris@0: $entity_types['node'] Chris@0: ->setFormClass('book_outline', 'Drupal\book\Form\BookOutlineForm') Chris@0: ->setLinkTemplate('book-outline-form', '/node/{node}/outline') Chris@0: ->setLinkTemplate('book-remove-form', '/node/{node}/outline/remove') Chris@0: ->addConstraint('BookOutline', []); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_node_links_alter(). Chris@0: */ Chris@0: function book_node_links_alter(array &$links, NodeInterface $node, array &$context) { Chris@0: if ($context['view_mode'] != 'rss') { Chris@0: $account = \Drupal::currentUser(); Chris@0: Chris@0: if (isset($node->book['depth'])) { Chris@0: if ($context['view_mode'] == 'full' && node_is_page($node)) { Chris@0: $child_type = \Drupal::config('book.settings')->get('child_type'); Chris@0: $access_control_handler = \Drupal::entityManager()->getAccessControlHandler('node'); Chris@0: 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) { Chris@0: $book_links['book_add_child'] = [ Chris@0: 'title' => t('Add child page'), Chris@0: 'url' => Url::fromRoute('node.add', ['node_type' => $child_type], ['query' => ['parent' => $node->id()]]), Chris@0: ]; Chris@0: } Chris@0: Chris@0: if ($account->hasPermission('access printer-friendly version')) { Chris@0: $book_links['book_printer'] = [ Chris@0: 'title' => t('Printer-friendly version'), Chris@0: 'url' => Url::fromRoute('book.export', [ Chris@0: 'type' => 'html', Chris@0: 'node' => $node->id(), Chris@0: ]), Chris@17: 'attributes' => ['title' => t('Show a printer-friendly version of this book page and its sub-pages.')], Chris@0: ]; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: if (!empty($book_links)) { Chris@0: $links['book'] = [ Chris@0: '#theme' => 'links__node__book', Chris@0: '#links' => $book_links, Chris@0: '#attributes' => ['class' => ['links', 'inline']], Chris@0: ]; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm. Chris@0: * Chris@0: * Adds the book form element to the node form. Chris@0: * Chris@0: * @see book_pick_book_nojs_submit() Chris@0: */ Chris@0: function book_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { Chris@0: $node = $form_state->getFormObject()->getEntity(); Chris@0: $account = \Drupal::currentUser(); Chris@0: $access = $account->hasPermission('administer book outlines'); Chris@0: if (!$access) { Chris@0: if ($account->hasPermission('add content to books') && ((!empty($node->book['bid']) && !$node->isNew()) || book_type_is_allowed($node->getType()))) { Chris@0: // Already in the book hierarchy, or this node type is allowed. Chris@0: $access = TRUE; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($access) { Chris@0: $collapsed = !($node->isNew() && !empty($node->book['pid'])); Chris@0: $form = \Drupal::service('book.manager')->addFormElements($form, $form_state, $node, $account, $collapsed); Chris@0: // The "js-hide" class hides submit button when Javascript is enabled. Chris@0: $form['book']['pick-book'] = [ Chris@0: '#type' => 'submit', Chris@0: '#value' => t('Change book (update list of parents)'), Chris@0: '#submit' => ['book_pick_book_nojs_submit'], Chris@0: '#weight' => 20, Chris@0: '#attributes' => [ Chris@0: 'class' => [ Chris@0: 'js-hide', Chris@0: ], Chris@0: ], Chris@0: ]; Chris@0: $form['#entity_builders'][] = 'book_node_builder'; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Entity form builder to add the book information to the node. Chris@0: * Chris@0: * @todo: Remove this in favor of an entity field. Chris@0: */ Chris@0: function book_node_builder($entity_type, NodeInterface $entity, &$form, FormStateInterface $form_state) { Chris@0: $entity->book = $form_state->getValue('book'); Chris@0: Chris@0: // Always save a revision for non-administrators. Chris@0: if (!empty($entity->book['bid']) && !\Drupal::currentUser()->hasPermission('administer nodes')) { Chris@0: $entity->setNewRevision(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Form submission handler for node_form(). Chris@0: * Chris@0: * This handler is run when JavaScript is disabled. It triggers the form to Chris@0: * rebuild so that the "Parent item" options are changed to reflect the newly Chris@0: * selected book. When JavaScript is enabled, the submit button that triggers Chris@0: * this handler is hidden, and the "Book" dropdown directly triggers the Chris@0: * book_form_update() Ajax callback instead. Chris@0: * Chris@0: * @see book_form_update() Chris@0: * @see book_form_node_form_alter() Chris@0: */ Chris@0: function book_pick_book_nojs_submit($form, FormStateInterface $form_state) { Chris@0: $node = $form_state->getFormObject()->getEntity(); Chris@0: $node->book = $form_state->getValue('book'); Chris@0: $form_state->setRebuild(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Renders a new parent page select element when the book selection changes. Chris@0: * Chris@0: * This function is called via Ajax when the selected book is changed on a node Chris@0: * or book outline form. Chris@0: * Chris@0: * @return Chris@0: * The rendered parent page select element. Chris@0: */ Chris@0: function book_form_update($form, FormStateInterface $form_state) { Chris@0: return $form['book']['pid']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_load() for node entities. Chris@0: */ Chris@0: function book_node_load($nodes) { Chris@0: /** @var \Drupal\book\BookManagerInterface $book_manager */ Chris@0: $book_manager = \Drupal::service('book.manager'); Chris@0: $links = $book_manager->loadBookLinks(array_keys($nodes), FALSE); Chris@0: foreach ($links as $record) { Chris@0: $nodes[$record['nid']]->book = $record; Chris@0: $nodes[$record['nid']]->book['link_path'] = 'node/' . $record['nid']; Chris@0: $nodes[$record['nid']]->book['link_title'] = $nodes[$record['nid']]->label(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_view() for node entities. Chris@0: */ Chris@0: function book_node_view(array &$build, EntityInterface $node, EntityViewDisplayInterface $display, $view_mode) { Chris@0: if ($view_mode == 'full') { Chris@0: if (!empty($node->book['bid']) && empty($node->in_preview)) { Chris@0: $book_node = Node::load($node->book['bid']); Chris@0: if (!$book_node->access()) { Chris@0: return; Chris@0: } Chris@0: $build['book_navigation'] = [ Chris@0: '#theme' => 'book_navigation', Chris@0: '#book_link' => $node->book, Chris@0: '#weight' => 100, Chris@0: // The book navigation is a listing of Node entities, so associate its Chris@0: // list cache tag for correct invalidation. Chris@0: '#cache' => [ Chris@0: 'tags' => $node->getEntityType()->getListCacheTags(), Chris@0: ], Chris@0: ]; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_presave() for node entities. Chris@0: */ Chris@0: function book_node_presave(EntityInterface $node) { Chris@0: // Make sure a new node gets a new menu link. Chris@0: if ($node->isNew()) { Chris@0: $node->book['nid'] = NULL; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_insert() for node entities. Chris@0: */ Chris@0: function book_node_insert(EntityInterface $node) { Chris@0: /** @var \Drupal\book\BookManagerInterface $book_manager */ Chris@0: $book_manager = \Drupal::service('book.manager'); Chris@0: $book_manager->updateOutline($node); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_update() for node entities. Chris@0: */ Chris@0: function book_node_update(EntityInterface $node) { Chris@0: /** @var \Drupal\book\BookManagerInterface $book_manager */ Chris@0: $book_manager = \Drupal::service('book.manager'); Chris@0: $book_manager->updateOutline($node); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_predelete() for node entities. Chris@0: */ Chris@0: function book_node_predelete(EntityInterface $node) { Chris@0: if (!empty($node->book['bid'])) { Chris@0: /** @var \Drupal\book\BookManagerInterface $book_manager */ Chris@0: $book_manager = \Drupal::service('book.manager'); Chris@0: $book_manager->deleteFromBook($node->book['nid']); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_prepare_form() for node entities. Chris@0: */ Chris@0: function book_node_prepare_form(NodeInterface $node, $operation, FormStateInterface $form_state) { Chris@0: /** @var \Drupal\book\BookManagerInterface $book_manager */ Chris@0: $book_manager = \Drupal::service('book.manager'); Chris@0: Chris@0: // Prepare defaults for the add/edit form. Chris@0: $account = \Drupal::currentUser(); Chris@0: if (empty($node->book) && ($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines'))) { Chris@0: $node->book = []; Chris@0: Chris@0: $query = \Drupal::request()->query; Chris@0: if ($node->isNew() && !is_null($query->get('parent')) && is_numeric($query->get('parent'))) { Chris@0: // Handle "Add child page" links: Chris@0: $parent = $book_manager->loadBookLink($query->get('parent'), TRUE); Chris@0: Chris@0: if ($parent && $parent['access']) { Chris@0: $node->book['bid'] = $parent['bid']; Chris@0: $node->book['pid'] = $parent['nid']; Chris@0: } Chris@0: } Chris@0: // Set defaults. Chris@0: $node_ref = !$node->isNew() ? $node->id() : 'new'; Chris@0: $node->book += $book_manager->getLinkDefaults($node_ref); Chris@0: } Chris@0: else { Chris@0: if (isset($node->book['bid']) && !isset($node->book['original_bid'])) { Chris@0: $node->book['original_bid'] = $node->book['bid']; Chris@0: } Chris@0: } Chris@0: // Find the depth limit for the parent select. Chris@0: if (isset($node->book['bid']) && !isset($node->book['parent_depth_limit'])) { Chris@0: $node->book['parent_depth_limit'] = $book_manager->getParentDepthLimit($node->book); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\Form\NodeDeleteForm. Chris@0: * Chris@0: * Alters the confirm form for a single node deletion. Chris@0: */ Chris@0: function book_form_node_confirm_form_alter(&$form, FormStateInterface $form_state) { Chris@0: // Only need to alter the delete operation form. Chris@0: if ($form_state->getFormObject()->getOperation() !== 'delete') { Chris@0: return; Chris@0: } Chris@0: Chris@0: /** @var \Drupal\node\NodeInterface $node */ Chris@0: $node = $form_state->getFormObject()->getEntity(); Chris@0: if (!book_type_is_allowed($node->getType())) { Chris@0: // Not a book node. Chris@0: return; Chris@0: } Chris@0: Chris@0: if (isset($node->book) && $node->book['has_children']) { Chris@0: $form['book_warning'] = [ Chris@0: '#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()]) . '
', Chris@0: '#weight' => -10, Chris@0: ]; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for book listing block templates. Chris@0: * Chris@0: * Default template: book-all-books-block.html.twig. Chris@0: * Chris@0: * All non-renderable elements are removed so that the template has full access Chris@0: * to the structured data but can also simply iterate over all elements and Chris@0: * render them (as in the default template). Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing the following key: Chris@0: * - book_menus: An associative array containing renderable menu links for all Chris@0: * book menus. Chris@0: */ Chris@0: function template_preprocess_book_all_books_block(&$variables) { Chris@0: // Remove all non-renderable elements. Chris@0: $elements = $variables['book_menus']; Chris@0: $variables['book_menus'] = []; Chris@0: foreach (Element::children($elements) as $index) { Chris@0: $variables['book_menus'][] = [ Chris@0: 'id' => $index, Chris@0: 'menu' => $elements[$index], Chris@0: 'title' => $elements[$index]['#book_title'], Chris@0: ]; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for book navigation templates. Chris@0: * Chris@0: * Default template: book-navigation.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing the following key: Chris@0: * - book_link: An associative array of book link properties. Chris@0: * Properties used: bid, link_title, depth, pid, nid. Chris@0: */ Chris@0: function template_preprocess_book_navigation(&$variables) { Chris@0: $book_link = $variables['book_link']; Chris@0: Chris@0: // Provide extra variables for themers. Not needed by default. Chris@0: $variables['book_id'] = $book_link['bid']; Chris@0: $variables['book_title'] = $book_link['link_title']; Chris@18: $variables['book_url'] = Url::fromRoute('entity.node.canonical', ['node' => $book_link['bid']])->toString(); Chris@0: $variables['current_depth'] = $book_link['depth']; Chris@0: $variables['tree'] = ''; Chris@0: Chris@0: /** @var \Drupal\book\BookOutline $book_outline */ Chris@0: $book_outline = \Drupal::service('book.outline'); Chris@0: Chris@0: if ($book_link['nid']) { Chris@0: $variables['tree'] = $book_outline->childrenLinks($book_link); Chris@0: Chris@0: $build = []; Chris@0: Chris@0: if ($prev = $book_outline->prevLink($book_link)) { Chris@18: $prev_href = Url::fromRoute('entity.node.canonical', ['node' => $prev['nid']])->toString(); Chris@0: $build['#attached']['html_head_link'][][] = [ Chris@0: 'rel' => 'prev', Chris@0: 'href' => $prev_href, Chris@0: ]; Chris@0: $variables['prev_url'] = $prev_href; Chris@0: $variables['prev_title'] = $prev['title']; Chris@0: } Chris@0: Chris@0: /** @var \Drupal\book\BookManagerInterface $book_manager */ Chris@0: $book_manager = \Drupal::service('book.manager'); Chris@0: if ($book_link['pid'] && $parent = $book_manager->loadBookLink($book_link['pid'])) { Chris@18: $parent_href = Url::fromRoute('entity.node.canonical', ['node' => $book_link['pid']])->toString(); Chris@0: $build['#attached']['html_head_link'][][] = [ Chris@0: 'rel' => 'up', Chris@0: 'href' => $parent_href, Chris@0: ]; Chris@0: $variables['parent_url'] = $parent_href; Chris@0: $variables['parent_title'] = $parent['title']; Chris@0: } Chris@0: Chris@0: if ($next = $book_outline->nextLink($book_link)) { Chris@18: $next_href = Url::fromRoute('entity.node.canonical', ['node' => $next['nid']])->toString(); Chris@0: $build['#attached']['html_head_link'][][] = [ Chris@0: 'rel' => 'next', Chris@0: 'href' => $next_href, Chris@0: ]; Chris@0: $variables['next_url'] = $next_href; Chris@0: $variables['next_title'] = $next['title']; Chris@0: } Chris@0: } Chris@0: Chris@0: if (!empty($build)) { Chris@0: \Drupal::service('renderer')->render($build); Chris@0: } Chris@0: Chris@0: $variables['has_links'] = FALSE; Chris@0: // Link variables to filter for values and set state of the flag variable. Chris@0: $links = ['prev_url', 'prev_title', 'parent_url', 'parent_title', 'next_url', 'next_title']; Chris@0: foreach ($links as $link) { Chris@0: if (isset($variables[$link])) { Chris@0: // Flag when there is a value. Chris@0: $variables['has_links'] = TRUE; Chris@0: } Chris@0: else { Chris@0: // Set empty to prevent notices. Chris@0: $variables[$link] = ''; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for book export templates. Chris@0: * Chris@0: * Default template: book-export-html.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - title: The title of the book. Chris@0: * - contents: Output of each book page. Chris@0: * - depth: The max depth of the book. Chris@0: */ Chris@0: function template_preprocess_book_export_html(&$variables) { Chris@0: global $base_url; Chris@0: $language_interface = \Drupal::languageManager()->getCurrentLanguage(); Chris@0: Chris@0: $variables['base_url'] = $base_url; Chris@0: $variables['language'] = $language_interface; Chris@0: $variables['language_rtl'] = ($language_interface->getDirection() == LanguageInterface::DIRECTION_RTL); Chris@0: Chris@0: // HTML element attributes. Chris@0: $attributes = []; Chris@0: $attributes['lang'] = $language_interface->getId(); Chris@0: $attributes['dir'] = $language_interface->getDirection(); Chris@0: $variables['html_attributes'] = new Attribute($attributes); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for single node export templates. Chris@0: * Chris@0: * Default template: book-node-export-html.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing the following keys: Chris@0: * - node: The node that will be output. Chris@0: * - children: All the rendered child nodes within the current node. Defaults Chris@0: * to an empty string. Chris@0: */ Chris@0: function template_preprocess_book_node_export_html(&$variables) { Chris@0: $variables['depth'] = $variables['node']->book['depth']; Chris@0: $variables['title'] = $variables['node']->label(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines if a given node type is in the list of types allowed for books. Chris@0: * Chris@0: * @param string $type Chris@0: * A node type. Chris@0: * Chris@0: * @return bool Chris@0: * A Boolean TRUE if the node type can be included in books; otherwise, FALSE. Chris@0: */ Chris@0: function book_type_is_allowed($type) { Chris@0: return in_array($type, \Drupal::config('book.settings')->get('allowed_types')); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Implements hook_ENTITY_TYPE_update() for node_type entities. Chris@0: * Chris@0: * Updates book.settings configuration object if the machine-readable name of a Chris@0: * node type is changed. Chris@0: */ Chris@0: function book_node_type_update(NodeTypeInterface $type) { Chris@0: if ($type->getOriginalId() != $type->id()) { Chris@0: $config = \Drupal::configFactory()->getEditable('book.settings'); Chris@0: // Update the list of node types that are allowed to be added to books. Chris@0: $allowed_types = $config->get('allowed_types'); Chris@0: $old_key = array_search($type->getOriginalId(), $allowed_types); Chris@0: Chris@0: if ($old_key !== FALSE) { Chris@0: $allowed_types[$old_key] = $type->id(); Chris@0: // Ensure that the allowed_types array is sorted consistently. Chris@0: // @see BookSettingsForm::submitForm() Chris@0: sort($allowed_types); Chris@0: $config->set('allowed_types', $allowed_types); Chris@0: } Chris@0: Chris@0: // Update the setting for the "Add child page" link. Chris@0: if ($config->get('child_type') == $type->getOriginalId()) { Chris@0: $config->set('child_type', $type->id()); Chris@0: } Chris@0: $config->save(); Chris@0: } Chris@0: } Chris@16: Chris@16: /** Chris@16: * Implements hook_migration_plugins_alter(). Chris@16: */ Chris@16: function book_migration_plugins_alter(array &$migrations) { Chris@16: // Book settings are migrated identically for Drupal 6 and Drupal 7. However, Chris@16: // a d6_book_settings migration already existed before the consolidated Chris@16: // book_settings migration existed, so to maintain backwards compatibility, Chris@16: // ensure that d6_book_settings is an alias of book_settings. Chris@16: if (isset($migrations['book_settings'])) { Chris@16: $migrations['d6_book_settings'] = &$migrations['book_settings']; Chris@16: } Chris@16: }