diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index 74b1986..786e7cb 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
@@ -189,13 +196,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',
@@ -234,20 +237,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')->checkNodeIsRemovable($node)
+ && _book_outline_access($node);
}
/**
@@ -286,17 +277,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.
@@ -341,145 +333,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
@@ -493,102 +346,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
@@ -736,19 +493,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) {
@@ -817,14 +561,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);
}
}
@@ -832,14 +577,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);
}
}
@@ -856,7 +602,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']);
@@ -871,6 +617,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();
@@ -887,7 +636,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->getLinkDefaults($node_ref);
}
else {
if (isset($node->book['bid']) && !isset($node->book['original_bid'])) {
@@ -896,24 +646,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->getParentDepthLimit($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.
@@ -932,19 +669,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) {
@@ -1037,68 +761,6 @@ function template_preprocess_book_navigation(&$variables) {
}
/**
- * Recursively processes and formats menu items for book_toc().
- *
- * This helper function recursively modifies the table of contents array for
- * each item in the menu tree, ignoring items in the exclude array or at a depth
- * greater than the limit. Truncates titles over thirty characters and appends
- * an indentation string incremented by depth.
- *
- * @param $tree
- * The data structure of the book's menu tree. Includes hidden links.
- * @param $indent
- * A string appended to each menu item title. Increments by '--' per depth
- * level.
- * @param $toc
- * Reference to the table of contents array. This is modified in place, so the
- * function does not have a return value.
- * @param $exclude
- * Optional array of menu link ID values. Any link whose menu link ID is in
- * this array will be excluded (along with its children).
- * @param $depth_limit
- * Any link deeper than this value will be excluded (along with its children).
- */
-function _book_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) {
- foreach ($tree as $data) {
- if ($data['link']['depth'] > $depth_limit) {
- // Don't iterate through any links on this level.
- break;
- }
-
- if (!in_array($data['link']['mlid'], $exclude)) {
- $toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
- if ($data['below']) {
- _book_toc_recurse($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
- }
- }
- }
-}
-
-/**
- * Returns an array of book pages in table of contents order.
- *
- * @param $bid
- * The ID of the book whose pages are to be listed.
- * @param $depth_limit
- * Any link deeper than this value will be excluded (along with its children).
- * @param $exclude
- * (optional) An array of menu link ID values. Any link whose menu link ID is
- * in this array will be excluded (along with its children). Defaults to an
- * empty array.
- *
- * @return
- * An array of (menu link ID, title) pairs for use as options for selecting a
- * book page.
- */
-function book_toc($bid, $depth_limit, $exclude = array()) {
- $tree = menu_tree_all_data(book_menu_name($bid));
- $toc = array();
- _book_toc_recurse($tree, '', $toc, $exclude, $depth_limit);
-
- return $toc;
-}
-
-/**
* Prepares variables for book export templates.
*
* Default template: book-export-html.html.twig.
diff --git a/core/modules/book/book.pages.inc b/core/modules/book/book.pages.inc
index d05a855..40e44b2 100644
--- a/core/modules/book/book.pages.inc
+++ b/core/modules/book/book.pages.inc
@@ -9,67 +9,6 @@
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-/**
- * 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().
@@ -83,38 +22,6 @@ function book_remove_button_submit($form, &$form_state) {
}
/**
- * 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
@@ -143,7 +50,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')->checkNodeIsRemovable($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 b44ccfb..93d6d2f 100644
--- a/core/modules/book/book.routing.yml
+++ b/core/modules/book/book.routing.yml
@@ -15,7 +15,7 @@ book_admin:
book_settings:
pattern: '/admin/structure/book/settings'
defaults:
- _form: 'Drupal\book\Form\BookSettingsForm'
+ _form: '\Drupal\book\Form\BookSettingsForm'
requirements:
_permission: 'administer site configuration'
@@ -28,3 +28,13 @@ book_export:
requirements:
_permission: 'access printer-friendly version'
_entity_access: 'node.view'
+
+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 e54cc62..510573a 100644
--- a/core/modules/book/book.services.yml
+++ b/core/modules/book/book.services.yml
@@ -1,7 +1,7 @@
services:
book.manager:
class: Drupal\book\BookManager
- arguments: ['@database', '@entity.manager']
+ arguments: ['@database', '@entity.manager', '@string_translation', '@config.factory']
book.export:
class: Drupal\book\BookExport
arguments: ['@entity.manager']
diff --git a/core/modules/book/lib/Drupal/book/BookManager.php b/core/modules/book/lib/Drupal/book/BookManager.php
index 19140fa..22ec086 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;
+
+ /**
+ * Config Factory Service Object.
+ *
+ * @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;
- $this->entityManager = $entityManager;
+ public function __construct(Connection $connection, EntityManager $entity_manager, TranslationInterface $translation, ConfigFactory $config_factory) {
+ $this->connection = $connection;
+ $this->entityManager = $entity_manager;
+ $this->translation = $translation;
+ $this->configFactory = $config_factory;
}
/**
@@ -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,382 @@ 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 getLinkDefaults($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 getParentDepthLimit(array $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 checkNodeIsRemovable(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();
+ }
+ 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);
+ }
+ }
+
+ return TRUE;
+ }
+
+ // Failed to save the menu link.
+ return FALSE;
+ }
+
+/**
+ * Translates a string to the current language or to a given language.
+ *
+ * See the t() documentation for details.
+ */
+ 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.
+ */
+ public 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 array
+ * A parent selection form element.
+ */
+ protected function addParentSelectFormElements(array $book_link) {
+ if ($this->configFactory->get('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'] .= '' . $this->t('This is the top-level page in this book.') . '';
+ }
+ else {
+ $form['#prefix'] .= '' . $this->t('This will be the top-level page in this book.') . '';
+ }
+ }
+ elseif (!$book_link['bid']) {
+ $form['#prefix'] .= '' . $this->t('No book selected.') . '';
+ }
+ else {
+ $form = array(
+ '#type' => 'select',
+ '#title' => $this->t('Parent item'),
+ '#default_value' => $book_link['plid'],
+ '#description' => $this->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' => $this->getTableOfContents($book_link['bid'], $book_link['parent_depth_limit'], array($book_link['mlid'])),
+ '#attributes' => array('class' => array('book-title-select')),
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ }
+
+ return $form;
+ }
+
+ /**
+ * Recursively processes and formats menu items for getTableOfContents().
+ *
+ * This helper function recursively modifies the table of contents array for
+ * each item in the menu tree, ignoring items in the exclude array or at a depth
+ * greater than the limit. Truncates titles over thirty characters and appends
+ * an indentation string incremented by depth.
+ *
+ * @param array $tree
+ * The data structure of the book's menu tree. Includes hidden links.
+ * @param string $indent
+ * A string appended to each menu item title. Increments by '--' per depth
+ * level.
+ * @param array $toc
+ * Reference to the table of contents array. This is modified in place, so the
+ * function does not have a return value.
+ * @param array $exclude
+ * Optional array of menu link ID values. Any link whose menu link ID is in
+ * this array will be excluded (along with its children).
+ * @param int $depth_limit
+ * Any link deeper than this value will be excluded (along with its children).
+ */
+ protected function recurseTableOfContents(array $tree, $indent, array &$toc, array $exclude, $depth_limit) {
+ foreach ($tree as $data) {
+ if ($data['link']['depth'] > $depth_limit) {
+ // Don't iterate through any links on this level.
+ break;
+ }
+
+ if (!in_array($data['link']['mlid'], $exclude)) {
+ $toc[$data['link']['mlid']] = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, TRUE);
+ if ($data['below']) {
+ $this->recurseTableOfContents($data['below'], $indent . '--', $toc, $exclude, $depth_limit);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns an array of book pages in table of contents order.
+ *
+ * @param int $bid
+ * The ID of the book whose pages are to be listed.
+ * @param int $depth_limit
+ * Any link deeper than this value will be excluded (along with its children).
+ * @param array $exclude
+ * (optional) An array of menu link ID values. Any link whose menu link ID is
+ * in this array will be excluded (along with its children). Defaults to an
+ * empty array.
+ *
+ * @return array
+ * An array of (menu link ID, title) pairs for use as options for selecting a
+ * book page.
+ */
+ public function getTableOfContents($bid, $depth_limit, array $exclude = array()) {
+ $tree = menu_tree_all_data($this->createMenuName($bid));
+ $toc = array();
+ $this->recurseTableOfContents($tree, '', $toc, $exclude, $depth_limit);
+
+ return $toc;
+ }
+
}
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..549fb04
--- /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->getLinkDefaults($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->getParentDepthLimit($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->checkNodeIsRemovable($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';
+ }
+
+}