core/modules/contextual/contextual.js | 63 ++++++++++++++++---- core/modules/contextual/contextual.module | 46 ++++++++++---- core/modules/contextual/contextual.routing.yml | 6 ++ core/modules/contextual/contextual.toolbar.js | 55 ++++++++++------- .../lib/Drupal/contextual/ContextualController.php | 63 ++++++++++++++++++++ .../Tests/ContextualDynamicContextTest.php | 2 +- core/modules/edit/edit.module | 18 +++--- core/modules/edit/js/edit.js | 10 ++-- core/modules/views/js/views-contextual.js | 9 +-- core/modules/views/views.module | 18 ++---- 10 files changed, 218 insertions(+), 72 deletions(-) diff --git a/core/modules/contextual/contextual.js b/core/modules/contextual/contextual.js index aa80b31..31e30e4 100644 --- a/core/modules/contextual/contextual.js +++ b/core/modules/contextual/contextual.js @@ -3,7 +3,7 @@ * Attaches behaviors for the Contextual module. */ -(function ($, Drupal) { +(function ($, Drupal, document, window) { "use strict"; @@ -11,19 +11,62 @@ var contextuals = []; /** * Attaches outline behavior for regions associated with contextual links. + * + * Events + * Contextual triggers an event that can be used by other scripts. + * - drupalContextualLinkAdded: Triggered when a contextual link is added. */ Drupal.behaviors.contextual = { attach: function (context) { - $('ul.contextual-links', context).once('contextual', function () { - var $this = $(this); - var contextual = new Drupal.contextual($this, $this.closest('.contextual-region')); - contextuals.push(contextual); - $this.data('drupal-contextual', contextual); + $('body').once('contextual', function () { + $(document) + // Bind to edit mode changes. + .on('drupalEditModeChanged.contextual', toggleEditMode) + // Track all contextual links on the page. + .on('drupalContextualLinkAdded.contextual', function(event, data) { + contextuals.push(data.contextual); + }); }); - // Bind to edit mode changes. - $('body').once('contextual', function () { - $(document).on('drupalEditModeChanged.contextual', toggleEditMode); + this._render(context); + }, + + /** + * Find contextual links, render them server-side and move them in the DOM. + */ + _render: function (context) { + var $context = $(context); + + var $contextualLinks = $context + .find('[data-contextual-id]') + .once('contextual-render'); + if ($contextualLinks.length === 0) { + return; + } + + var ids = []; + $contextualLinks.each(function () { + ids.push($(this).attr('data-contextual-id')); + }); + $.ajax({ + url: Drupal.url('contextual/render'), + type: 'POST', + data: { 'ids[]' : ids }, + dataType: 'json', + success: function(results) { + for (var id in results) if (results.hasOwnProperty(id)) { + var $contextual = $context + // Find the location for the current rendered contextual link. + .find('[data-contextual-id="' + id + '"]') + // Move it into the DOM. + .html(results[id]); + // Create a Drupal.contextual object and notify listeners of a new + // contextual link. + $(document).trigger('drupalContextualLinkAdded', { + contextual: new Drupal.contextual($contextual, $contextual.closest('.contextual-region')) + }); + } + } }); } }; @@ -198,4 +241,4 @@ Drupal.theme.contextualTrigger = function () { return ''; }; -})(jQuery, Drupal); +})(jQuery, Drupal, document, window); diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index 76e39ba..ea5e439 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -6,6 +6,18 @@ */ /** + * Implements hook_custom_theme(). + * + * @todo Add an event subscriber to the Ajax system to automatically set the + * base page theme for all Ajax requests, and then remove this one off. + */ +function contextual_custom_theme() { + if (substr(current_path(), 0, 11) === 'contextual/') { + return ajax_base_page_theme(); + } +} + +/** * Implements hook_toolbar(). */ function contextual_toolbar() { @@ -24,14 +36,13 @@ function contextual_toolbar() { 'role' => 'button', 'aria-pressed' => 'false', ), - // @todo remove this once http://drupal.org/node/1908906 lands. - '#options' => array('attributes' => array()), ), '#wrapper_attributes' => array( 'class' => array('element-hidden', 'contextual-toolbar-tab'), ), '#attached' => array( 'library' => array( + array('contextual', 'drupal.contextual-links'), array('contextual', 'drupal.contextual-toolbar'), ), ), @@ -91,6 +102,7 @@ function contextual_library_info() { 'dependencies' => array( array('system', 'jquery'), array('system', 'drupal'), + array('system', 'drupalSettings'), array('system', 'jquery.once'), ), ); @@ -139,11 +151,6 @@ function contextual_element_info() { * @see contextual_pre_render_links() */ function contextual_preprocess(&$variables, $hook) { - // Nothing to do here if the user is not permitted to access contextual links. - if (!user_access('access contextual links')) { - return; - } - $hooks = theme_get_registry(FALSE); // Determine the primary theme function argument. @@ -159,14 +166,27 @@ function contextual_preprocess(&$variables, $hook) { } if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) { - // Initialize the template variable as a renderable array. - $variables['title_suffix']['contextual_links'] = array( - '#type' => 'contextual_links', - '#contextual_links' => $element['#contextual_links'], - '#element' => $element, - ); // Mark this element as potentially having contextual links attached to it. $variables['attributes']['class'][] = 'contextual-region'; + + // Render an empty (and thus invisible) div as a title suffix, with a data- + // attribute that contains an identifier, which allows contextual.module's + // JavaScript to determine which contextual links should be rendered. + // This div with data- attribute is added unconditionally, and thus does not + // break the render cache. + // Examples of the data- attribute syntax: + // - node[node]:1 + // - views_ui[admin/structure/views/view]:frontpage + // - menu[admin/structure/menu/manage]:tools|block[admin/structure/block/manage]:bartik.tools + $id = ''; + foreach ($element['#contextual_links'] as $module => $args) { + if (drupal_strlen($id) > 0) { + $id .= '|'; + } + $id .= $module . '[' . $args[0] . ']:' . implode(':', $args[1]); + } + $variables['title_suffix']['contextual_links']['#id'] = $id; + $variables['title_suffix']['contextual_links']['#markup'] = '
'; } } diff --git a/core/modules/contextual/contextual.routing.yml b/core/modules/contextual/contextual.routing.yml new file mode 100644 index 0000000..5dd4457 --- /dev/null +++ b/core/modules/contextual/contextual.routing.yml @@ -0,0 +1,6 @@ +contextual_render: + pattern: '/contextual/render' + defaults: + _controller: '\Drupal\contextual\ContextualController::render' + requirements: + _permission: 'access contextual links' diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js index 45f9757..1ce6a46 100644 --- a/core/modules/contextual/contextual.toolbar.js +++ b/core/modules/contextual/contextual.toolbar.js @@ -17,34 +17,45 @@ Drupal.behaviors.contextualToolbar = { attach: function (context) { $('body').once('contextualToolbar-init', function () { - var $contextuals = $(context).find('.contextual-links'); - var $tab = $('.js .toolbar .bar .contextual-toolbar-tab'); var model = new Drupal.contextualToolbar.models.EditToggleModel({ isViewing: true }); var view = new Drupal.contextualToolbar.views.EditToggleView({ - el: $tab, + el: $('.js .toolbar .bar .contextual-toolbar-tab'), model: model }); - // Update the model based on overlay events. - $(document) - .on('drupalOverlayOpen.contextualToolbar', function () { - model.set('isVisible', false); - }) - .on('drupalOverlayClose.contextualToolbar', function () { - model.set('isVisible', true); - }); + // Update the model based on events to the page. + $(document).on({ + 'drupalOverlayOpen.contextualToolbar': function () { + model.set('overlayIsOpen', true); + }, + 'drupalOverlayClose.contextualToolbar': function () { + model.set('overlayIsOpen', false); + }, + 'drupalContextualLinkAdded.contextualToolbar': function() { + model.set('contextualCount', model.get('contextualCount') + 1); + } + }); - // Update the model to show the edit tab if there's >=1 contextual link. - if ($contextuals.length > 0) { - model.set('isVisible', true); - } + // React when the model is changed. + model + .on('change:overlayIsOpen', function(model, value) { + model.set('isVisible', !value); + }) + .on('change:contextualCount', function(model, value) { + model.set('isVisible', (value > 0 && model.get('overlayIsOpen') === false)); - // Allow other scripts to respond to edit mode changes. - model.on('change:isViewing', function (model, value) { - $(document).trigger('drupalEditModeChanged', { status: !value }); - }); + // If Edit mode is enabled, also apply it to this new contextual link. + if (!model.get('isViewing')) { + $(document).trigger('drupalEditModeChanged', { status: true }); + } + }) + // Allow other scripts to respond to edit mode changes. + .on('change:isViewing', function (model, value) { + $(document).trigger('drupalEditModeChanged', { status: !value }); + }) +; // Checks whether localStorage indicates we should start in edit mode // rather than view mode. @@ -66,7 +77,11 @@ Drupal.contextualToolbar.models.EditToggleModel = Backbone.Model.extend({ // Indicates whether the toggle is currently in "view" or "edit" mode. isViewing: true, // Indicates whether the toggle should be visible or hidden. - isVisible: false + isVisible: false, + // Indicates whether the overlay is open or not. + overlayIsOpen: false, + // Indicates how many contextual links exist on the page. + contextualCount: 0 } }); diff --git a/core/modules/contextual/lib/Drupal/contextual/ContextualController.php b/core/modules/contextual/lib/Drupal/contextual/ContextualController.php new file mode 100644 index 0000000..f776844 --- /dev/null +++ b/core/modules/contextual/lib/Drupal/contextual/ContextualController.php @@ -0,0 +1,63 @@ +request->get('ids'); + if (!isset($ids)) { + throw new BadRequestHttpException(); + } + + $rendered = array(); + foreach ($ids as $id) { + $element = array( + '#type' => 'contextual_links', + '#contextual_links' => array(), + ); + + // Figure out which contextual links should be rendered. + $contexts = explode('|', $id); + foreach ($contexts as $context) { + $args = explode(':', $context); + $provider = array_shift($args); + $pos = strpos($provider, '['); + $module = drupal_substr($provider, 0, $pos); + $parent_path = drupal_substr($provider, $pos + 1, drupal_strlen($provider) - $pos - 2); + $element['#contextual_links'][$module] = array($parent_path, $args); + } + + // Render the contextual links. + $rendered[$id] = drupal_render($element); + } + + return new JsonResponse($rendered); + } + +} diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php index f84e9f4..c657684 100644 --- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php +++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualDynamicContextTest.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\contextual\Tests\ContextualDynamicContextTest. + * Contains \Drupal\contextual\Tests\ContextualDynamicContextTest. */ namespace Drupal\contextual\Tests; diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index a6ee046..f1e9227 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -39,7 +39,7 @@ function edit_permission() { } /** - * Implements hook_contextual_links_view_alter(). + * Implements hook_toolbar_alter(). * * In-place editing builds upon contextual.module, but doesn't actually add its * "Quick edit" contextual link in PHP (i.e. here) because: @@ -48,14 +48,16 @@ function edit_permission() { * - it should only work when JavaScript is enabled, because only then in-place * editing is possible. */ -function edit_contextual_links_view_alter(&$element, $items) { - if (!user_access('access in-place editing')) { - return; - } +function edit_toolbar_alter(&$items) { + if (isset($items['contextual'])) { + if (!user_access('access in-place editing')) { + return; + } - // Include the attachments and settings for all available editors. - $attachments = drupal_container()->get('edit.editor.selector')->getAllEditorAttachments(); - $element['#attached'] = NestedArray::mergeDeep($element['#attached'], $attachments); + // Include the attachments and settings for all available editors. + $attachments = drupal_container()->get('edit.editor.selector')->getAllEditorAttachments(); + $items['contextual']['#attached'] = NestedArray::mergeDeep($items['contextual']['#attached'], $attachments); + } } /** diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index f924e7b..183e70b 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -92,11 +92,13 @@ Drupal.edit.init = function() { // Add "Quick edit" links to all contextual menus where editing the full // node is possible. // @todo Generalize this to work for all entities. - $('ul.contextual-links li.node-edit') - .before('
  • ') - .each(function() { + $(document).on('drupalContextualLinkAdded.edit', function(event, data) { + var $editContextualLink = data.contextual.$links + .find('li.node-edit') + .before('
  • ') + .prev(); + // Instantiate ContextualLinkView. - var $editContextualLink = $(this).prev(); var editContextualLinkView = new Drupal.edit.views.ContextualLinkView({ el: $editContextualLink.get(0), model: appModel, diff --git a/core/modules/views/js/views-contextual.js b/core/modules/views/js/views-contextual.js index 3c3ae96..d10768e 100644 --- a/core/modules/views/js/views-contextual.js +++ b/core/modules/views/js/views-contextual.js @@ -8,10 +8,11 @@ Drupal.behaviors.viewsContextualLinks = { attach: function (context) { - // If there are views-related contextual links attached to the main page - // content, find the smallest region that encloses both the links and the - // view, and display it as a contextual links region. - $('.views-contextual-links-page', context).closest(':has(.view)').addClass('contextual-region'); + var id = $('body[data-views-page-contextual-id]') + .attr('data-views-page-contextual-id'); + $('[data-contextual-id="' + id + '"]') + .closest(':has(.view)') + .addClass('contextual-region'); } }; diff --git a/core/modules/views/views.module b/core/modules/views/views.module index f1cb911..698f32e 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -467,6 +467,11 @@ function views_page_alter(&$page) { * Implements MODULE_preprocess_HOOK(). */ function views_preprocess_html(&$variables) { + // Early-return to prevent adding unnecessary JavaScript. + if (!user_access('access contextual links')) { + return; + } + // If the page contains a view as its main content, contextual links may have // been attached to the page as a whole; for example, by views_page_alter(). // This allows them to be associated with the page and rendered by default @@ -483,6 +488,7 @@ function views_preprocess_html(&$variables) { $key = array_search('contextual-region', $variables['attributes']['class']->value()); if ($key !== FALSE) { unset($variables['attributes']['class'][$key]); + $variables['attributes']['data-views-page-contextual-id'] = $variables['title_suffix']['contextual_links']['#id']; // Add the JavaScript, with a group and weight such that it will run // before modules/contextual/contextual.js. drupal_add_library('views', 'views.contextual-links'); @@ -491,18 +497,6 @@ function views_preprocess_html(&$variables) { } /** - * Implements hook_contextual_links_view_alter(). - */ -function views_contextual_links_view_alter(&$element, $items) { - // If we are rendering views-related contextual links attached to the overall - // page array, add a class to the list of contextual links. This will be used - // by the JavaScript added in views_preprocess_html(). - if (!empty($element['#element']['#views_contextual_links_info']) && !empty($element['#element']['#type']) && $element['#element']['#type'] == 'page') { - $element['#attributes']['class'][] = 'views-contextual-links-page'; - } -} - -/** * Adds contextual links associated with a view display to a renderable array. * * This function should be called when a view is being rendered in a particular