Commit 61fcb6fc authored by catch's avatar catch

Issue #1927174 by jessebeach, Wim Leers, nod_, Gábor Hojtsy: Complete the work...

Issue #1927174 by jessebeach, Wim Leers, nod_, Gábor Hojtsy: Complete the work to make the Toolbar administration menu sub tree rendering more efficient.
parent e560115e
......@@ -67,11 +67,14 @@ Drupal.behaviors.toolbar = {
strings: options.strings
});
// Handle the resolution of Drupal.toolbar.setSubtrees().
// Handle the resolution of Drupal.toolbar.setSubtrees.
// This is handled with a deferred so that the function may be invoked
// asynchronously.
Drupal.toolbar.setSubtrees.done(function (subtrees) {
menuModel.set('subtrees', subtrees);
localStorage.setItem('Drupal.toolbar.subtrees', JSON.stringify(subtrees));
// Indicate on the toolbarModel that subtrees are now loaded.
model.set('areSubtreesLoaded', true);
});
// Attach a listener to the configured media query breakpoints.
......@@ -88,6 +91,15 @@ Drupal.behaviors.toolbar = {
}
}
// Trigger an initial attempt to load menu subitems. This first attempt
// is made after the media query handlers have had an opportunity to
// process. The toolbar starts in the vertical orientation by default,
// unless the viewport is wide enough to accomodate a horizontal
// orientation. Thus we give the Toolbar a chance to determine if it
// should be set to horizontal orientation before attempting to load menu
// subtrees.
Drupal.toolbar.views.toolbarVisualView.loadSubtrees();
$(document)
// Update the model when the viewport offset changes.
.on('drupalViewportOffsetChange.toolbar', function (event, offsets) {
......@@ -196,6 +208,9 @@ Drupal.toolbar = {
// Indicates whether the toolbar is positioned absolute (false) or fixed
// (true).
isFixed: false,
// Menu subtrees are loaded through an AJAX request only when the Toolbar
// is set to a vertical orientation.
areSubtreesLoaded: false,
// If the viewport overflow becomes constrained, such as when the overlay
// is open, isFixed must be true so that elements in the trays aren't
// lost offscreen and impossible to get to.
......@@ -316,10 +331,25 @@ Drupal.toolbar = {
/**
* {@inheritdoc}
*/
render: function (model) {
render: function () {
this.updateTabs();
this.updateTrayOrientation();
this.updateBarAttributes();
// Load the subtrees if the orientation of the toolbar is changed to
// vertical. This condition responds to the case that the toolbar switches
// from horizontal to vertical orientation. The toolbar starts in a
// vertical orientation by default and then switches to horizontal during
// initialization if the media query conditions are met. Simply checking
// that the orientation is vertical here would result in the subtrees
// always being loaded, even when the toolbar initialization ultimately
// results in a horizontal orientation.
//
// @see Drupal.behaviors.toolbar.attach() where admin menu subtrees
// loading is invoked during initialization after media query conditions
// have been processed.
if (this.model.changed.orientation === 'vertical' || this.model.changed.activeTab) {
this.loadSubtrees();
}
// Trigger a recalculation of viewport displacing elements. Use setTimeout
// to ensure this recalculation happens after changes to visual elements
// have processed.
......@@ -485,6 +515,49 @@ Drupal.toolbar = {
// the container for the trays.
$trays.css('padding-top', this.$el.find('.toolbar-bar').outerHeight());
}
},
/**
* Calls the endpoint URI that will return rendered subtrees with JSONP.
*
* The rendered admin menu subtrees HTML is cached on the client in
* localStorage until the cache of the admin menu subtrees on the server-
* side is invalidated. The subtreesHash is stored in localStorage as well
* and compared to the subtreesHash in drupalSettings to determine when the
* admin menu subtrees cache has been invalidated.
*/
loadSubtrees: function () {
var $activeTab = $(this.model.get('activeTab'));
var orientation = this.model.get('orientation');
// Only load and render the admin menu subtrees if:
// (1) They have not been loaded yet.
// (2) The active tab is the administration menu tab, indicated by the
// presence of the data-drupal-subtrees attribute.
// (3) The orientation of the tray is vertical.
if (!this.model.get('areSubtreesLoaded') && $activeTab.data('drupal-subtrees') !== undefined && orientation === 'vertical') {
var subtreesHash = drupalSettings.toolbar.subtreesHash;
var endpoint = Drupal.url('toolbar/subtrees/' + subtreesHash);
var cachedSubtreesHash = localStorage.getItem('Drupal.toolbar.subtreesHash');
var cachedSubtrees = JSON.parse(localStorage.getItem('Drupal.toolbar.subtrees'));
var isVertical = this.model.get('orientation') === 'vertical';
// If we have the subtrees in localStorage and the subtree hash has not
// changed, then use the cached data.
if (isVertical && subtreesHash === cachedSubtreesHash && cachedSubtrees) {
Drupal.toolbar.setSubtrees.resolve(cachedSubtrees);
}
// Only make the call to get the subtrees if the orientation of the
// toolbar is vertical.
else if (isVertical) {
// Remove the cached menu information.
localStorage.removeItem('Drupal.toolbar.subtreesHash');
localStorage.removeItem('Drupal.toolbar.subtrees');
// The response from the server will call the resolve method of the
// Drupal.toolbar.setSubtrees Promise.
$.ajax(endpoint);
// Cache the hash for the subtrees locally.
localStorage.setItem('Drupal.toolbar.subtreesHash', subtreesHash);
}
}
}
}),
......
......@@ -28,7 +28,7 @@ public function appliesTo() {
*/
public function access(Route $route, Request $request) {
$hash = $request->get('hash');
return (user_access('access toolbar') && ($hash == _toolbar_get_subtree_hash())) ? static::ALLOW : static::DENY;
return (user_access('access toolbar') && ($hash == _toolbar_get_subtrees_hash())) ? static::ALLOW : static::DENY;
}
}
......@@ -5,11 +5,14 @@
* Administration toolbar for quick access to top level administration items.
*/
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\Language;
use Symfony\Component\HttpFoundation\JsonResponse;
use Drupal\Core\Template\Attribute;
use Drupal\Component\Utility\Crypt;
use Symfony\Component\HttpFoundation\Response;
use Drupal\menu_link\MenuLinkInterface;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* Implements hook_help().
......@@ -437,11 +440,16 @@ function toolbar_toolbar() {
);
// 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.
// 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'][url('toolbar/subtrees/' . _toolbar_get_subtree_hash())] = array('type' => 'external');
$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.
......@@ -455,6 +463,12 @@ function toolbar_toolbar() {
'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' => '',
),
),
),
......@@ -551,34 +565,6 @@ function toolbar_get_rendered_subtrees() {
return $subtrees;
}
/**
* Checks whether an item is in the active trail.
*
* Useful when using a menu generated by menu_tree_all_data() which does
* not set the 'in_active_trail' flag on items.
*
* @return
* TRUE when path is in the active trail, FALSE if not.
*
* @todo
* Look at migrating to a menu system level function.
*/
function toolbar_in_active_trail($path) {
$active_paths = &drupal_static(__FUNCTION__);
// Gather active paths.
if (!isset($active_paths)) {
$active_paths = array();
$trail = menu_get_active_trail();
foreach ($trail as $item) {
if (!empty($item['href'])) {
$active_paths[] = $item['href'];
}
}
}
return in_array($path, $active_paths);
}
/**
* Implements hook_library_info().
*/
......@@ -599,6 +585,7 @@ function toolbar_library_info() {
array('system', 'jquery'),
array('system', 'drupal'),
array('system', 'drupalSettings'),
array('system', 'drupal.announce'),
array('system', 'backbone'),
array('system', 'matchmedia'),
array('system', 'jquery.once'),
......@@ -628,17 +615,93 @@ function toolbar_library_info() {
/**
* Returns the hash of the per-user rendered toolbar subtrees.
*
* @return string
* The hash of the admin_menu subtrees.
*/
function _toolbar_get_subtree_hash() {
$user = \Drupal::currentUser();
$cid = $user->id() . ':' . language(Language::TYPE_INTERFACE)->id;
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('toolbar')->set($cid, $hash);
// 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_enabled().
*/
function toolbar_modules_enabled($modules) {
_toolbar_clear_user_cache();
}
/**
* Implements hook_modules_disabled().
*/
function toolbar_modules_disabled($modules) {
_toolbar_clear_user_cache();
}
/**
* Implements hook_ENTITY_TYPE_update().
*/
function toolbar_menu_link_update(MenuLinkInterface $menu_link) {
if ($menu_link->get('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 $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();
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment