diff --git a/core/modules/toolbar/css/toolbar.base.css b/core/modules/toolbar/css/toolbar.base.css
index adf9779..5e218a2 100644
--- a/core/modules/toolbar/css/toolbar.base.css
+++ b/core/modules/toolbar/css/toolbar.base.css
@@ -44,7 +44,7 @@ html.js .toolbar {
display: block;
}
.js .toolbar .bar li,
-.js .toolbar .tray li {
+.js .toolbar .horizontal li {
float: left; /* LTR */
}
.js .toolbar a {
@@ -82,13 +82,20 @@ html.js .toolbar {
/**
* Toolbar tray.
*/
-.toolbar .horizontal,
-.toolbar .vertical {
+.js .toolbar .tray {
display: none;
position: absolute;
- width: 100%;
z-index: 250;
}
+.toolbar .horizontal {
+ width: 100%;
+}
+.toolbar .vertical,
+.toolbar .vertical > .lining:before {
+ bottom: 0;
+ width: 240px;
+ width: 15rem;
+}
.toolbar .vertical {
left: -100%; /* LTR */
position: absolute;
@@ -136,6 +143,14 @@ html.js .toolbar {
.toolbar .horizontal .menu li ul {
display: none;
}
+@media only screen {
+ .toolbar .vertical,
+ .toolbar .vertical > .lining:before {
+ bottom: auto;
+ width: 100%;
+ }
+}
+
@media only screen and (min-width: 16.5em) {
.toolbar .vertical {
bottom: 0;
@@ -170,11 +185,11 @@ html.js .toolbar {
* Hide the orientation toggle from browsers that do not interpret
* media queries. They get a standard horizontal toolbar.
*/
-.toolbar .toggle-orientation {
+.toolbar .horizontal .toggle-orientation {
display: none;
}
@media only screen {
- .toolbar .toggle-orientation {
+ .toolbar .tray .toggle-orientation {
display: block;
}
}
diff --git a/core/modules/toolbar/css/toolbar.icons.css b/core/modules/toolbar/css/toolbar.icons.css
index 3b8b49e..943d77a 100644
--- a/core/modules/toolbar/css/toolbar.icons.css
+++ b/core/modules/toolbar/css/toolbar.icons.css
@@ -10,7 +10,6 @@
background-color: transparent;
background-position: center center;
background-repeat: no-repeat;
- background-size: 100% auto;
content: '';
display: block;
height: 100%;
@@ -125,7 +124,6 @@
width: 4em;
}
.toolbar .bar .icon:before {
- background-size: auto auto;
left: 0; /* LTR */
width: 100%;
}
@@ -140,7 +138,6 @@
width: auto;
}
.toolbar .bar .icon:before {
- background-size: 100% auto;
left: 0.6667em; /* LTR */
width: 20px;
}
diff --git a/core/modules/toolbar/css/toolbar.theme.css b/core/modules/toolbar/css/toolbar.theme.css
index a298793..ea54a80 100644
--- a/core/modules/toolbar/css/toolbar.theme.css
+++ b/core/modules/toolbar/css/toolbar.theme.css
@@ -128,10 +128,10 @@
.toolbar .toggle-orientation button {
cursor: pointer;
display: inline-block;
- height: 14px;
+ height: 16px;
padding: 0;
text-indent: -999em;
- width: 18px;
+ width: 20px;
}
.toolbar .toggle-orientation button:before {
left: 0; /* LTR */
diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index ae2954e..3265ea9 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -48,6 +48,15 @@ Drupal.behaviors.toolbar = {
var options = $.extend(this.options, drupalSettings.toolbar);
$toolbar = $(context).find('#toolbar-administration').once('toolbar');
if ($toolbar.length) {
+ // Add subtrees.
+ // @todo Optimize this to delay adding each subtree to the DOM until it is
+ // needed; however, take into account screen readers for determining
+ // when the DOM elements are needed.
+ if (Drupal.toolbar.subtrees) {
+ for (var id in Drupal.toolbar.subtrees) {
+ $('#toolbar-link-' + id).after(Drupal.toolbar.subtrees[id]);
+ }
+ }
// Append a messages element for appending interaction updates for screen
// readers.
$messages = $(Drupal.theme('toolbarMessageBox')).appendTo($toolbar);
@@ -100,6 +109,13 @@ Drupal.behaviors.toolbar = {
};
/**
+ * Set subtrees.
+ */
+Drupal.toolbar.setSubtrees = function(subtrees) {
+ Drupal.toolbar.subtrees = subtrees;
+}
+
+/**
* Toggle a toolbar tab and the associated tray.
*/
Drupal.toolbar.toggleTray = function (event) {
diff --git a/core/modules/toolbar/toolbar.install b/core/modules/toolbar/toolbar.install
index aa8c3d6..2b23e3d 100644
--- a/core/modules/toolbar/toolbar.install
+++ b/core/modules/toolbar/toolbar.install
@@ -6,12 +6,30 @@
*/
/**
+ * Implements hook_schema().
+ */
+function toolbar_schema() {
+ $schema['cache_toolbar'] = drupal_get_schema_unprocessed('system', 'cache');
+ $schema['cache_toolbar']['description'] = 'Cache table for the Toolbar module to store per-user hashes of rendered toolbar subtrees.';
+ return $schema;
+}
+
+/**
* @defgroup updates-7.x-to-8.x Updates from 7.x to 8.x
* @{
* Update functions from 7.x to 8.x.
*/
/**
+ * Creates the {cache_toolbar} cache table.
+ */
+function toolbar_update_8000() {
+ $schema['cache_toolbar'] = drupal_get_schema_unprocessed('system', 'cache');
+ $schema['cache_toolbar']['description'] = 'Cache table for the Toolbar module to store per-user hashes of rendered toolbar subtrees.';
+ db_create_table('cache_toolbar', $schema['cache_toolbar']);
+}
+
+/**
* Enable the Breakpoint and Config modules.
*
* The 7.x version of the Toolbar module had no dependencies. The 8.x version
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 4f8f501..e543326 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -5,6 +5,7 @@
* Administration toolbar for quick access to top level administration items.
*/
+use Symfony\Component\HttpFoundation\JsonResponse;
use Drupal\Core\Template\Attribute;
/**
@@ -12,7 +13,7 @@
*/
function toolbar_help($path, $arg) {
switch ($path) {
- case 'admin/help#toolbar-administration':
+ case 'admin/help#toolbar':
$output = '
' . 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') . '
';
@@ -47,6 +48,82 @@ function toolbar_theme($existing, $type, $theme, $path) {
}
/**
+ * Implements hook_menu().
+ */
+function toolbar_menu() {
+ $items['toolbar/subtrees/%'] = array(
+ 'page callback' => 'toolbar_subtrees_jsonp',
+ 'page arguments' => array(2),
+ 'access callback' => '_toolbar_subtrees_access',
+ 'access arguments' => array(2),
+ 'type' => MENU_CALLBACK,
+ );
+ return $items;
+}
+
+/**
+ * Access callback: Returns if the user has access to the rendered subtree requested by the hash.
+ *
+ * @see toolbar_menu().
+ */
+function _toolbar_subtrees_access($hash) {
+ return user_access('access toolbar') && ($hash == _toolbar_get_subtree_hash());
+}
+
+/**
+ * Page callback: Returns the rendered subtree of each top-level toolbar link.
+ *
+ * @see toolbar_menu().
+ */
+function toolbar_subtrees_jsonp($hash) {
+ _toolbar_initialize_page_cache();
+ $subtrees = toolbar_get_rendered_subtrees();
+ $response = new JsonResponse($subtrees);
+ $response->setCallback('Drupal.toolbar.setSubtrees');
+ return $response;
+}
+
+/**
+ * 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()
+ $cache = drupal_page_get_cache();
+ if (is_object($cache)) {
+ header('X-Drupal-Cache: HIT');
+ // Restore the metadata cached with the page.
+ $_GET['q'] = $cache->data['path'];
+ date_default_timezone_set(drupal_get_user_timezone());
+
+ drupal_serve_page_from_cache($cache);
+
+ // We are done.
+ exit;
+ }
+
+ // Otherwise, create a new page response (that will be cached).
+ 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.
@@ -163,6 +240,13 @@ function toolbar_toolbar() {
'#heading' => t('Administration menu'),
);
+ // To conserve bandwidth, we only include the top-level links in the HTML.
+ // The subtrees are included in a JSONP script, cached by the browser. Here we
+ // add that JSONP script. We add it as an external script, because it's a
+ // Drupal path, not a file available via a stream wrapper.
+ // @see toolbar_subtrees_jsonp()
+ $menu['toolbar_administration']['#attached']['js'][url('toolbar/subtrees/' . _toolbar_get_subtree_hash())] = array('type' => 'external');
+
$items['administration'] = array(
'tab' => array(
'title' => t('Menu'),
@@ -276,13 +360,14 @@ function toolbar_get_menu_tree() {
$tree = array();
$admin_link = db_query('SELECT * FROM {menu_links} WHERE menu_name = :menu_name AND module = :module AND link_path = :path', array(':menu_name' => 'admin', ':module' => 'system', ':path' => 'admin'))->fetchAssoc();
if ($admin_link) {
- $tree = menu_tree_all_data('admin');
- }
- // Return the sub-menus of the admin menu root.
- foreach ($tree as $key => $menu) {
- return (!empty($tree[$key]['below'])) ? $tree[$key]['below'] : array();
+ $tree = menu_build_tree('admin', array(
+ 'expanded' => array($admin_link['mlid']),
+ 'min_depth' => $admin_link['depth'] + 1,
+ 'max_depth' => $admin_link['depth'] + 1,
+ ));
}
- return array();
+
+ return $tree;
}
/**
@@ -313,6 +398,39 @@ function toolbar_menu_navigation_links(&$tree) {
}
/**
+ * 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;
+}
+
+/**
* Checks whether an item is in the active trail.
*
* Useful when using a menu generated by menu_tree_all_data() which does
@@ -384,3 +502,27 @@ function toolbar_library_info() {
return $libraries;
}
+
+/**
+ * Implements hook_cache_flush().
+ */
+function toolbar_cache_flush() {
+ return array('toolbar');
+}
+
+/**
+ * Returns the hash of the per-user rendered toolbar subtrees.
+ */
+function _toolbar_get_subtree_hash() {
+ global $user;
+ $cid = $user->uid . ':' . language(LANGUAGE_TYPE_INTERFACE)->langcode;
+ if ($cache = cache('toolbar')->get($cid)) {
+ $hash = $cache->data;
+ }
+ else {
+ $subtrees = toolbar_get_rendered_subtrees();
+ $hash = drupal_hash_base64(serialize($subtrees));
+ cache('toolbar')->set($cid, $hash);
+ }
+ return $hash;
+}