Commit 2b1f8e6a authored by alexpott's avatar alexpott

Issue #507488 by lauriii, mdrummond, Wim Leers, Manuel Garcia, pwolanin,...

Issue #507488 by lauriii, mdrummond, Wim Leers, Manuel Garcia, pwolanin, davidhernandez, chx, jibran, andypost, dawehner, Xano, tuutti, Gábor Hojtsy, LewisNyman, maximiliam, eiriksm, borisson_, bill richardson, Fabianx, joelpittet, Jeff Burnz, xjm, webchick, Bojhan, EclipseGc, emma.maria, tim.plunkett: Convert page elements (local tasks, actions) into blocks
parent 663848e1
......@@ -93,7 +93,7 @@ function menu_list_system_menus() {
}
/**
* Collects the local tasks (tabs), action links, and the root path.
* Collects the local tasks (tabs) for the current route.
*
* @param int $level
* The level of tasks you ask for. Primary tasks are 0, secondary are 1.
......@@ -101,95 +101,45 @@ function menu_list_system_menus() {
* @return array
* An array containing
* - tabs: Local tasks for the requested level.
* - actions: Action links for the requested level.
* - root_path: The router path for the current page. If the current page is
* a default local task, then this corresponds to the parent tab.
* - route_name: The route name for the current page used to collect the local
* tasks.
*
* @see hook_menu_local_tasks()
* @see hook_menu_local_tasks_alter()
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
*/
function menu_local_tasks($level = 0) {
$data = &drupal_static(__FUNCTION__);
$root_path = &drupal_static(__FUNCTION__ . ':root_path', '');
$empty = array(
'tabs' => array(),
'actions' => array(),
'root_path' => &$root_path,
);
if (!isset($data)) {
// Look for route-based tabs.
$data['tabs'] = array();
$data['actions'] = array();
$route_name = \Drupal::routeMatch()->getRouteName();
if (!\Drupal::request()->attributes->has('exception') && !empty($route_name)) {
$manager = \Drupal::service('plugin.manager.menu.local_task');
$local_tasks = $manager->getTasksBuild($route_name);
foreach ($local_tasks as $level => $items) {
$data['tabs'][$level] = empty($data['tabs'][$level]) ? $items : array_merge($data['tabs'][$level], $items);
}
}
// Allow modules to dynamically add further tasks.
$module_handler = \Drupal::moduleHandler();
foreach ($module_handler->getImplementations('menu_local_tasks') as $module) {
$function = $module . '_menu_local_tasks';
$function($data, $route_name);
}
// Allow modules to alter local tasks.
$module_handler->alter('menu_local_tasks', $data, $route_name);
}
if (isset($data['tabs'][$level])) {
return array(
'tabs' => $data['tabs'][$level],
'actions' => $data['actions'],
'root_path' => $root_path,
);
}
elseif (!empty($data['actions'])) {
return array('actions' => $data['actions']) + $empty;
}
return $empty;
/** @var \Drupal\Core\Menu\LocalTaskManagerInterface $manager */
$manager = \Drupal::service('plugin.manager.menu.local_task');
return $manager->getLocalTasks(\Drupal::routeMatch()->getRouteName(), $level);
}
/**
* Returns the rendered local tasks at the top level.
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
*/
function menu_primary_local_tasks() {
$links = menu_local_tasks(0);
/** @var \Drupal\Core\Menu\LocalTaskManagerInterface $manager */
$manager = \Drupal::service('plugin.manager.menu.local_task');
$links = $manager->getLocalTasks(\Drupal::routeMatch()->getRouteName(), 0);
// Do not display single tabs.
return count(Element::getVisibleChildren($links['tabs'])) > 1 ? $links['tabs'] : '';
}
/**
* Returns the rendered local tasks at the second level.
*
* @deprecated in Drupal 8.0.0, will be removed before Drupal 9.0.0.
*/
function menu_secondary_local_tasks() {
$links = menu_local_tasks(1);
/** @var \Drupal\Core\Menu\LocalTaskManagerInterface $manager */
$manager = \Drupal::service('plugin.manager.menu.local_task');
$links = $manager->getLocalTasks(\Drupal::routeMatch()->getRouteName(), 1);
// Do not display single tabs.
return count(Element::getVisibleChildren($links['tabs'])) > 1 ? $links['tabs'] : '';
}
/**
* Returns the rendered local actions at the current level.
*/
function menu_get_local_actions() {
$links = menu_local_tasks();
$route_name = Drupal::routeMatch()->getRouteName();
$manager = \Drupal::service('plugin.manager.menu.local_action');
return $manager->getActionsForRoute($route_name) + $links['actions'];
}
/**
* Returns the router path, or the path for a default local task's parent.
*/
function menu_tab_root_path() {
$links = menu_local_tasks();
return $links['root_path'];
}
/**
* Returns a renderable element for the primary and secondary tabs.
*/
......
......@@ -1322,14 +1322,6 @@ function template_preprocess_page(&$variables) {
$variables['is_front'] = FALSE;
$variables['db_is_active'] = FALSE;
}
if (!defined('MAINTENANCE_MODE')) {
$variables['action_links'] = menu_get_local_actions();
$variables['tabs'] = menu_local_tabs();
}
else {
$variables['action_links'] = array();
$variables['tabs'] = array();
}
if ($node = \Drupal::routeMatch()->getParameter('node')) {
$variables['node'] = $node;
......
......@@ -191,10 +191,11 @@ public function getActionsForRoute($route_appears) {
'url' => Url::fromRoute($route_name, $route_parameters),
'localized_options' => $plugin->getOptions($this->routeMatch),
),
'#access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account),
'#access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE),
'#weight' => $plugin->getWeight(),
);
}
return $links;
}
......
......@@ -81,6 +81,13 @@ class LocalTaskManager extends DefaultPluginManager implements LocalTaskManagerI
*/
protected $instances = array();
/**
* The local task render arrays for the current route.
*
* @var array
*/
protected $taskData;
/**
* The route provider to load routes by name.
*
......@@ -296,40 +303,75 @@ public function getTasksBuild($current_route_name) {
// of SQL queries that would otherwise be triggered by the access manager.
$routes = $route_names ? $this->routeProvider->getRoutesByNames($route_names) : array();
// @todo add cacheability data in https://www.drupal.org/node/2511516 so
// that we are not re-building inaccessible links on every page request.
foreach ($tree as $level => $instances) {
/** @var $instances \Drupal\Core\Menu\LocalTaskInterface[] */
foreach ($instances as $plugin_id => $child) {
$route_name = $child->getRouteName();
$route_parameters = $child->getRouteParameters($this->routeMatch);
// Find out whether the user has access to the task.
$access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account);
if ($access) {
$active = $this->isRouteActive($current_route_name, $route_name, $route_parameters);
// The plugin may have been set active in getLocalTasksForRoute() if
// one of its child tabs is the active tab.
$active = $active || $child->getActive();
// @todo It might make sense to use link render elements instead.
$link = array(
'title' => $this->getTitle($child),
'url' => Url::fromRoute($route_name, $route_parameters),
'localized_options' => $child->getOptions($this->routeMatch),
);
$build[$level][$plugin_id] = array(
'#theme' => 'menu_local_task',
'#link' => $link,
'#active' => $active,
'#weight' => $child->getWeight(),
'#access' => $access,
);
}
$active = $this->isRouteActive($current_route_name, $route_name, $route_parameters);
// The plugin may have been set active in getLocalTasksForRoute() if
// one of its child tabs is the active tab.
$active = $active || $child->getActive();
// @todo It might make sense to use link render elements instead.
$link = [
'title' => $this->getTitle($child),
'url' => Url::fromRoute($route_name, $route_parameters),
'localized_options' => $child->getOptions($this->routeMatch),
];
$build[$level][$plugin_id] = [
'#theme' => 'menu_local_task',
'#link' => $link,
'#active' => $active,
'#weight' => $child->getWeight(),
'#access' => $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account, TRUE),
];
}
}
return $build;
}
/**
* {@inheritdoc}
*/
public function getLocalTasks($route_name, $level = 0) {
if (!isset($this->taskData[$route_name])) {
// Look for route-based tabs.
$this->taskData[$route_name] = [
'tabs' => [],
];
if (!$this->requestStack->getCurrentRequest()->attributes->has('exception')) {
// Safe to build tasks only when no exceptions raised.
$data = [];
$local_tasks = $this->getTasksBuild($route_name);
foreach ($local_tasks as $tab_level => $items) {
$data[$tab_level] = empty($data[$tab_level]) ? $items : array_merge($data[$tab_level], $items);
}
$this->taskData[$route_name]['tabs'] = $data;
// Allow modules to alter local tasks.
$this->moduleHandler->alter('menu_local_tasks', $this->taskData[$route_name], $route_name);
}
}
if (isset($this->taskData[$route_name]['tabs'][$level])) {
return [
'tabs' => $this->taskData[$route_name]['tabs'][$level],
'route_name' => $route_name,
];
}
return [
'tabs' => [],
'route_name' => $route_name,
];
}
/**
* Determines whether the route of a certain local task is currently active.
*
......
......@@ -53,4 +53,22 @@ public function getLocalTasksForRoute($route_name);
*/
public function getTasksBuild($current_route_name);
/**
* Collects the local tasks (tabs) for the current route.
*
* @param string $route_name
* The route for which to make renderable local tasks.
* @param int $level
* The level of tasks you ask for. Primary tasks are 0, secondary are 1.
*
* @return array
* An array containing
* - tabs: Local tasks render array for the requested level.
* - route_name: The route name for the current page used to collect the
* local tasks.
*
* @see hook_menu_local_tasks_alter()
*/
public function getLocalTasks($route_name, $level = 0);
}
<?php
/**
* @file
* Contains \Drupal\Core\Plugin\Block\LocalActionsBlock.
*/
namespace Drupal\Core\Menu\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\LocalActionManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Provides a block to display the local actions.
*
* @Block(
* id = "local_actions_block",
* admin_label = @Translation("Primary admin actions")
* )
*/
class LocalActionsBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The local action manager.
*
* @var \Drupal\Core\Menu\LocalActionManagerInterface
*/
protected $localActionManager;
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Creates a LocalActionsBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Menu\LocalActionManagerInterface $local_action_manager
* A local action manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LocalActionManagerInterface $local_action_manager, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->localActionManager = $local_action_manager;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.menu.local_action'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return ['label_display' => FALSE];
}
/**
* {@inheritdoc}
*/
public function build() {
$route_name = $this->routeMatch->getRouteName();
$local_actions = $this->localActionManager->getActionsForRoute($route_name);
if (empty($local_actions)) {
return [];
}
return $local_actions;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// The "Primary admin actions" block is never cacheable because hooks creating local
// actions don't provide cacheability metadata.
// @todo Remove after https://www.drupal.org/node/2511516 has landed.
$form['cache']['#disabled'] = TRUE;
$form['cache']['#description'] = $this->t('This block is never cacheable.');
$form['cache']['max_age']['#value'] = 0;
return $form;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// @todo Remove after https://www.drupal.org/node/2511516 has landed.
return 0;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['route.name'];
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Menu\Plugin\Block\LocalTasksBlock.
*/
namespace Drupal\Core\Menu\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\LocalTaskManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a "Tabs" block to display the local tasks.
*
* @Block(
* id = "local_tasks_block",
* admin_label = @Translation("Tabs"),
* )
*/
class LocalTasksBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The local task manager.
*
* @var \Drupal\Core\Menu\LocalTaskManagerInterface
*/
protected $localTaskManager;
/**
* The route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Creates a LocalTasksBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Menu\LocalTaskManagerInterface $local_task_manager
* The local task manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LocalTaskManagerInterface $local_task_manager, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->localTaskManager = $local_task_manager;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.menu.local_task'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'label_display' => FALSE,
'primary' => TRUE,
'secondary' => TRUE,
];
}
/**
* {@inheritdoc}
*/
public function build() {
$config = $this->configuration;
$tabs = [
'#theme' => 'menu_local_tasks',
];
// Add only selected levels for the printed output.
if ($config['primary']) {
$links = $this->localTaskManager->getLocalTasks($this->routeMatch->getRouteName(), 0);
// Do not display single tabs.
$tabs += [
'#primary' => count(Element::getVisibleChildren($links['tabs'])) > 1 ? $links['tabs'] : [],
];
}
if ($config['secondary']) {
$links = $this->localTaskManager->getLocalTasks($this->routeMatch->getRouteName(), 1);
// Do not display single tabs.
$tabs += [
'#secondary' => count(Element::getVisibleChildren($links['tabs'])) > 1 ? $links['tabs'] : [],
];
}
if (empty($tabs['#primary']) && empty($tabs['#secondary'])) {
return [];
}
return $tabs;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// The "Page actions" block is never cacheable because of hooks creating
// local tasks doesn't provide cacheability metadata.
// @todo Remove after https://www.drupal.org/node/2511516 has landed.
$form['cache']['#disabled'] = TRUE;
$form['cache']['#description'] = $this->t('This block is never cacheable.');
$form['cache']['max_age']['#value'] = 0;
return $form;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// @todo Remove after https://www.drupal.org/node/2511516 has landed.
return 0;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return ['route.name'];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$config = $this->configuration;
$defaults = $this->defaultConfiguration();
$form['levels'] = [
'#type' => 'details',
'#title' => $this->t('Shown tabs'),
'#description' => $this->t('Select tabs being shown in the block'),
// Open if not set to defaults.
'#open' => $defaults['primary'] !== $config['primary'] || $defaults['secondary'] !== $config['secondary'],
];
$form['levels']['primary'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show primary tabs'),
'#default_value' => $config['primary'],
];
$form['levels']['secondary'] = [
'#type' => 'checkbox',
'#title' => $this->t('Show secondary tabs'),
'#default_value' => $config['secondary'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$levels = $form_state->getValue('levels');
$this->configuration['primary'] = $levels['primary'];
$this->configuration['secondary'] = $levels['secondary'];
}
}
......@@ -393,48 +393,35 @@ function hook_menu_links_discovered_alter(&$links) {
}
/**
* Alter tabs and actions displayed on the page before they are rendered.
* Alter local tasks displayed on the page before they are rendered.
*
* This hook is invoked by menu_local_tasks(). The system-determined tabs and
* actions are passed in by reference. Additional tabs or actions may be added.
* actions are passed in by reference. Additional tabs may be added.
*
* Each tab or action is an associative array containing:
* The local tasks are under the 'tabs' element and keyed by plugin ID.
*
* Each local task is an associative array containing:
* - #theme: The theme function to use to render.
* - #link: An associative array containing:
* - title: The localized title of the link.
* - href: The system path to link to.
* - url: a Url object.
* - localized_options: An array of options to pass to _l().
* - #weight: The link's weight compared to other links.
* - #active: Whether the link should be marked as 'active'.
*
* @param array $data
* An associative array containing:
* - actions: A list of of actions keyed by their href, each one being an
* associative array as described above.
* - tabs: A list of (up to 2) tab levels that contain a list of of tabs keyed
* by their href, each one being an associative array as described above.
* An associative array containing list of (up to 2) tab levels that contain a
* list of of tabs keyed by their href, each one being an associative array
* as described above.
* @param string $route_name
* The route name of the page.
*
* @ingroup menu
*/
function hook_menu_local_tasks(&$data, $route_name) {
// Add an action linking to node/add to all pages.
$data['actions']['node/add'] = array(
'#theme' => 'menu_local_action',
'#link' => array(
'title' => t('Add content'),
'url' => Url::fromRoute('node.add_page'),
'localized_options' => array(
'attributes' => array(
'title' => t('Add content'),
),
),
),
);
function hook_menu_local_tasks_alter(&$data, $route_name) {
// Add a tab linking to node/add to all pages.
$data['tabs'][0]['node/add'] = array(
$data['tabs'][0]['node.add_page'] = array(
'#theme' => 'menu_local_task',
'#link' => array(
'title' => t('Example tab'),
......@@ -448,25 +435,6 @@ function hook_menu_local_tasks(&$data, $route_name) {
);
}
/**
* Alter tabs and actions displayed on the page before they are rendered.
*
* This hook is invoked by menu_local_tasks(). The system-determined tabs and
* actions are passed in by reference. Existing tabs or actions may be altered.
*
* @param array $data
* An associative array containing tabs and actions. See
* hook_menu_local_tasks() for details.
* @param string $route_name
* The route name of the page.
*
* @see hook_menu_local_tasks()
*
* @ingroup menu
*/
function hook_menu_local_tasks_alter(&$data, $route_name) {
}
/**
* Alter local actions plugins.
*
......