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
if (!$entity->isNew() && $view_mode == 'full') {
$build['#contextual_links']['custom_block'] = array(
'route_parameters' => array('custom_block' => $entity->id()),
'metadata' => array('changed' => $entity->getChangedTime()),
);
}
}
......
......@@ -293,7 +293,7 @@ public function testBlockContextualLinks() {
$response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
$this->assertResponse(200);
$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) {
'route_name' => isset($item['route_name']) ? $item['route_name'] : '',
'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;
......
......@@ -3,7 +3,7 @@
* Attaches behaviors for the Contextual module.
*/
(function ($, Drupal, drupalSettings, Backbone) {
(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
"use strict";
......@@ -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
*
* @param jQuery $contextual
* A contextual links placeholder DOM element, containing the actual
* 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 contextual = Drupal.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
// positioning and behavior attachment context.
.addClass('contextual')
// Ensure a trigger element exists before the actual contextual links.
.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.
var model = new contextual.StateModel({
title: $region.find('h2:first').text().trim()
......@@ -128,35 +155,47 @@ Drupal.behaviors.contextual = {
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
// each of the placeholders.
$.ajax({
url: Drupal.url('contextual/render') + '?destination=' + Drupal.encodePath(drupalSettings.currentPath),
type: 'POST',
data: { 'ids[]' : ids },
dataType: 'json',
success: function (results) {
for (var id in results) {
// If the rendered contextual links are empty, then the current user
// does not have permission to access the associated links: don't
// render anything.
if (results.hasOwnProperty(id) && results[id].length > 0) {
// Update the placeholders to contain its rendered contextual links.
// Usually there will only be one placeholder, but it's possible for
// multiple identical placeholders exist on the page (probably
// because the same content appears more than once).
var $placeholders = $context
.find('[data-contextual-id="' + id + '"]')
.html(results[id]);
// Initialize the contextual links.
for (var i = 0; i < $placeholders.length; i++) {
initContextual($placeholders.eq(i));
if (uncachedIDs.length > 0) {
$.ajax({
url: Drupal.url('contextual/render'),
type: 'POST',
data: { 'ids[]' : uncachedIDs },
dataType: 'json',
success: function (results) {
_.each(results, function (html, contextualID) {
// Store the metadata.
storage.setItem('Drupal.contextual.' + contextualID, html);
// If the rendered contextual links are empty, then the current user
// does not have permission to access the associated links: don't
// render anything.
if (html.length > 0) {
// Update the placeholders to contain its rendered contextual links.
// Usually there will only be one placeholder, but it's possible for
// multiple identical placeholders exist on the page (probably
// 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++) {
initContextual($placeholders.eq(i), html);
}
}
}
});
}
}
});
});
}
}
};
......@@ -183,4 +222,4 @@ Drupal.theme.contextualTrigger = function () {
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() {
// Now, on the front page, all article nodes should have contextual links
// placeholders, as should the view that contains them.
$ids = array(
'node:node=' . $node1->id() . ':',
'node:node=' . $node2->id() . ':',
'node:node=' . $node3->id() . ':',
'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime(),
'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime(),
'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime(),
'views_ui_edit:view=frontpage:location=page&name=frontpage&display_id=page_1',
);
......@@ -78,9 +78,9 @@ function testDifferentPermissions() {
$response = $this->renderContextualLinks($ids, 'node');
$this->assertResponse(200);
$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[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]], '');
// Authenticated user: can access contextual links, cannot edit articles.
......
......@@ -77,17 +77,7 @@ Drupal.behaviors.edit = {
// Process each entity element: identical entities that appear multiple
// times will get a numeric identifier, starting at 0.
$(context).find('[data-edit-entity-id]').once('edit').each(function (index, 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);
processEntity(entityElement);
});
// Process each field element: queue to be used or to fetch metadata.
......@@ -188,6 +178,12 @@ if (permissionsHashValue !== permissionsHash) {
*/
$(document).on('drupalContextualLinkAdded', function (event, data) {
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 = {
entityID: data.$region.attr('data-edit-entity-id'),
entityInstanceID: data.$region.attr('data-edit-entity-instance-id'),
......@@ -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).
*
......
......@@ -418,7 +418,7 @@ public function testBlockContextualLinks() {
$response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-page')));
$this->assertResponse(200);
$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
if ($entity->id()) {
$build['#contextual_links']['node'] = array(
'route_parameters' =>array('node' => $entity->id()),
'metadata' => array('changed' => $entity->getChangedTime()),
);
}
......
......@@ -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['#contextual_links']['taxonomy_term'] = array(
'route_parameters' => array('taxonomy_term' => $entity->id()),
'metadata' => array('changed' => $entity->getChangedTime()),
);
}
......
......@@ -313,7 +313,7 @@ public function testPageContextualLinks() {
$response = $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => 'test-display')));
$this->assertResponse(200);
$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