' . 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( 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' => 'navigation', ), // Metadata for the administration bar. '#bar' => array( '#heading' => t('Toolbar items'), '#attributes' => array( 'id' => 'toolbar-bar', 'class' => array('toolbar-bar', 'clearfix'), ), ), ); // 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_bootstrap_page_cache(). * * @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_bootstrap_page_cache() $request = \Drupal::request(); $cache = drupal_page_get_cache($request); if (is_object($cache)) { $response = new Response(); $response->headers->set('X-Drupal-Cache', 'HIT'); date_default_timezone_set(drupal_get_user_timezone()); drupal_serve_page_from_cache($cache, $response, $request); $response->prepare($request); $response->send(); // We are done. exit; } // Otherwise, create a new page response (that will be cached). drupal_add_http_header('X-Drupal-Cache', 'MISS'); // 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(DATE_RFC1123, 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' => user_access('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_alter('toolbar', $items); // Sort the children. uasort($items, 'element_sort'); // 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'; } return $element; } /** * Implements hook_toolbar(). */ function toolbar_toolbar() { $items = array(); // 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' => '', '#options' => array( '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( array('toolbar', 'toolbar.escapeAdmin'), ), ), '#weight' => -20, ); // Retrieve the administration menu from the database. $tree = toolbar_get_menu_tree(); // Add attributes to the links before rendering. toolbar_menu_navigation_links($tree); $menu = array( '#heading' => t('Administration menu'), 'toolbar_administration' => array( '#type' => 'container', '#attributes' => array( 'class' => array('toolbar-menu-administration'), ), 'administration_menu' => menu_tree_output($tree), ), ); // 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() $menu['toolbar_administration']['#attached']['js'][] = array( 'type' => 'setting', 'data' => array('toolbar' => array( 'subtreesHash' => _toolbar_get_subtrees_hash(), )), ); // 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', '#options' => array( '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' => $menu, '#weight' => -15, ); return $items; } /** * Gets only the top level items below the 'admin' path. * * @return * An array containing a menu tree of top level items below the 'admin' path. */ function toolbar_get_menu_tree() { $tree = array(); $query = \Drupal::entityQuery('menu_link') ->condition('menu_name', 'admin') ->condition('module', 'system') ->condition('link_path', 'admin'); $result = $query->execute(); if (!empty($result)) { $admin_link = menu_link_load(reset($result)); $tree = menu_build_tree('admin', array( 'expanded' => array($admin_link['mlid']), 'min_depth' => $admin_link['depth'] + 1, 'max_depth' => $admin_link['depth'] + 1, )); } return $tree; } /** * Generates an array of links from a menu tree array. * * Based on menu_navigation_links(). Adds path based IDs and icon placeholders * to the links. * * @return * An array of links as defined above. */ function toolbar_menu_navigation_links(&$tree) { foreach ($tree as $key => $item) { // Configure sub-items. if (!empty($item['below'])) { toolbar_menu_navigation_links($tree[$key]['below']); } // Make sure we have a path specific ID in place, so we can attach icons // and behaviors to the items. $tree[$key]['link']['localized_options']['attributes'] = array( 'id' => 'toolbar-link-' . str_replace(array('/', '<', '>'), array('-', '', ''), $item['link']['link_path']), 'class' => array( 'toolbar-icon', 'toolbar-icon-' . strtolower(str_replace(' ', '-', $item['link']['link_title'])), ), 'title' => check_plain($item['link']['description']), ); } } /** * Returns the rendered subtree of each top-level toolbar link. */ function toolbar_get_rendered_subtrees() { $subtrees = array(); $tree = toolbar_get_menu_tree(); foreach ($tree as $tree_item) { $item = $tree_item['link']; if (!$item['hidden'] && $item['access']) { if ($item['has_children']) { $query = db_select('menu_links'); $query->addField('menu_links', 'mlid'); $query->condition('has_children', 1); for ($i=1; $i <= $item['depth']; $i++) { $query->condition('p' . $i, $item['p' . $i]); } $parents = $query->execute()->fetchCol(); $subtree = menu_build_tree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1)); toolbar_menu_navigation_links($subtree); $subtree = menu_tree_output($subtree); $subtree = drupal_render($subtree); } else { $subtree = ''; } $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['href']); $subtrees[$id] = $subtree; } } return $subtrees; } /** * Implements hook_library_info(). */ function toolbar_library_info() { $path = drupal_get_path('module', 'toolbar'); $libraries['toolbar'] = array( 'title' => 'Toolbar', 'version' => \Drupal::VERSION, 'js' => array( // Core. $path . '/js/toolbar.js' => array(), // Models. $path . '/js/models/MenuModel.js' => array(), $path . '/js/models/ToolbarModel.js' => array(), // Views. $path . '/js/views/BodyVisualView.js' => array(), $path . '/js/views/MenuVisualView.js' => array(), $path . '/js/views/ToolbarAuralView.js' => array(), $path . '/js/views/ToolbarVisualView.js' => array(), ), 'css' => array( $path . '/css/toolbar.module.css', $path . '/css/toolbar.theme.css', $path . '/css/toolbar.icons.css', ), 'dependencies' => array( array('system', 'modernizr'), array('system', 'jquery'), array('system', 'drupal'), array('system', 'drupalSettings'), array('system', 'drupal.announce'), array('system', 'backbone'), array('system', 'matchmedia'), array('system', 'jquery.once'), array('system', 'drupal.displace'), array('toolbar', 'toolbar.menu'), ), ); $libraries['toolbar.menu'] = array( 'title' => 'Toolbar nested accordion menus.', 'version' => \Drupal::VERSION, 'js' => array( $path . '/js/toolbar.menu.js' => array(), ), 'css' => array( $path . '/css/toolbar.menu.css', ), 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), array('system', 'jquery.once'), ), ); $libraries['toolbar.escapeAdmin'] = array( 'title' => 'Provides a button to escape the administration area.', 'version' => \Drupal::VERSION, 'js' => array( $path . '/js/escapeAdmin.js', ), 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), array('system', 'drupalSettings'), array('system', 'jquery.once'), ), ); return $libraries; } /** * Returns the hash of the per-user rendered toolbar subtrees. * * @return string * The hash of the admin_menu subtrees. */ function _toolbar_get_subtrees_hash() { $uid = \Drupal::currentUser()->id(); $cid = _toolbar_get_user_cid($uid); if ($cache = 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. cache('toolbar')->set($cid, $hash, CacheBackendInterface::CACHE_PERMANENT, array('user' => array($uid), 'locale' => TRUE,)); } return $hash; } /** * Implements hook_modules_installed(). */ function toolbar_modules_installed($modules) { _toolbar_clear_user_cache(); } /** * Implements hook_modules_uninstalled(). */ function toolbar_modules_uninstalled($modules) { _toolbar_clear_user_cache(); } /** * Implements hook_ENTITY_TYPE_update(). */ function toolbar_menu_link_update(MenuLinkInterface $menu_link) { if ($menu_link->menu_name === 'admin') { _toolbar_clear_user_cache(); } } /** * Implements hook_ENTITY_TYPE_update(). */ function toolbar_user_update(UserInterface $user) { _toolbar_clear_user_cache($user->id()); } /** * Implements hook_ENTITY_TYPE_update(). */ function toolbar_user_role_update(RoleInterface $role) { _toolbar_clear_user_cache(); } /** * Returns a cache ID from the user and language IDs. * * @param int $uid * A user ID. * * @return string * A unique cache ID for the user. */ function _toolbar_get_user_cid($uid) { return 'toolbar_' . $uid . ':' . \Drupal::languageManager()->getLanguage(Language::TYPE_INTERFACE)->id; } /** * Clears the Toolbar user cache. * * @param int $uid * (optional) The user ID whose toolbar cache entry to clear. */ function _toolbar_clear_user_cache($uid = NULL) { $cache = cache('toolbar'); if (!$cache->isEmpty()) { // Clear by the 'user' tag in order to delete all caches, in any language, // associated with this user. if (isset($uid)) { $cache->deleteTags(array('user' => array($uid))); } else { $cache->deleteAll(); } } }