Commit 377a9187 authored by webchick's avatar webchick
Browse files

Issue #1938316 by disasm, jaskho, stella, scor, tim.plunkett, jibran | Crell:...

Issue #1938316 by disasm, jaskho, stella, scor, tim.plunkett, jibran | Crell: Convert book_outline() to a new-style Controller.
parent bf37effc
......@@ -112,6 +112,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.
*
......@@ -190,13 +197,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',
......@@ -235,20 +238,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);
}
/**
......@@ -298,7 +289,7 @@ function book_form_node_form_alter(&$form, &$form_state, $form_id) {
}
if ($access) {
_book_add_form_elements($form, $form_state, $node);
$form = Drupal::service('book.manager')->addFormElements($form, $form_state, $node, $account);
// Since the "Book" dropdown can't trigger a form submission when
// JavaScript is disabled, add a submit button to do that. book.admin.css hides
// this button when JavaScript is enabled.
......@@ -342,145 +333,6 @@ function book_pick_book_nojs_submit($form, &$form_state) {
$form_state['rebuild'] = TRUE;
}
/**
* 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' => '<div id="edit-book-plid-wrapper">',
'#suffix' => '</div>',
);
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'] .= '<em>' . t('This is the top-level page in this book.') . '</em>';
}
else {
$form['#prefix'] .= '<em>' . t('This will be the top-level page in this book.') . '</em>';
}
}
elseif (!$book_link['bid']) {
$form['#prefix'] .= '<em>' . t('No book selected.') . '</em>';
}
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' => '<div id="edit-book-plid-wrapper">',
'#suffix' => '</div>',
);
}
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 (Drupal::currentUser()->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 => 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.
*
......@@ -494,102 +346,6 @@ function book_form_update($form, $form_state) {
return $form['book']['plid'];
}
/**
* 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.
*
......@@ -737,19 +493,6 @@ function book_children($book_link) {
return '';
}
/**
* 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().
*/
......@@ -819,14 +562,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);
}
}
......@@ -834,14 +578,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);
}
}
......@@ -858,7 +603,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']);
......@@ -873,6 +618,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.
$account = Drupal::currentUser();
if (empty($node->book) && ($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines'))) {
......@@ -890,7 +638,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'])) {
......@@ -899,23 +648,10 @@ 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().
*
......@@ -935,19 +671,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) {
......@@ -1039,68 +762,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.
*
......
......@@ -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().
......@@ -82,38 +21,6 @@ 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.
*
......@@ -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())
......