From 387d5858364dd1182eb26ae1fa3aca90971ebdb6 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Wed, 22 Jan 2014 14:27:17 +0000 Subject: [PATCH] Issue #2136507 by Wim Leers: Use client-side cache tags & caching to eliminate 1 HTTP requests/page for rendering Contextual Links. --- .../custom_block/CustomBlockViewBuilder.php | 1 + .../block/Tests/Views/DisplayBlockTest.php | 2 +- core/modules/contextual/contextual.module | 3 - core/modules/contextual/js/contextual.js | 95 +++++++++++++------ .../Tests/ContextualDynamicContextTest.php | 10 +- core/modules/edit/js/edit.js | 39 +++++--- .../menu/lib/Drupal/menu/Tests/MenuTest.php | 2 +- .../node/lib/Drupal/node/NodeViewBuilder.php | 1 + .../lib/Drupal/taxonomy/TermViewBuilder.php | 1 + .../lib/Drupal/views_ui/Tests/DisplayTest.php | 2 +- 10 files changed, 106 insertions(+), 50 deletions(-) diff --git a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php index 1416777bf966..16e11ee27870 100644 --- a/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php +++ b/core/modules/block/custom_block/lib/Drupal/custom_block/CustomBlockViewBuilder.php @@ -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()), ); } } diff --git a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php index 079095d1d438..b86a1ea16108 100644 --- a/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php +++ b/core/modules/block/lib/Drupal/block/Tests/Views/DisplayBlockTest.php @@ -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>'); } } diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index e6af59d37fd8..3fd2cb900d5f 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -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; diff --git a/core/modules/contextual/js/contextual.js b/core/modules/contextual/js/contextual.js index 0b0561e02355..c6400692fb4f 100644 --- a/core/modules/contextual/js/contextual.js +++ b/core/modules/contextual/js/contextual.js @@ -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); diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php index 7b79bff7bcde..e5a727a8a3e1 100644 --- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php +++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php @@ -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. diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index fe36fd1e123e..127fbbe41cfd 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -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). * diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php index 97de9e14291e..8d213b52fa7f 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php @@ -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>'); } /** diff --git a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php index fcaeaede7e98..2d7e639800e5 100644 --- a/core/modules/node/lib/Drupal/node/NodeViewBuilder.php +++ b/core/modules/node/lib/Drupal/node/NodeViewBuilder.php @@ -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()), ); } diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php index 1d9d6c004e0a..fbf29eac6469 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermViewBuilder.php @@ -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()), ); } diff --git a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php index ae15d5cba41b..f341d6fb1198 100644 --- a/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php +++ b/core/modules/views_ui/lib/Drupal/views_ui/Tests/DisplayTest.php @@ -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>'); } /** -- GitLab