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; +}