diff --git a/core/modules/book/book.module b/core/modules/book/book.module index 525056e..7248352 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -113,6 +113,13 @@ function book_permission() { } /** + * Implements hook_entity_info(). + */ +function book_entity_info(&$entity_info) { + $entity_info['node']['controllers']['form']['book_outline'] = '\Drupal\book\Form\BookOutlineForm'; +} + +/** * Adds relevant book links to the node's links. * * @param \Drupal\Core\Entity\EntityInterface $node @@ -197,13 +204,9 @@ function book_menu() { ); $items['node/%node/outline'] = array( 'title' => 'Outline', - 'page callback' => 'book_outline', - 'page arguments' => array(1), - 'access callback' => '_book_outline_access', - 'access arguments' => array(1), + 'route_name' => 'book_outline', 'type' => MENU_LOCAL_TASK, 'weight' => 2, - 'file' => 'book.pages.inc', ); $items['node/%node/outline/remove'] = array( 'title' => 'Remove from outline', @@ -252,20 +255,8 @@ function _book_outline_access(EntityInterface $node) { * @see book_menu() */ function _book_outline_remove_access(EntityInterface $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. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * The node to remove from the outline. - */ -function _book_node_is_removable(EntityInterface $node) { - return (!empty($node->book['bid']) && (($node->book['bid'] != $node->id()) || !$node->book['has_children'])); + return Drupal::service('book.manager')->nodeIsRemovable($node) + && _book_outline_access($node); } /** @@ -304,17 +295,18 @@ function book_get_books() { * @see book_pick_book_nojs_submit() */ function book_form_node_form_alter(&$form, &$form_state, $form_id) { + $user = Drupal::currentUser(); $node = $form_state['controller']->getEntity(); - $access = user_access('administer book outlines'); + $access = $user->hasPermission('administer book outlines'); if (!$access) { - if (user_access('add content to books') && ((!empty($node->book['mlid']) && !$node->isNew()) || book_type_is_allowed($node->getType()))) { + if ($user->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) { - _book_add_form_elements($form, $form_state, $node); + $form = Drupal::service('book.manager')->addFormElements($form, $form_state, $node, $user); // 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. @@ -359,145 +351,6 @@ function book_pick_book_nojs_submit($form, &$form_state) { } /** - * 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 (Drupal::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' => '
', - '#suffix' => '
', - ); - - if ($book_link['nid'] === $book_link['bid']) { - // This is a book - at the top level. - if ($book_link['original_bid'] === $book_link['bid']) { - $form['#prefix'] .= '' . t('This is the top-level page in this book.') . ''; - } - else { - $form['#prefix'] .= '' . t('This will be the top-level page in this book.') . ''; - } - } - elseif (!$book_link['bid']) { - $form['#prefix'] .= '' . t('No book selected.') . ''; - } - else { - $form = array( - '#type' => 'select', - '#title' => t('Parent item'), - '#default_value' => $book_link['plid'], - '#description' => t('The parent page in the book. The maximum depth for a book and all child pages is !maxdepth. Some pages in the selected book may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)), - '#options' => book_toc($book_link['bid'], $book_link['parent_depth_limit'], array($book_link['mlid'])), - '#attributes' => array('class' => array('book-title-select')), - '#prefix' => '
', - '#suffix' => '
', - ); - } - - return $form; -} - -/** - * Builds the common elements of the book form for the node and outline forms. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * The node whose form is being viewed. - */ -function _book_add_form_elements(&$form, &$form_state, EntityInterface $node) { - // If the form is being processed during the Ajax callback of our book bid - // dropdown, then $form_state will hold the value that was selected. - if (isset($form_state['values']['book'])) { - $node->book = $form_state['values']['book']; - } - - $form['book'] = array( - '#type' => 'details', - '#title' => t('Book outline'), - '#weight' => 10, - '#collapsed' => TRUE, - '#group' => 'advanced', - '#attributes' => array( - 'class' => array('book-outline-form'), - ), - '#attached' => array( - 'library' => array(array('book', 'drupal.book')), - ), - '#tree' => TRUE, - ); - foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'original_bid', 'parent_depth_limit') as $key) { - $form['book'][$key] = array( - '#type' => 'value', - '#value' => $node->book[$key], - ); - } - - $form['book']['plid'] = _book_parent_select($node->book); - - // @see _book_admin_table_tree(). The weight may be larger than 15. - $form['book']['weight'] = array( - '#type' => 'weight', - '#title' => t('Weight'), - '#default_value' => $node->book['weight'], - '#delta' => max(15, abs($node->book['weight'])), - '#weight' => 5, - '#description' => t('Pages at a given level are ordered first by weight and then by title.'), - ); - $options = array(); - $nid = !$node->isNew() ? $node->id() : 'new'; - - if ($node->id() && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) { - // This is the top level node in a maximum depth book and thus cannot be moved. - $options[$node->id()] = $node->label(); - } - else { - foreach (book_get_books() as $book) { - $options[$book['nid']] = $book['title']; - } - } - - if (user_access('create new books') && ($nid == 'new' || ($nid != $node->book['original_bid']))) { - // 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['mlid']) { - // The node is not currently in the hierarchy. - $options = array(0 => t('- None -')) + $options; - } - - // Add a drop-down to select the destination book. - $form['book']['bid'] = array( - '#type' => 'select', - '#title' => t('Book'), - '#default_value' => $node->book['bid'], - '#options' => $options, - '#access' => (bool) $options, - '#description' => t('Your page will be a part of the selected book.'), - '#weight' => -5, - '#attributes' => array('class' => array('book-title-select')), - '#ajax' => array( - 'callback' => 'book_form_update', - 'wrapper' => 'edit-book-plid-wrapper', - 'effect' => 'fade', - 'speed' => 'fast', - ), - ); -} - -/** * 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 @@ -511,102 +364,6 @@ function book_form_update($form, $form_state) { } /** - * Handles additions and updates to the book outline. - * - * This common helper function performs all additions and updates to the book - * outline through node addition, node editing, node deletion, or the outline - * tab. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * The node that is being saved, added, deleted, or moved. - * - * @return - * TRUE if the menu link was saved; FALSE otherwise. - */ -function _book_update_outline(EntityInterface $node) { - if (empty($node->book['bid'])) { - return FALSE; - } - $new = empty($node->book['mlid']); - - $node->book['link_path'] = 'node/' . $node->id(); - $node->book['link_title'] = $node->label(); - $node->book['parent_mismatch'] = FALSE; // The normal case. - - if ($node->book['bid'] == $node->id()) { - $node->book['plid'] = 0; - $node->book['menu_name'] = book_menu_name($node->id()); - } - else { - // Check in case the parent is not is this book; the book takes precedence. - if (!empty($node->book['plid'])) { - $parent = db_query("SELECT * FROM {book} WHERE mlid = :mlid", array( - ':mlid' => $node->book['plid'], - ))->fetchAssoc(); - } - if (empty($node->book['plid']) || !$parent || $parent['bid'] != $node->book['bid']) { - $node->book['plid'] = db_query("SELECT mlid FROM {book} WHERE nid = :nid", array( - ':nid' => $node->book['bid'], - ))->fetchField(); - $node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled. - } - } - - $node->book = entity_create('menu_link', $node->book); - if ($node->book->save()) { - if ($new) { - // Insert new. - db_insert('book') - ->fields(array( - 'nid' => $node->id(), - 'mlid' => $node->book['mlid'], - 'bid' => $node->book['bid'], - )) - ->execute(); - // Reset the cache of stored books. - drupal_static_reset('book_get_books'); - } - else { - if ($node->book['bid'] != db_query("SELECT bid FROM {book} WHERE nid = :nid", array( - ':nid' => $node->id(), - ))->fetchField()) { - // Update the bid for this page and all children. - book_update_bid($node->book); - // Reset the cache of stored books. - drupal_static_reset('book_get_books'); - } - } - - return TRUE; - } - - // Failed to save the menu link. - return FALSE; -} - -/** - * Updates the book ID of a page and its children when it moves to a new book. - * - * @param $book_link - * A fully loaded menu link that is part of the book hierarchy. - */ -function book_update_bid($book_link) { - $query = db_select('menu_links'); - $query->addField('menu_links', 'mlid'); - for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) { - $query->condition("p$i", $book_link["p$i"]); - } - $mlids = $query->execute()->fetchCol(); - - if ($mlids) { - db_update('book') - ->fields(array('bid' => $book_link['bid'])) - ->condition('mlid', $mlids, 'IN') - ->execute(); - } -} - -/** * Gets the book menu tree for a page and returns it as a linear array. * * @param $book_link @@ -754,19 +511,6 @@ function book_children($book_link) { } /** - * Generates the corresponding menu name from a book ID. - * - * @param $bid - * The book ID for which to make a menu name. - * - * @return - * The menu name. - */ -function book_menu_name($bid) { - return 'book-toc-' . $bid; -} - -/** * Implements hook_node_load(). */ function book_node_load($nodes, $types) { @@ -835,14 +579,15 @@ function book_node_presave(EntityInterface $node) { * 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_menu_name($node->book['bid']); - _book_update_outline($node); + $node->book['menu_name'] = $book_manager->createMenuName($node->book['bid']); + $book_manager->updateOutline($node); } } @@ -850,14 +595,15 @@ function book_node_insert(EntityInterface $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_menu_name($node->book['bid']); - _book_update_outline($node); + $node->book['menu_name'] = $book_manager->createMenuName($node->book['bid']); + $book_manager->updateOutline($node); } } @@ -874,7 +620,7 @@ function book_node_predelete(EntityInterface $node) { foreach ($result as $child) { $child_node = node_load($child->id()); $child_node->book['bid'] = $child_node->id(); - _book_update_outline($child_node); + Drupal::service('book.manager')->updateOutline($child_node); } } menu_link_delete($node->book['mlid']); @@ -889,6 +635,9 @@ function book_node_predelete(EntityInterface $node) { * 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. if (empty($node->book) && (user_access('add content to books') || user_access('administer book outlines'))) { $node->book = array(); @@ -905,7 +654,8 @@ function book_node_prepare_form(NodeInterface $node, $form_display, $operation, } } // Set defaults. - $node->book += _book_link_defaults(!$node->isNew() ? $node->id() : 'new'); + $node_ref = !$node->isNew() ? $node->id() : 'new'; + $node->book += $book_manager->linkDefaults($node_ref); } else { if (isset($node->book['bid']) && !isset($node->book['original_bid'])) { @@ -914,24 +664,11 @@ function book_node_prepare_form(NodeInterface $node, $form_display, $operation, } // 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_parent_depth_limit($node->book); + $node->book['parent_depth_limit'] = $book_manager->parentDepthLimit($node->book); } } /** - * Finds the depth limit for items in the parent select. - * - * @param $book_link - * A fully loaded menu link that is part of the book hierarchy. - * - * @return - * The depth limit for items in the parent select. - */ -function _book_parent_depth_limit($book_link) { - return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($book_link) : 0); -} - -/** * Implements hook_form_FORM_ID_alter() for node_delete_confirm(). * * Alters the confirm form for a single node deletion. @@ -950,19 +687,6 @@ function book_form_node_delete_confirm_alter(&$form, $form_state) { } /** - * 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.html.twig. */ function book_preprocess_block(&$variables) { @@ -1109,7 +833,7 @@ function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) { * book page. */ function book_toc($bid, $depth_limit, $exclude = array()) { - $tree = menu_tree_all_data(book_menu_name($bid)); + $tree = menu_tree_all_data(Drupal::service('book.manager')->createMenuName($bid)); $toc = array(); _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit); diff --git a/core/modules/book/book.pages.inc b/core/modules/book/book.pages.inc index 31410a7..c059633 100644 --- a/core/modules/book/book.pages.inc +++ b/core/modules/book/book.pages.inc @@ -85,111 +85,6 @@ function book_export_html(EntityInterface $node) { } /** - * Page callback: Shows the outline form for a single node. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * The book node for which to show the outline. - * - * @return string - * A HTML-formatted string with the outline form for a single node. - * - * @see book_menu() - */ -function book_outline(EntityInterface $node) { - drupal_set_title($node->label()); - return drupal_get_form('book_outline_form', $node); -} - -/** - * Form constructor for the book outline form. - * - * Allows handling of all book outline operations via the outline tab. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * The book node for which to show the outline. - * - * @see book_outline_form_submit() - * @see book_remove_button_submit() - * @ingroup forms - */ -function book_outline_form($form, &$form_state, EntityInterface $node) { - if (!isset($node->book)) { - // The node is not part of any book yet - set default options. - $node->book = _book_link_defaults($node->id()); - } - else { - $node->book['original_bid'] = $node->book['bid']; - } - - // Find the depth limit for the parent select. - if (!isset($node->book['parent_depth_limit'])) { - $node->book['parent_depth_limit'] = _book_parent_depth_limit($node->book); - } - $form['#node'] = $node; - $form['#id'] = 'book-outline'; - _book_add_form_elements($form, $form_state, $node); - - $form['update'] = array( - '#type' => 'submit', - '#value' => $node->book['original_bid'] ? t('Update book outline') : t('Add to book outline'), - '#weight' => 15, - ); - - $form['remove'] = array( - '#type' => 'submit', - '#value' => t('Remove from book outline'), - '#access' => _book_node_is_removable($node), - '#weight' => 20, - '#submit' => array('book_remove_button_submit'), - ); - - return $form; -} - -/** - * Form submission handler for book_outline_form(). - * - * Redirects to removal confirmation form. - * - * @see book_outline_form_submit() - */ -function book_remove_button_submit($form, &$form_state) { - $form_state['redirect'] = 'node/' . $form['#node']->id() . '/outline/remove'; -} - -/** - * Form submission handler for book_outline_form(). - * - * @see book_remove_button_submit() - */ -function book_outline_form_submit($form, &$form_state) { - $node = $form['#node']; - $form_state['redirect'] = "node/" . $node->id(); - $book_link = $form_state['values']['book']; - if (!$book_link['bid']) { - drupal_set_message(t('No changes were made')); - - return; - } - - $book_link['menu_name'] = book_menu_name($book_link['bid']); - $node->book = $book_link; - if (_book_update_outline($node)) { - if ($node->book['parent_mismatch']) { - // This will usually only happen when JS is disabled. - drupal_set_message(t('The post has been added to the selected book. You may now position it relative to other pages.')); - $form_state['redirect'] = "node/" . $node->id() . "/outline"; - } - else { - drupal_set_message(t('The book outline has been updated.')); - } - } - else { - drupal_set_message(t('There was an error adding the post to the book.'), 'error'); - } -} - -/** * Form constructor to confirm removal of a node from a book. * * @param \Drupal\Core\Entity\EntityInterface $node @@ -218,7 +113,7 @@ function book_remove_form($form, &$form_state, EntityInterface $node) { */ function book_remove_form_submit($form, &$form_state) { $node = $form['#node']; - if (_book_node_is_removable($node)) { + if (Drupal::service('book.manager')->nodeIsRemovable($node)) { menu_link_delete($node->book['mlid']); db_delete('book') ->condition('nid', $node->id()) diff --git a/core/modules/book/book.routing.yml b/core/modules/book/book.routing.yml index 0ce057a..414daf5 100644 --- a/core/modules/book/book.routing.yml +++ b/core/modules/book/book.routing.yml @@ -18,3 +18,13 @@ book_settings: _form: 'Drupal\book\Form\BookSettingsForm' requirements: _permission: 'administer site configuration' + +book_outline: + pattern: '/node/{node}/outline' + defaults: + _entity_form: 'node.book_outline' + options: + _access_mode: 'ALL' + requirements: + _permission: 'administer book outlines' + _entity_access: 'node.view' diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml index 9d8c140..64e560e 100644 --- a/core/modules/book/book.services.yml +++ b/core/modules/book/book.services.yml @@ -1,4 +1,4 @@ services: book.manager: class: Drupal\book\BookManager - arguments: ['@database', '@plugin.manager.entity'] + arguments: ['@database', '@plugin.manager.entity', '@string_translation', '@config.factory'] diff --git a/core/modules/book/lib/Drupal/book/BookManager.php b/core/modules/book/lib/Drupal/book/BookManager.php index 19140fa..237ab6f 100644 --- a/core/modules/book/lib/Drupal/book/BookManager.php +++ b/core/modules/book/lib/Drupal/book/BookManager.php @@ -8,7 +8,11 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityManager; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Core\Config\ConfigFactory; +use Drupal\node\NodeInterface; /** * Book Manager Service. @@ -20,7 +24,7 @@ class BookManager { * * @var \Drupal\Core\Database\Connection */ - protected $database; + protected $connection; /** * Entity manager Service Object. @@ -30,6 +34,20 @@ class BookManager { protected $entityManager; /** + * The translation service. + * + * @var \Drupal\Core\StringTranslation\TranslationInterface + */ + protected $translation; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactory + */ + protected $configFactory; + + /** * Books Array. * * @var array @@ -39,9 +57,11 @@ class BookManager { /** * Constructs a BookManager object. */ - public function __construct(Connection $database, EntityManager $entityManager) { - $this->database = $database; + public function __construct(Connection $connection, EntityManager $entityManager, TranslationInterface $translation, ConfigFactory $configFactory) { + $this->connection = $connection; $this->entityManager = $entityManager; + $this->translation = $translation; + $this->ConfigFactory = $configFactory; } /** @@ -65,10 +85,10 @@ public function getAllBooks() { */ protected function loadBooks() { $this->books = array(); - $nids = $this->database->query("SELECT DISTINCT(bid) FROM {book}")->fetchCol(); + $nids = $this->connection->query("SELECT DISTINCT(bid) FROM {book}")->fetchCol(); if ($nids) { - $query = $this->database->select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC)); + $query = $this->connection->select('book', 'b', array('fetch' => \PDO::FETCH_ASSOC)); $query->join('menu_links', 'ml', 'b.mlid = ml.mlid'); $query->fields('b'); $query->fields('ml'); @@ -94,4 +114,339 @@ protected function loadBooks() { } } + /** + * Returns an array with default values for a book page's menu link. + * + * @param string|int $nid + * The ID of the node whose menu link is being created. + * + * @return array + * The default values for the menu link. + */ + public function linkDefaults($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(), + ); + } + + /** + * Finds the depth limit for items in the parent select. + * + * @param array $book_link + * A fully loaded menu link that is part of the book hierarchy. + * + * @return int + * The depth limit for items in the parent select. + */ + public function parentDepthLimit($book_link) { + return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? $this->entityManager->getStorageController('menu_link')->findChildrenRelativeDepth($book_link) : 0); + } + + /** + * Builds the common elements of the book form for the node and outline forms. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * @param \Drupal\node\NodeInterface $node + * The node whose form is being viewed. + * @param \Drupal\Core\Session\AccountInterface $account + * The account viewing the form. + * + * @return array + * The form structure, with the book elements added. + */ + public function addFormElements(array $form, array &$form_state, NodeInterface $node, AccountInterface $account) { + // If the form is being processed during the Ajax callback of our book bid + // dropdown, then $form_state will hold the value that was selected. + if (isset($form_state['values']['book'])) { + $node->book = $form_state['values']['book']; + } + $form['book'] = array( + '#type' => 'details', + '#title' => $this->t('Book outline'), + '#weight' => 10, + '#collapsed' => TRUE, + '#group' => 'advanced', + '#attributes' => array( + 'class' => array('book-outline-form'), + ), + '#attached' => array( + 'library' => array(array('book', 'drupal.book')), + ), + '#tree' => TRUE, + ); + foreach (array('menu_name', 'mlid', 'nid', 'router_path', 'has_children', 'options', 'module', 'original_bid', 'parent_depth_limit') as $key) { + $form['book'][$key] = array( + '#type' => 'value', + '#value' => $node->book[$key], + ); + } + + $form['book']['plid'] = $this->addParentSelectFormElements($node->book); + + // @see _book_admin_table_tree(). The weight may be larger than 15. + $form['book']['weight'] = array( + '#type' => 'weight', + '#title' => $this->t('Weight'), + '#default_value' => $node->book['weight'], + '#delta' => max(15, abs($node->book['weight'])), + '#weight' => 5, + '#description' => $this->t('Pages at a given level are ordered first by weight and then by title.'), + ); + $options = array(); + $nid = !$node->isNew() ? $node->id() : 'new'; + if ($node->id() && ($nid == $node->book['original_bid']) && ($node->book['parent_depth_limit'] == 0)) { + // This is the top level node in a maximum depth book and thus cannot be moved. + $options[$node->id()] = $node->label(); + } + else { + foreach ($this->getAllBooks() as $book) { + $options[$book['nid']] = $book['title']; + } + } + + if ($account->hasPermission('create new books') && ($nid == 'new' || ($nid != $node->book['original_bid']))) { + // The node can become a new book, if it is not one already. + $options = array($nid => $this->t('- Create a new book -')) + $options; + } + if (!$node->book['mlid']) { + // The node is not currently in the hierarchy. + $options = array(0 => $this->t('- None -')) + $options; + } + + // Add a drop-down to select the destination book. + $form['book']['bid'] = array( + '#type' => 'select', + '#title' => $this->t('Book'), + '#default_value' => $node->book['bid'], + '#options' => $options, + '#access' => (bool) $options, + '#description' => $this->t('Your page will be a part of the selected book.'), + '#weight' => -5, + '#attributes' => array('class' => array('book-title-select')), + '#ajax' => array( + 'callback' => 'book_form_update', + 'wrapper' => 'edit-book-plid-wrapper', + 'effect' => 'fade', + 'speed' => 'fast', + ), + ); + return $form; + } + + /** + * 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. + * + * @param \Drupal\node\NodeInterface $node + * The node to remove from the outline. + * + * @return bool + * TRUE if a node can be removed from the book, FALSE otherwise. + */ + public function nodeIsRemovable(NodeInterface $node) { + return (!empty($node->book['bid']) && (($node->book['bid'] != $node->id()) || !$node->book['has_children'])); + } + + /** + * Handles additions and updates to the book outline. + * + * This common helper function performs all additions and updates to the book + * outline through node addition, node editing, node deletion, or the outline + * tab. + * + * @param \Drupal\node\NodeInterface $node + * The node that is being saved, added, deleted, or moved. + * + * @return bool + * TRUE if the menu link was saved; FALSE otherwise. + */ + public function updateOutline(NodeInterface $node) { + if (empty($node->book['bid'])) { + return FALSE; + } + $new = empty($node->book['mlid']); + + $node->book['link_path'] = 'node/' . $node->id(); + $node->book['link_title'] = $node->label(); + $node->book['parent_mismatch'] = FALSE; // The normal case. + + if ($node->book['bid'] == $node->id()) { + $node->book['plid'] = 0; + $node->book['menu_name'] = $this->createMenuName($node->id()); + } + else { + // Check in case the parent is not is this book; the book takes precedence. + if (!empty($node->book['plid'])) { + $parent = $this->connection->query("SELECT * FROM {book} WHERE mlid = :mlid", array( + ':mlid' => $node->book['plid'], + ))->fetchAssoc(); + } + if (empty($node->book['plid']) || !$parent || $parent['bid'] != $node->book['bid']) { + $node->book['plid'] = $this->connection->query("SELECT mlid FROM {book} WHERE nid = :nid", array( + ':nid' => $node->book['bid'], + ))->fetchField(); + $node->book['parent_mismatch'] = TRUE; // Likely when JS is disabled. + } + } + + $node->book = $this->entityManager + ->getStorageController('menu_link')->create($node->book); + if ($node->book->save()) { + if ($new) { + // Insert new. + $this->connection->insert('book') + ->fields(array( + 'nid' => $node->id(), + 'mlid' => $node->book['mlid'], + 'bid' => $node->book['bid'], + )) + ->execute(); + // Reset the cache of stored books. + drupal_static_reset('book_get_books'); + } + else { + if ($node->book['bid'] != $this->connection->query("SELECT bid FROM {book} WHERE nid = :nid", array( + ':nid' => $node->id(), + ))->fetchField()) { + // Update the bid for this page and all children. + $this->updateID($node->book); + // Reset the cache of stored books. + drupal_static_reset('book_get_books'); + } + } + + return TRUE; + } + + // Failed to save the menu link. + return FALSE; + } + + /** + * Translates a string to the current language or to a given language. + * + * @param string $string + * A string containing the English string to translate. + * @param array $args + * An associative array of replacements to make after translation. Based + * on the first character of the key, the value is escaped and/or themed. + * See \Drupal\Core\Utility\String::format() for details. + * @param array $options + * An associative array of additional options, with the following elements: + * - 'langcode': The language code to translate to a language other than + * what is used to display the page. + * - 'context': The context the source string belongs to. + * + * @return string + * The translated string. + * + * @todo Remove when https://drupal.org/node/2018411 is in. + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->translation->translate($string, $args, $options); + } + + /** + * Generates the corresponding menu name from a book ID. + * + * @param $id + * The book ID for which to make a menu name. + * + * @return + * The menu name. + */ + public function createMenuName($id) { + return 'book-toc-' . $id; + } + + /** + * Updates the book ID of a page and its children when it moves to a new book. + * + * @param array $book_link + * A fully loaded menu link that is part of the book hierarchy. + */ + function updateID($book_link) { + $query = $this->connection->select('menu_links'); + $query->addField('menu_links', 'mlid'); + for ($i = 1; $i <= MENU_MAX_DEPTH && $book_link["p$i"]; $i++) { + $query->condition("p$i", $book_link["p$i"]); + } + $mlids = $query->execute()->fetchCol(); + + if ($mlids) { + $this->connection->update('book') + ->fields(array('bid' => $book_link['bid'])) + ->condition('mlid', $mlids, 'IN') + ->execute(); + } + } + + /** + * 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 array $book_link + * A fully loaded menu link that is part of the book hierarchy. + * + * @return + * A parent selection form element. + */ + function addParentSelectFormElements($book_link) { + if ($this->configFactory('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' => '
', + '#suffix' => '
', + ); + + if ($book_link['nid'] === $book_link['bid']) { + // This is a book - at the top level. + if ($book_link['original_bid'] === $book_link['bid']) { + $form['#prefix'] .= '' . t('This is the top-level page in this book.') . ''; + } + else { + $form['#prefix'] .= '' . t('This will be the top-level page in this book.') . ''; + } + } + elseif (!$book_link['bid']) { + $form['#prefix'] .= '' . t('No book selected.') . ''; + } + else { + $form = array( + '#type' => 'select', + '#title' => t('Parent item'), + '#default_value' => $book_link['plid'], + '#description' => t('The parent page in the book. The maximum depth for a book and all child pages is !maxdepth. Some pages in the selected book may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)), + '#options' => book_toc($book_link['bid'], $book_link['parent_depth_limit'], array($book_link['mlid'])), + '#attributes' => array('class' => array('book-title-select')), + '#prefix' => '
', + '#suffix' => '
', + ); + } + + return $form; + } + } diff --git a/core/modules/book/lib/Drupal/book/Form/BookOutlineForm.php b/core/modules/book/lib/Drupal/book/Form/BookOutlineForm.php new file mode 100644 index 0000000..b60ef0f --- /dev/null +++ b/core/modules/book/lib/Drupal/book/Form/BookOutlineForm.php @@ -0,0 +1,128 @@ +bookManager = $bookManager; + } + + /** + * This method lets us inject the services this class needs. + * + * Only inject services that are actually needed. Which services + * are needed will vary by the controller. + */ + public static function create(ContainerInterface $container) { + return new static($container->get('book.manager')); + } + + /** + * {@inheritdoc} + */ + public function getBaseFormID() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, array &$form_state) { + $form['#title'] = $this->entity->label(); + + if (!isset($this->entity->book)) { + // The node is not part of any book yet - set default options. + $this->entity->book = $this->bookManager->linkDefaults($this->entity->id()); + } + else { + $this->entity->book['original_bid'] = $this->entity->book['bid']; + } + + // Find the depth limit for the parent select. + if (!isset($this->entity->book['parent_depth_limit'])) { + $this->entity->book['parent_depth_limit'] = $this->bookManager->parentDepthLimit($this->entity->book); + } + $form = $this->bookManager->addFormElements($form, $form_state, $this->entity, $this->getCurrentUser()); + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, array &$form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->entity->book['original_bid'] ? $this->t('Update book outline') : $this->t('Add to book outline'); + $actions['delete']['#value'] = $this->t('Remove from book outline'); + $actions['delete']['#access'] = $this->bookManager->nodeIsRemovable($this->entity); + return $actions; + } + + /** + * {@inheritdoc} + * + * @see book_remove_button_submit() + */ + public function submit(array $form, array &$form_state) { + $form_state['redirect'] = 'node/' . $this->entity->id(); + $book_link = $form_state['values']['book']; + if (!$book_link['bid']) { + drupal_set_message($this->t('No changes were made')); + return; + } + + $book_link['menu_name'] = $this->bookManager->createMenuName($book_link['bid']); + $this->entity->book = $book_link; + if ($this->bookManager->updateOutline($this->entity)) { + if ($this->entity->book['parent_mismatch']) { + // This will usually only happen when JS is disabled. + drupal_set_message($this->t('The post has been added to the selected book. You may now position it relative to other pages.')); + $form_state['redirect'] = 'node/' . $this->entity->id() . '/outline'; + } + else { + drupal_set_message($this->t('The book outline has been updated.')); + } + } + else { + drupal_set_message($this->t('There was an error adding the post to the book.'), 'error'); + } + } + + /** + * {@inheritdoc} + */ + public function delete(array $form, array &$form_state) { + $form_state['redirect'] = 'node/' . $this->entity->id() . '/outline/remove'; + } + +}