Commit 387d5858 authored by catch's avatar catch

Issue #2136507 by Wim Leers: Use client-side cache tags & caching to eliminate...

Issue #2136507 by Wim Leers: Use client-side cache tags & caching to eliminate 1 HTTP requests/page for rendering Contextual Links.
parent 9e4cdb98
...@@ -25,6 +25,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView ...@@ -25,6 +25,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView
if (!$entity->isNew() && $view_mode == 'full') { if (!$entity->isNew() && $view_mode == 'full') {
$build['#contextual_links']['custom_block'] = array( $build['#contextual_links']['custom_block'] = array(
'route_parameters' => array('custom_block' => $entity->id()), 'route_parameters' => array('custom_block' => $entity->id()),
'metadata' => array('changed' => $entity->getChangedTime()),
); );
} }
} }
......
...@@ -293,7 +293,7 @@ public function testBlockContextualLinks() { ...@@ -293,7 +293,7 @@ public function testBlockContextualLinks() {
$response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page'))); $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
$this->assertResponse(200); $this->assertResponse(200);
$json = drupal_json_decode($response); $json = drupal_json_decode($response);
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>'); $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
} }
} }
...@@ -280,9 +280,6 @@ function contextual_pre_render_links($element) { ...@@ -280,9 +280,6 @@ function contextual_pre_render_links($element) {
'route_name' => isset($item['route_name']) ? $item['route_name'] : '', 'route_name' => isset($item['route_name']) ? $item['route_name'] : '',
'route_parameters' => isset($item['route_parameters']) ? $item['route_parameters'] : array(), 'route_parameters' => isset($item['route_parameters']) ? $item['route_parameters'] : array(),
); );
$item['localized_options'] += array('query' => array());
$item['localized_options']['query'] += drupal_get_destination();
$links[$class] += $item['localized_options'];
} }
$element['#links'] = $links; $element['#links'] = $links;
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* Attaches behaviors for the Contextual module. * Attaches behaviors for the Contextual module.
*/ */
(function ($, Drupal, drupalSettings, Backbone) { (function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
"use strict"; "use strict";
...@@ -17,24 +17,51 @@ var options = $.extend(drupalSettings.contextual, ...@@ -17,24 +17,51 @@ var options = $.extend(drupalSettings.contextual,
} }
); );
// Clear the cached contextual links whenever the current user's set of
// permissions changes.
var cachedPermissionsHash = storage.getItem('Drupal.contextual.permissionsHash');
var permissionsHash = drupalSettings.user.permissionsHash;
if (cachedPermissionsHash !== permissionsHash) {
if (typeof permissionsHash === 'string') {
_.chain(storage).keys().each(function (key) {
if (key.substring(0, 18) === 'Drupal.contextual.') {
storage.removeItem(key);
}
});
}
storage.setItem('Drupal.contextual.permissionsHash', permissionsHash);
}
/** /**
* Initializes a contextual link: updates its DOM, sets up model and views * Initializes a contextual link: updates its DOM, sets up model and views
* *
* @param jQuery $contextual * @param jQuery $contextual
* A contextual links placeholder DOM element, containing the actual * A contextual links placeholder DOM element, containing the actual
* contextual links as rendered by the server. * contextual links as rendered by the server.
* @param string html
* The server-side rendered HTML for this contextual link.
*/ */
function initContextual ($contextual) { function initContextual ($contextual, html) {
var $region = $contextual.closest('.contextual-region'); var $region = $contextual.closest('.contextual-region');
var contextual = Drupal.contextual; var contextual = Drupal.contextual;
$contextual $contextual
// Update the placeholder to contain its rendered contextual links.
.html(html)
// Use the placeholder as a wrapper with a specific class to provide // Use the placeholder as a wrapper with a specific class to provide
// positioning and behavior attachment context. // positioning and behavior attachment context.
.addClass('contextual') .addClass('contextual')
// Ensure a trigger element exists before the actual contextual links. // Ensure a trigger element exists before the actual contextual links.
.prepend(Drupal.theme('contextualTrigger')); .prepend(Drupal.theme('contextualTrigger'));
// Set the destination parameter on each of the contextual links.
var destination = 'destination=' + Drupal.encodePath(drupalSettings.currentPath);
$contextual.find('.contextual-links a').each(function () {
var url = this.getAttribute('href');
var glue = (url.indexOf('?') === -1) ? '?' : '&';
this.setAttribute('href', url + glue + destination);
});
// Create a model and the appropriate views. // Create a model and the appropriate views.
var model = new contextual.StateModel({ var model = new contextual.StateModel({
title: $region.find('h2:first').text().trim() title: $region.find('h2:first').text().trim()
...@@ -128,35 +155,47 @@ Drupal.behaviors.contextual = { ...@@ -128,35 +155,47 @@ Drupal.behaviors.contextual = {
ids.push($(this).attr('data-contextual-id')); ids.push($(this).attr('data-contextual-id'));
}); });
// Update all contextual links placeholders whose HTML is cached.
var uncachedIDs = _.filter(ids, function initIfCached (contextualID) {
var html = storage.getItem('Drupal.contextual.' + contextualID);
if (html !== null) {
initContextual($context.find('[data-contextual-id="' + contextualID + '"]'), html);
return false;
}
return true;
});
// Perform an AJAX request to let the server render the contextual links for // Perform an AJAX request to let the server render the contextual links for
// each of the placeholders. // each of the placeholders.
$.ajax({ if (uncachedIDs.length > 0) {
url: Drupal.url('contextual/render') + '?destination=' + Drupal.encodePath(drupalSettings.currentPath), $.ajax({
type: 'POST', url: Drupal.url('contextual/render'),
data: { 'ids[]' : ids }, type: 'POST',
dataType: 'json', data: { 'ids[]' : uncachedIDs },
success: function (results) { dataType: 'json',
for (var id in results) { success: function (results) {
// If the rendered contextual links are empty, then the current user _.each(results, function (html, contextualID) {
// does not have permission to access the associated links: don't // Store the metadata.
// render anything. storage.setItem('Drupal.contextual.' + contextualID, html);
if (results.hasOwnProperty(id) && results[id].length > 0) { // If the rendered contextual links are empty, then the current user
// Update the placeholders to contain its rendered contextual links. // does not have permission to access the associated links: don't
// Usually there will only be one placeholder, but it's possible for // render anything.
// multiple identical placeholders exist on the page (probably if (html.length > 0) {
// because the same content appears more than once). // Update the placeholders to contain its rendered contextual links.
var $placeholders = $context // Usually there will only be one placeholder, but it's possible for
.find('[data-contextual-id="' + id + '"]') // multiple identical placeholders exist on the page (probably
.html(results[id]); // because the same content appears more than once).
var $placeholders = $context.find('[data-contextual-id="' + contextualID + '"]');
// Initialize the contextual links.
for (var i = 0; i < $placeholders.length; i++) { // Initialize the contextual links.
initContextual($placeholders.eq(i)); for (var i = 0; i < $placeholders.length; i++) {
initContextual($placeholders.eq(i), html);
}
} }
} });
} }
} });
}); }
} }
}; };
...@@ -183,4 +222,4 @@ Drupal.theme.contextualTrigger = function () { ...@@ -183,4 +222,4 @@ Drupal.theme.contextualTrigger = function () {
return '<button class="trigger visually-hidden focusable" type="button"></button>'; return '<button class="trigger visually-hidden focusable" type="button"></button>';
}; };
})(jQuery, Drupal, drupalSettings, Backbone); })(jQuery, Drupal, drupalSettings, _, Backbone, window.JSON, window.sessionStorage);
...@@ -61,9 +61,9 @@ function testDifferentPermissions() { ...@@ -61,9 +61,9 @@ function testDifferentPermissions() {
// Now, on the front page, all article nodes should have contextual links // Now, on the front page, all article nodes should have contextual links
// placeholders, as should the view that contains them. // placeholders, as should the view that contains them.
$ids = array( $ids = array(
'node:node=' . $node1->id() . ':', 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime(),
'node:node=' . $node2->id() . ':', 'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime(),
'node:node=' . $node3->id() . ':', 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime(),
'views_ui_edit:view=frontpage:location=page&name=frontpage&display_id=page_1', 'views_ui_edit:view=frontpage:location=page&name=frontpage&display_id=page_1',
); );
...@@ -78,9 +78,9 @@ function testDifferentPermissions() { ...@@ -78,9 +78,9 @@ function testDifferentPermissions() {
$response = $this->renderContextualLinks($ids, 'node'); $response = $this->renderContextualLinks($ids, 'node');
$this->assertResponse(200); $this->assertResponse(200);
$json = drupal_json_decode($response); $json = drupal_json_decode($response);
$this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/1/edit?destination=node">Edit</a></li></ul>'); $this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/1/edit">Edit</a></li></ul>');
$this->assertIdentical($json[$ids[1]], ''); $this->assertIdentical($json[$ids[1]], '');
$this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/3/edit?destination=node">Edit</a></li></ul>'); $this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="nodepage-edit"><a href="' . base_path() . 'node/3/edit">Edit</a></li></ul>');
$this->assertIdentical($json[$ids[3]], ''); $this->assertIdentical($json[$ids[3]], '');
// Authenticated user: can access contextual links, cannot edit articles. // Authenticated user: can access contextual links, cannot edit articles.
......
...@@ -77,17 +77,7 @@ Drupal.behaviors.edit = { ...@@ -77,17 +77,7 @@ Drupal.behaviors.edit = {
// Process each entity element: identical entities that appear multiple // Process each entity element: identical entities that appear multiple
// times will get a numeric identifier, starting at 0. // times will get a numeric identifier, starting at 0.
$(context).find('[data-edit-entity-id]').once('edit').each(function (index, entityElement) { $(context).find('[data-edit-entity-id]').once('edit').each(function (index, entityElement) {
var entityID = entityElement.getAttribute('data-edit-entity-id'); processEntity(entityElement);
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
entityInstancesTracker[entityID] = 0;
}
else {
entityInstancesTracker[entityID]++;
}
// Set the calculated entity instance ID for this element.
var entityInstanceID = entityInstancesTracker[entityID];
entityElement.setAttribute('data-edit-entity-instance-id', entityInstanceID);
}); });
// Process each field element: queue to be used or to fetch metadata. // Process each field element: queue to be used or to fetch metadata.
...@@ -188,6 +178,12 @@ if (permissionsHashValue !== permissionsHash) { ...@@ -188,6 +178,12 @@ if (permissionsHashValue !== permissionsHash) {
*/ */
$(document).on('drupalContextualLinkAdded', function (event, data) { $(document).on('drupalContextualLinkAdded', function (event, data) {
if (data.$region.is('[data-edit-entity-id]')) { if (data.$region.is('[data-edit-entity-id]')) {
// If the contextual link is cached on the client side, an entity instance
// will not yet have been assigned. So assign one.
if (!data.$region.is('[data-edit-entity-instance-id]')) {
data.$region.once('edit');
processEntity(data.$region.get(0));
}
var contextualLink = { var contextualLink = {
entityID: data.$region.attr('data-edit-entity-id'), entityID: data.$region.attr('data-edit-entity-id'),
entityInstanceID: data.$region.attr('data-edit-entity-instance-id'), entityInstanceID: data.$region.attr('data-edit-entity-instance-id'),
...@@ -234,6 +230,27 @@ function initEdit (bodyElement) { ...@@ -234,6 +230,27 @@ function initEdit (bodyElement) {
}); });
} }
/**
* Assigns the entity an instance ID.
*
* @param DOM entityElement.
* A Drupal Entity API entity's DOM element with a data-edit-entity-id
* attribute.
*/
function processEntity (entityElement) {
var entityID = entityElement.getAttribute('data-edit-entity-id');
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
entityInstancesTracker[entityID] = 0;
}
else {
entityInstancesTracker[entityID]++;
}
// Set the calculated entity instance ID for this element.
var entityInstanceID = entityInstancesTracker[entityID];
entityElement.setAttribute('data-edit-entity-instance-id', entityInstanceID);
}
/** /**
* Fetch the field's metadata; queue or initialize it (if EntityModel exists). * Fetch the field's metadata; queue or initialize it (if EntityModel exists).
* *
......
...@@ -418,7 +418,7 @@ public function testBlockContextualLinks() { ...@@ -418,7 +418,7 @@ public function testBlockContextualLinks() {
$response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page'))); $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
$this->assertResponse(200); $this->assertResponse(200);
$json = drupal_json_decode($response); $json = drupal_json_decode($response);
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '?destination=test-page">Configure block</a></li><li class="menu-edit"><a href="' . base_path() . 'admin/structure/menu/manage/tools?destination=test-page">Edit menu</a></li></ul>'); $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="menu-edit"><a href="' . base_path() . 'admin/structure/menu/manage/tools">Edit menu</a></li></ul>');
} }
/** /**
......
...@@ -141,6 +141,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView ...@@ -141,6 +141,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView
if ($entity->id()) { if ($entity->id()) {
$build['#contextual_links']['node'] = array( $build['#contextual_links']['node'] = array(
'route_parameters' =>array('node' => $entity->id()), 'route_parameters' =>array('node' => $entity->id()),
'metadata' => array('changed' => $entity->getChangedTime()),
); );
} }
......
...@@ -58,6 +58,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView ...@@ -58,6 +58,7 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView
$build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/css/taxonomy.module.css'; $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/css/taxonomy.module.css';
$build['#contextual_links']['taxonomy_term'] = array( $build['#contextual_links']['taxonomy_term'] = array(
'route_parameters' => array('taxonomy_term' => $entity->id()), 'route_parameters' => array('taxonomy_term' => $entity->id()),
'metadata' => array('changed' => $entity->getChangedTime()),
); );
} }
......
...@@ -313,7 +313,7 @@ public function testPageContextualLinks() { ...@@ -313,7 +313,7 @@ public function testPageContextualLinks() {
$response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display'))); $response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display')));
$this->assertResponse(200); $this->assertResponse(200);
$json = drupal_json_decode($response); $json = drupal_json_decode($response);
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1?destination=test-display">Edit view</a></li></ul>'); $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-uiedit"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1">Edit view</a></li></ul>');
} }
/** /**
......
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