' . t('About') . ''; $output .= '

' . t('The Toolbar module displays links to top-level administration menu items and links from other modules at the top of the screen. For more information, see the online handbook entry for Toolbar module.', array('@toolbar' => 'http://drupal.org/documentation/modules/toolbar')) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Displaying administrative links') . '
'; $output .= '
' . t('The Toolbar module displays a bar containing top-level administrative components across the top of the screen. Below that, the Toolbar module has a drawer section where it displays links provided by other modules, such as the core Shortcut module. The drawer can be hidden/shown by clicking on its corresponding tab.', array('@shortcuts-help' => url('admin/help/shortcut'))) . '
'; $output .= '
'; return $output; } } /** * Implements hook_permission(). */ function toolbar_permission() { return array( 'access toolbar' => array( 'title' => t('Use the administration toolbar'), ), ); } /** * Implements hook_theme(). */ function toolbar_theme($existing, $type, $theme, $path) { $items['toolbar'] = array( 'render element' => 'element', 'template' => 'toolbar', ); $items['toolbar_item'] = array( 'render element' => 'element', ); return $items; } /** * Implements hook_element_info(). */ function toolbar_element_info() { $elements = array(); $elements['toolbar'] = array( '#pre_render' => array('toolbar_pre_render'), '#theme' => 'toolbar', '#attached' => array( 'library' => array( 'toolbar/toolbar', ), ), // Metadata for the toolbar wrapping element. '#attributes' => array( // The id cannot be simply "toolbar" or it will clash with the simpletest // tests listing which produces a checkbox with attribute id="toolbar" 'id' => 'toolbar-administration', 'class' => array('toolbar'), 'role' => 'group', 'aria-label' => t('Site administration toolbar'), ), // Metadata for the administration bar. '#bar' => array( '#heading' => t('Toolbar items'), '#attributes' => array( 'id' => 'toolbar-bar', 'class' => array('toolbar-bar', 'clearfix',), 'role' => 'navigation', 'aria-label' => t('Toolbar items'), ), ), ); // A toolbar item is wrapped in markup for common styling. The 'tray' // property contains a renderable array. $elements['toolbar_item'] = array( '#pre_render' => array('toolbar_pre_render_item'), '#theme' => 'toolbar_item', 'tab' => array( '#type' => 'link', '#title' => NULL, '#href' => '', ), ); return $elements; } /** * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users. * * This gets invoked after full bootstrap, so must duplicate some of what's * done by \Drupal\Core\DrupalKernel::handlePageCache(). * * @todo Replace this hack with something better integrated with DrupalKernel * once Drupal's page caching itself is properly integrated. */ function _toolbar_initialize_page_cache() { $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE; drupal_page_is_cacheable(TRUE); // If we have a cache, serve it. // @see \Drupal\Core\DrupalKernel::handlePageCache() $request = \Drupal::request(); $response = drupal_page_get_cache($request); if ($response) { $response->headers->set('X-Drupal-Cache', 'HIT'); drupal_serve_page_from_cache($response, $request); $response->prepare($request); $response->send(); // We are done. exit; } // The Expires HTTP header is the heart of the client-side HTTP caching. The // additional server-side page cache only takes effect when the client // accesses the callback URL again (e.g., after clearing the browser cache or // when force-reloading a Drupal page). $max_age = 3600 * 24 * 365; drupal_add_http_header('Expires', gmdate(DateTimePlus::RFC7231, REQUEST_TIME + $max_age)); drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age); } /** * Implements hook_page_build(). * * Add admin toolbar to the page_top region automatically. */ function toolbar_page_build(&$page) { $page['page_top']['toolbar'] = array( '#type' => 'toolbar', '#access' => \Drupal::currentUser()->hasPermission('access toolbar'), ); } /** * Builds the Toolbar as a structured array ready for drupal_render(). * * Since building the toolbar takes some time, it is done just prior to * rendering to ensure that it is built only if it will be displayed. * * @param array $element * A renderable array. * * @return * A renderable array. * * @see toolbar_page_build(). */ function toolbar_pre_render($element) { // Get the configured breakpoints to switch from vertical to horizontal // toolbar presentation. $breakpoints = entity_load('breakpoint_group', 'module.toolbar.toolbar'); if (!empty($breakpoints)) { $media_queries = array(); $media_queries['toolbar']['breakpoints'] = array_map( function ($object) { return $object->mediaQuery; }, $breakpoints->getBreakpoints() ); $element['#attached']['js'][] = array( 'data' => $media_queries, 'type' => 'setting', ); } // Get toolbar items from all modules that implement hook_toolbar(). $items = \Drupal::moduleHandler()->invokeAll('toolbar'); // Allow for altering of hook_toolbar(). \Drupal::moduleHandler()->alter('toolbar', $items); // Sort the children. uasort($items, array('\Drupal\Component\Utility\SortArray', 'sortByWeightProperty')); // Merge in the original toolbar values. $element = array_merge($element, $items); // Render the children. $element['#children'] = drupal_render_children($element); return $element; } /** * Prepares variables for administration toolbar templates. * * Default template: toolbar.html.twig. * * @param array $variables * An associative array containing: * - element: An associative array containing the properties and children of * the tray. Properties used: #children, #attributes and #bar. */ function template_preprocess_toolbar(&$variables) { $element = $variables['element']; // Prepare the toolbar attributes. $variables['attributes'] = $element['#attributes']; $variables['toolbar_attributes'] = new Attribute($element['#bar']['#attributes']); $variables['toolbar_heading'] = $element['#bar']['#heading']; // Prepare the trays and tabs for each toolbar item as well as the remainder // variable that will hold any non-tray, non-tab elements. $variables['trays'] = array(); $variables['tabs'] = array(); $variables['remainder'] = array(); foreach (Element::children($element) as $key) { // Add the tray. if (isset($element[$key]['tray'])) { $variables['trays'][$key] = array( 'links' => $element[$key]['tray'], 'attributes' => new Attribute($element[$key]['tray']['#wrapper_attributes']), ); if (array_key_exists('#heading', $element[$key]['tray'])) { $variables['trays'][$key]['label'] = $element[$key]['tray']['#heading']; } } // Pass the wrapper attributes along. if (array_key_exists('#wrapper_attributes', $element[$key])) { $element[$key]['#wrapper_attributes']['class'][] = 'toolbar-tab'; $attributes = $element[$key]['#wrapper_attributes']; } else { $attributes = array('class' => array('toolbar-tab')); } // Add the tab. $variables['tabs'][$key] = array( 'link' => $element[$key]['tab'], 'attributes' => new Attribute($attributes), ); // Add other non-tray, non-tab child elements to the remainder variable for // later rendering. foreach (Element::children($element[$key]) as $child_key) { if (!in_array($child_key, array('tray', 'tab'))) { $variables['remainder'][$key][$child_key] = $element[$key][$child_key]; } } } } /** * Provides markup for associating a tray trigger with a tray element. * * A tray is a responsive container that wraps renderable content. Trays present * content well on small and large screens alike. * * @param array $element * A renderable array. * * @return * A renderable array. */ function toolbar_pre_render_item($element) { // Assign each item a unique ID. $id = drupal_html_id('toolbar-item'); // Provide attributes for a toolbar item. $attributes = array( 'id' => $id, ); // If tray content is present, markup the tray and its associated trigger. if (!empty($element['tray'])) { // Provide attributes necessary for trays. $attributes += array( 'data-toolbar-tray' => $id . '-tray', 'aria-owns' => $id, 'role' => 'button', 'aria-pressed' => 'false', ); // Merge in module-provided attributes. $element['tab'] += array('#attributes' => array()); $element['tab']['#attributes'] += $attributes; $element['tab']['#attributes']['class'][] = 'trigger'; // Provide attributes for the tray theme wrapper. $attributes = array( 'id' => $id . '-tray', 'data-toolbar-tray' => $id . '-tray', 'aria-owned-by' => $id, ); // Merge in module-provided attributes. if (!isset($element['tray']['#wrapper_attributes'])) { $element['tray']['#wrapper_attributes'] = array(); } $element['tray']['#wrapper_attributes'] += $attributes; $element['tray']['#wrapper_attributes']['class'][] = 'toolbar-tray'; } $element['tab']['#attributes']['class'][] = 'toolbar-item'; return $element; } /** * Implements hook_toolbar(). */ function toolbar_toolbar() { // The 'Home' tab is a simple link, with no corresponding tray. $items['home'] = array( '#type' => 'toolbar_item', 'tab' => array( '#type' => 'link', '#title' => t('Back to site'), '#href' => '', '#attributes' => array( 'title' => t('Return to site content'), 'class' => array('toolbar-icon', 'toolbar-icon-escape-admin'), 'data-toolbar-escape-admin' => TRUE, ), ), '#wrapper_attributes' => array( 'class' => array('hidden'), ), '#attached' => array( 'library' => array( 'toolbar/toolbar.escapeAdmin', ), ), '#weight' => -20, ); // To conserve bandwidth, we only include the top-level links in the HTML. // The subtrees are fetched through a JSONP script that is generated at the // toolbar_subtrees route. We provide the JavaScript requesting that JSONP // script here with the hash parameter that is needed for that route. // @see toolbar_subtrees_jsonp() $langcode = \Drupal::languageManager()->getCurrentLanguage()->id; $subtrees_attached['js'][] = array( 'type' => 'setting', 'data' => array('toolbar' => array( 'subtreesHash' => _toolbar_get_subtrees_hash($langcode), 'langcode' => $langcode, )), ); // The administration element has a link that is themed to correspond to // a toolbar tray. The tray contains the full administrative menu of the site. $items['administration'] = array( '#type' => 'toolbar_item', 'tab' => array( '#type' => 'link', '#title' => t('Manage'), '#href' => 'admin', '#attributes' => array( 'title' => t('Admin menu'), 'class' => array('toolbar-icon', 'toolbar-icon-menu'), // A data attribute that indicates to the client to defer loading of // the admin menu subtrees until this tab is activated. Admin menu // subtrees will not render to the DOM if this attribute is removed. // The value of the attribute is intentionally left blank. Only the // presence of the attribute is necessary. 'data-drupal-subtrees' => '', ), ), 'tray' => array( '#heading' => t('Administration menu'), '#attached' => $subtrees_attached, 'toolbar_administration' => array( '#pre_render' => array( 'toolbar_prerender_toolbar_administration_tray', ), '#type' => 'container', '#attributes' => array( 'class' => array('toolbar-menu-administration'), ), ), ), '#weight' => -15, ); return $items; } /** * Renders the toolbar's administration tray. * * @param array $element * A renderable array. * * @return array * The updated renderable array. * * @see drupal_render() */ function toolbar_prerender_toolbar_administration_tray(array $element) { $menu_tree = \Drupal::menuTree(); // Render the top-level administration menu links. $parameters = new MenuTreeParameters(); $parameters->setRoot('system.admin')->excludeRoot()->setTopLevelOnly()->excludeHiddenLinks(); $tree = $menu_tree->load(NULL, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), array('callable' => 'toolbar_menu_navigation_links'), ); $tree = $menu_tree->transform($tree, $manipulators); $element['administration_menu'] = $menu_tree->build($tree); return $element; } /** * Adds toolbar-specific attributes to the menu link tree. * * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree * The menu link tree to manipulate. * * @return \Drupal\Core\Menu\MenuLinkTreeElement[] * The manipulated menu link tree. */ function toolbar_menu_navigation_links(array $tree) { foreach ($tree as $element) { if ($element->subtree) { toolbar_menu_navigation_links($element->subtree); } // Make sure we have a path specific ID in place, so we can attach icons // and behaviors to the menu links. $link = $element->link; $url = $link->getUrlObject(); if ($url->isExternal()) { // This is an unusual case, so just get a distinct, safe string. $id = substr(Crypt::hashBase64($url->getPath()), 0, 16); } else { $id = str_replace(array('.', '<', '>'), array('-', '', ''), $url->getRouteName()); } // Get the non-localized title to make the icon class. $definition = $link->getPluginDefinition(); $element->options['attributes']['id'] = 'toolbar-link-' . $id; $element->options['attributes']['class'][] = 'toolbar-icon'; // @todo Change to use the plugin ID as class since titles might change. // https://www.drupal.org/node/2310365 $element->options['attributes']['class'][] = 'toolbar-icon-' . strtolower(str_replace(' ', '-', $definition['title'])); $element->options['attributes']['title'] = String::checkPlain($link->getDescription()); } return $tree; } /** * Returns the rendered subtree of each top-level toolbar link. */ function toolbar_get_rendered_subtrees() { $menu_tree = \Drupal::menuTree(); $parameters = new MenuTreeParameters(); $parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(3)->excludeHiddenLinks(); $tree = $menu_tree->load(NULL, $parameters); $manipulators = array( array('callable' => 'menu.default_tree_manipulators:checkAccess'), array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), array('callable' => 'toolbar_menu_navigation_links'), ); $tree = $menu_tree->transform($tree, $manipulators); $subtrees = array(); foreach ($tree as $element) { /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ $link = $element->link; if ($element->subtree) { $subtree = $menu_tree->build($element->subtree); $output = drupal_render($subtree); } else { $output = ''; } // Many routes have dots as route name, while some special ones like // have <> characters in them. $id = str_replace(array('.', '<', '>'), array('-', '', '' ), $link->getUrlObject()->getRouteName()); $subtrees[$id] = $output; } return $subtrees; } /** * Returns the hash of the per-user rendered toolbar subtrees. * * @param string $langcode * The langcode of the current request. * * @return string * The hash of the admin_menu subtrees. */ function _toolbar_get_subtrees_hash($langcode) { $uid = \Drupal::currentUser()->id(); $cid = _toolbar_get_user_cid($uid, $langcode); if ($cache = \Drupal::cache('toolbar')->get($cid)) { $hash = $cache->data; } else { $subtrees = toolbar_get_rendered_subtrees(); $hash = Crypt::hashBase64(serialize($subtrees)); // Cache using a tag 'user' so that we can invalidate all user-specific // caches later, based on the user's ID regardless of language. // Clear the cache when the 'locale' tag is deleted. This ensures a fresh // subtrees rendering when string translations are made. \Drupal::cache('toolbar')->set($cid, $hash, Cache::PERMANENT, array('user' => array($uid), 'locale' => TRUE, 'menu' => 'admin', 'user_roles' => TRUE)); } return $hash; } /** * Returns a cache ID from the user and language IDs. * * @param int $uid * A user ID. * @param string $langcode * The langcode of the current request. * * @return string * A unique cache ID for the user. */ function _toolbar_get_user_cid($uid, $langcode) { return 'toolbar_' . $uid . ':' . $langcode; }