Issue #914382 by Wim Leers, Cottser, Gábor Hojtsy, franz, droplet, sun, Niklas Fiekas, catch, dawehner, effulgentsia: Fixed Contextual links incompatible with render cache.
parent
d02b879b48
commit
eea18a4869
|
@ -152,10 +152,29 @@ class DisplayBlockTest extends ViewTestBase {
|
|||
* Tests the contextual links on a Views block.
|
||||
*/
|
||||
public function testBlockContextualLinks() {
|
||||
$this->drupalLogin($this->drupalCreateUser(array('administer views', 'access contextual links')));
|
||||
$this->drupalPlaceBlock('views_block:test_view_block-block_1', array(), array('title' => 'test_view_block-block_1:1'));
|
||||
$this->drupalLogin($this->drupalCreateUser(array('administer views', 'access contextual links', 'administer blocks')));
|
||||
$block = $this->drupalPlaceBlock('views_block:test_view_block-block_1');
|
||||
$this->drupalGet('test-page');
|
||||
$this->assertLinkByHref("admin/structure/views/view/test_view_block/edit");
|
||||
|
||||
$id = 'block:admin/structure/block/manage:' . $block->id() . ':|views_ui:admin/structure/views/view:test_view_block:location=block&name=test_view_block&display_id=block_1';
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
|
||||
$this->assertRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
|
||||
|
||||
// Get server-rendered contextual links.
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
|
||||
$post = urlencode('ids[0]') . '=' . urlencode($id);
|
||||
$response = $this->curlExec(array(
|
||||
CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => 'test-page'))),
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $post,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
),
|
||||
));
|
||||
$this->assertResponse(200);
|
||||
$json = drupal_json_decode($response);
|
||||
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '/configure?destination=test-page">Configure block</a></li><li class="views-ui-edit even last"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1?destination=test-page">Edit view</a></li></ul>');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,31 +17,26 @@ var options = $.extend({
|
|||
/**
|
||||
* Initializes a contextual link: updates its DOM, sets up model and views
|
||||
*
|
||||
* @param DOM links
|
||||
* A contextual links DOM element as rendered by the server.
|
||||
* @param jQuery $contextual
|
||||
* A contextual links placeholder DOM element, containing the actual
|
||||
* contextual links as rendered by the server.
|
||||
*/
|
||||
function initContextual (index, links) {
|
||||
var $links = $(links);
|
||||
var $region = $links.closest('.contextual-region');
|
||||
function initContextual ($contextual) {
|
||||
var $region = $contextual.closest('.contextual-region');
|
||||
var contextual = Drupal.contextual;
|
||||
|
||||
// Create a contextual links wrapper to provide positioning and behavior
|
||||
// attachment context.
|
||||
var $wrapper = $(Drupal.theme('contextualWrapper'))
|
||||
.insertBefore($links)
|
||||
// In the wrapper, first add the trigger element.
|
||||
.append(Drupal.theme('contextualTrigger'))
|
||||
// In the wrapper, then add the contextual links.
|
||||
.append($links);
|
||||
$contextual
|
||||
// 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'))
|
||||
|
||||
// Create a model, add it to the collection.
|
||||
// Create a model and the appropriate views.
|
||||
var model = new contextual.Model({
|
||||
title: $region.find('h2:first').text().trim()
|
||||
});
|
||||
contextual.collection.add(model);
|
||||
|
||||
// Create the appropriate views for this model.
|
||||
var viewOptions = $.extend({ el: $wrapper, model: model }, options);
|
||||
var viewOptions = $.extend({ el: $contextual, model: model }, options);
|
||||
contextual.views.push({
|
||||
visual: new contextual.VisualView(viewOptions),
|
||||
aural: new contextual.AuralView(viewOptions),
|
||||
|
@ -51,15 +46,20 @@ function initContextual (index, links) {
|
|||
$.extend({ el: $region, model: model }, options))
|
||||
);
|
||||
|
||||
// Add the model to the collection. This must happen after the views have been
|
||||
// associated with it, otherwise collection change event handlers can't
|
||||
// trigger the model change event handler in its views.
|
||||
contextual.collection.add(model);
|
||||
|
||||
// Let other JavaScript react to the adding of a new contextual link.
|
||||
$(document).trigger('drupalContextualLinkAdded', {
|
||||
$el: $links,
|
||||
$el: $contextual,
|
||||
$region: $region,
|
||||
model: model
|
||||
});
|
||||
|
||||
// Fix visual collisions between contextual link triggers.
|
||||
adjustIfNestedAndOverlapping($wrapper);
|
||||
adjustIfNestedAndOverlapping($contextual);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,7 +68,8 @@ function initContextual (index, links) {
|
|||
* This only deals with two levels of nesting; deeper levels are not touched.
|
||||
*
|
||||
* @param jQuery $contextual
|
||||
* A contextual link.
|
||||
* A contextual links placeholder DOM element, containing the actual
|
||||
* contextual links as rendered by the server.
|
||||
*/
|
||||
function adjustIfNestedAndOverlapping ($contextual) {
|
||||
var $contextuals = $contextual
|
||||
|
@ -110,7 +111,40 @@ function adjustIfNestedAndOverlapping ($contextual) {
|
|||
*/
|
||||
Drupal.behaviors.contextual = {
|
||||
attach: function (context) {
|
||||
$(context).find('.contextual-links').once('contextual').each(initContextual);
|
||||
var $context = $(context);
|
||||
|
||||
// Find all contextual links placeholders, if any.
|
||||
var $placeholders = $context.find('[data-contextual-id]').once('contextual-render');
|
||||
if ($placeholders.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect the IDs for all contextual links placeholders.
|
||||
var ids = [];
|
||||
$placeholders.each(function () {
|
||||
ids.push($(this).attr('data-contextual-id'));
|
||||
});
|
||||
|
||||
// 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 (results.hasOwnProperty(id)) {
|
||||
// Update the placeholder to contain its rendered contextual links.
|
||||
var $placeholder = $context.find('[data-contextual-id="' + id + '"]')
|
||||
.html(results[id]);
|
||||
|
||||
// Initialize the contextual link.
|
||||
initContextual($placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -370,17 +404,6 @@ Drupal.contextual = {
|
|||
// A Backbone.Collection of Drupal.contextual.Model instances.
|
||||
Drupal.contextual.collection = new Backbone.Collection([], { model: Drupal.contextual.Model });
|
||||
|
||||
|
||||
/**
|
||||
* Wraps contextual links.
|
||||
*
|
||||
* @return String
|
||||
* A string representing a DOM fragment.
|
||||
*/
|
||||
Drupal.theme.contextualWrapper = function () {
|
||||
return '<div class="contextual" />';
|
||||
};
|
||||
|
||||
/**
|
||||
* A trigger is an interactive element often bound to a click handler.
|
||||
*
|
||||
|
|
|
@ -5,6 +5,19 @@
|
|||
* Adds contextual links to perform actions related to elements on a page.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* See http://drupal.org/node/1954892.
|
||||
*/
|
||||
function contextual_custom_theme() {
|
||||
if (substr(current_path(), 0, 11) === 'contextual/') {
|
||||
return ajax_base_page_theme();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_toolbar().
|
||||
*/
|
||||
|
@ -24,8 +37,6 @@ 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'),
|
||||
|
@ -40,6 +51,22 @@ function contextual_toolbar() {
|
|||
return $tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_page_build().
|
||||
*
|
||||
* Adds the drupal.contextual-links library to the page for any user who has the
|
||||
* 'access contextual links' permission.
|
||||
*
|
||||
* @see contextual_preprocess()
|
||||
*/
|
||||
function contextual_page_build(&$page) {
|
||||
if (!user_access('access contextual links')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page['#attached']['library'][] = array('contextual', 'drupal.contextual-links');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
|
@ -115,6 +142,7 @@ function contextual_library_info() {
|
|||
array('system', 'jquery.once'),
|
||||
array('system', 'drupal.tabbingmanager'),
|
||||
array('system', 'drupal.announce'),
|
||||
array('contextual', 'drupal.contextual-links')
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -125,6 +153,10 @@ function contextual_library_info() {
|
|||
* Implements hook_element_info().
|
||||
*/
|
||||
function contextual_element_info() {
|
||||
$types['contextual_links_placeholder'] = array(
|
||||
'#pre_render' => array('contextual_pre_render_placeholder'),
|
||||
'#id' => NULL,
|
||||
);
|
||||
$types['contextual_links'] = array(
|
||||
'#pre_render' => array('contextual_pre_render_links'),
|
||||
'#theme' => 'links__contextual',
|
||||
|
@ -142,14 +174,11 @@ function contextual_element_info() {
|
|||
/**
|
||||
* Implements hook_preprocess().
|
||||
*
|
||||
* @see contextual_pre_render_links()
|
||||
* @see contextual_pre_render_placeholder()
|
||||
* @see contextual_page_build()
|
||||
* @see \Drupal\contextual\ContextualController::render()
|
||||
*/
|
||||
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.
|
||||
|
@ -165,17 +194,42 @@ 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';
|
||||
|
||||
// Renders a contextual links placeholder unconditionally, thus not breaking
|
||||
// the render cache. Although the empty placeholder is rendered for all
|
||||
// users, contextual_page_build() only adds the drupal.contextual-links
|
||||
// library for users with the 'access contextual links' permission, thus
|
||||
// preventing unnecessary HTTP requests for users without that permission.
|
||||
$variables['title_suffix']['contextual_links'] = array(
|
||||
'#type' => 'contextual_links_placeholder',
|
||||
'#id' => _contextual_links_to_id($element['#contextual_links']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-render callback: Renders a contextual links placeholder into #markup.
|
||||
*
|
||||
* Renders an empty (hence invisible) placeholder div with a data-attribute that
|
||||
* contains an identifier ("contextual id"), which allows the JavaScript of the
|
||||
* drupal.contextual-links library to dynamically render contextual links.
|
||||
*
|
||||
* @param $element
|
||||
* A structured array with #id containing a "contextual id".
|
||||
*
|
||||
* @return
|
||||
* The passed-in element with a contextual link placeholder in '#markup'.
|
||||
*
|
||||
* @see _contextual_links_to_id()
|
||||
* @see contextual_element_info()
|
||||
*/
|
||||
function contextual_pre_render_placeholder($element) {
|
||||
$element['#markup'] = '<div data-contextual-id="' . $element['#id'] . '"></div>';
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-render callback: Builds a renderable array for contextual links.
|
||||
*
|
||||
|
@ -230,3 +284,73 @@ function contextual_pre_render_links($element) {
|
|||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_contextual_links_view_alter().
|
||||
*
|
||||
* @see \Drupal\contextual\Plugin\views\field\ContextualLinks::render()
|
||||
*/
|
||||
function contextual_contextual_links_view_alter(&$element, $items) {
|
||||
if (isset($element['#contextual_links']['contextual'])) {
|
||||
$encoded_links = $element['#contextual_links']['contextual'][2]['contextual-views-field-links'];
|
||||
$element['#links'] = drupal_json_decode(rawurldecode($encoded_links));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes #contextual_links property value array to a string.
|
||||
*
|
||||
* Examples:
|
||||
* - node:node:1:
|
||||
* - views_ui:admin/structure/views/view:frontpage:location=page&view_name=frontpage&view_display_id=page_1
|
||||
* - menu:admin/structure/menu/manage:tools:|block:admin/structure/block/manage:bartik.tools:
|
||||
*
|
||||
* So, expressed in a pattern:
|
||||
* <module name>:<parent path>:<path args>:<options>
|
||||
*
|
||||
* The (dynamic) path args are joined with slashes. The options are encoded as a
|
||||
* query string.
|
||||
*
|
||||
* @param array $contextual_links
|
||||
* The $element['#contextual_links'] value for some render element.
|
||||
*
|
||||
* @return string
|
||||
* A serialized representation of a #contextual_links property value array for
|
||||
* use in a data- attribute.
|
||||
*/
|
||||
function _contextual_links_to_id($contextual_links) {
|
||||
$id = '';
|
||||
foreach ($contextual_links as $module => $args) {
|
||||
$parent_path = $args[0];
|
||||
$path_args = implode('/', $args[1]);
|
||||
$metadata = drupal_http_build_query((isset($args[2])) ? $args[2] : array());
|
||||
|
||||
if (drupal_strlen($id) > 0) {
|
||||
$id .= '|';
|
||||
}
|
||||
$id .= $module . ':' . $parent_path . ':' . $path_args . ':' . $metadata;
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unserializes the result of _contextual_links_to_id().
|
||||
*
|
||||
* @see _contextual_links_to_id
|
||||
*
|
||||
* @param string $id
|
||||
* A serialized representation of a #contextual_links property value array.
|
||||
*
|
||||
* @return array
|
||||
* The value for a #contextual_links property.
|
||||
*/
|
||||
function _contextual_id_to_links($id) {
|
||||
$contextual_links = array();
|
||||
$contexts = explode('|', $id);
|
||||
foreach ($contexts as $context) {
|
||||
list($module, $parent_path, $path_args, $metadata_raw) = explode(':', $context);
|
||||
$path_args = explode('/', $path_args);
|
||||
$metadata = drupal_get_query_array($metadata_raw);
|
||||
$contextual_links[$module] = array($parent_path, $path_args, $metadata);
|
||||
}
|
||||
return $contextual_links;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
contextual_render:
|
||||
pattern: '/contextual/render'
|
||||
defaults:
|
||||
_controller: '\Drupal\contextual\ContextualController::render'
|
||||
requirements:
|
||||
_permission: 'access contextual links'
|
|
@ -23,10 +23,7 @@ var options = {
|
|||
*/
|
||||
function initContextualToolbar (context) {
|
||||
var contextualToolbar = Drupal.contextualToolbar;
|
||||
var model = contextualToolbar.model = new contextualToolbar.Model({
|
||||
isViewing: true,
|
||||
isVisible: false
|
||||
});
|
||||
var model = contextualToolbar.model = new contextualToolbar.Model();
|
||||
|
||||
var viewOptions = $.extend({
|
||||
el: $('.js .toolbar .bar .contextual-toolbar-tab'),
|
||||
|
@ -38,27 +35,33 @@ function initContextualToolbar (context) {
|
|||
// Update the model based on overlay events.
|
||||
$(document).on({
|
||||
'drupalOverlayOpen.contextualToolbar': function () {
|
||||
model.set('isVisible', false);
|
||||
model.set('overlayIsOpen', true);
|
||||
},
|
||||
'drupalOverlayClose.contextualToolbar': function () {
|
||||
model.set('isVisible', true);
|
||||
model.set('overlayIsOpen', false);
|
||||
}
|
||||
});
|
||||
|
||||
// Show the edit tab while there's >=1 contextual link.
|
||||
var collection = Drupal.contextual.collection;
|
||||
var updateVisibility = function () {
|
||||
model.set('isVisible', collection.length > 0);
|
||||
};
|
||||
collection.on('reset remove add', updateVisibility);
|
||||
updateVisibility();
|
||||
var contextualCollection = Drupal.contextual.collection;
|
||||
function trackContextualCount () {
|
||||
model.set('contextualCount', contextualCollection.length);
|
||||
}
|
||||
contextualCollection.on('reset remove add', trackContextualCount);
|
||||
trackContextualCount();
|
||||
|
||||
// Whenever edit mode is toggled, update all contextual links.
|
||||
// Whenever edit mode is toggled, lock all contextual links.
|
||||
model.on('change:isViewing', function() {
|
||||
collection.each(function (contextualModel) {
|
||||
contextualCollection.each(function (contextualModel) {
|
||||
contextualModel.set('isLocked', !model.get('isViewing'));
|
||||
});
|
||||
});
|
||||
// When a new contextual link is added and edit mode is enabled, lock it.
|
||||
contextualCollection.on('add', function (contextualModel) {
|
||||
if (!model.get('isViewing')) {
|
||||
contextualModel.set('isLocked', true);
|
||||
}
|
||||
});
|
||||
|
||||
// Checks whether localStorage indicates we should start in edit mode
|
||||
// rather than view mode.
|
||||
|
@ -93,11 +96,21 @@ Drupal.contextualToolbar = {
|
|||
defaults: {
|
||||
// Indicates whether the toggle is currently in "view" or "edit" mode.
|
||||
isViewing: true,
|
||||
// Indicates whether the toggle should be visible or hidden.
|
||||
// Indicates whether the toggle should be visible or hidden. Automatically
|
||||
// calculated, depends on overlayIsOpen and contextualCount.
|
||||
isVisible: false,
|
||||
// Indicates whether the overlay is open or not.
|
||||
overlayIsOpen: false,
|
||||
// Tracks how many contextual links exist on the page.
|
||||
contextualCount: 0,
|
||||
// A TabbingContext object as returned by Drupal.TabbingManager: the set
|
||||
// of tabbable elements when edit mode is enabled.
|
||||
tabbingContext: null
|
||||
},
|
||||
initialize: function () {
|
||||
this.on('change:overlayIsOpen change:contextualCount', function (model) {
|
||||
model.set('isVisible', !model.get('overlayIsOpen') && model.get('contextualCount') > 0);
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\ContextualController.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerAware;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Returns responses for Contextual module routes.
|
||||
*/
|
||||
class ContextualController extends ContainerAware {
|
||||
|
||||
/**
|
||||
* Returns the requested rendered contextual links.
|
||||
*
|
||||
* Given a list of contextual links IDs, render them. Hence this must be
|
||||
* robust to handle arbitrary input.
|
||||
*
|
||||
* @see contextual_preprocess()
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse
|
||||
* The JSON response.
|
||||
*/
|
||||
public function render(Request $request) {
|
||||
$ids = $request->request->get('ids');
|
||||
if (!isset($ids)) {
|
||||
throw new BadRequestHttpException(t('No contextual ids specified.'));
|
||||
}
|
||||
|
||||
$rendered = array();
|
||||
foreach ($ids as $id) {
|
||||
$element = array(
|
||||
'#type' => 'contextual_links',
|
||||
'#contextual_links' => _contextual_id_to_links($id),
|
||||
);
|
||||
$rendered[$id] = drupal_render($element);
|
||||
}
|
||||
|
||||
return new JsonResponse($rendered);
|
||||
}
|
||||
|
||||
}
|
|
@ -64,6 +64,9 @@ class ContextualLinks extends FieldPluginBase {
|
|||
|
||||
/**
|
||||
* Render the contextual fields.
|
||||
*
|
||||
* @see contextual_preprocess()
|
||||
* @see contextual_contextual_links_view_alter()
|
||||
*/
|
||||
function render($values) {
|
||||
$links = array();
|
||||
|
@ -92,19 +95,23 @@ class ContextualLinks extends FieldPluginBase {
|
|||
}
|
||||
}
|
||||
|
||||
// Renders a contextual links placeholder.
|
||||
if (!empty($links)) {
|
||||
$build = array(
|
||||
'#prefix' => '<div class="contextual">',
|
||||
'#suffix' => '</div>',
|
||||
'#theme' => 'links__contextual',
|
||||
'#links' => $links,
|
||||
'#attributes' => array('class' => array('contextual-links')),
|
||||
'#attached' => array(
|
||||
'library' => array(array('contextual', 'contextual-links')),
|
||||
),
|
||||
'#access' => user_access('access contextual links'),
|
||||
$contextual_links = array(
|
||||
'contextual' => array(
|
||||
'',
|
||||
array(),
|
||||
array(
|
||||
'contextual-views-field-links' => drupal_encode_path(drupal_json_encode($links)),
|
||||
)
|
||||
)
|
||||
);
|
||||
return drupal_render($build);
|
||||
|
||||
$element = array(
|
||||
'#type' => 'contextual_links_placeholder',
|
||||
'#id' => _contextual_links_to_id($contextual_links),
|
||||
);
|
||||
return drupal_render($element);
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\contextual\Tests\ContextualDynamicContextTest.
|
||||
* Contains \Drupal\contextual\Tests\ContextualDynamicContextTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual\Tests;
|
||||
|
@ -19,7 +19,7 @@ class ContextualDynamicContextTest extends WebTestBase {
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('contextual', 'node', 'views');
|
||||
public static $modules = array('contextual', 'node', 'views', 'views_ui');
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
|
@ -31,16 +31,24 @@ class ContextualDynamicContextTest extends WebTestBase {
|
|||
|
||||
function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
|
||||
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
|
||||
$web_user = $this->drupalCreateUser(array('access content', 'access contextual links', 'edit any article content'));
|
||||
$this->drupalLogin($web_user);
|
||||
|
||||
$this->editor_user = $this->drupalCreateUser(array('access content', 'access contextual links', 'edit any article content'));
|
||||
$this->authenticated_user = $this->drupalCreateUser(array('access content', 'access contextual links'));
|
||||
$this->anonymous_user = $this->drupalCreateUser(array('access content'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests contextual links on node lists with different permissions.
|
||||
* Tests contextual links with different permissions.
|
||||
*
|
||||
* Ensures that contextual link placeholders always exist, even if the user is
|
||||
* not allowed to use contextual links.
|
||||
*/
|
||||
function testNodeLinks() {
|
||||
function testDifferentPermissions() {
|
||||
$this->drupalLogin($this->editor_user);
|
||||
|
||||
// Create three nodes in the following order:
|
||||
// - An article, which should be user-editable.
|
||||
// - A page, which should not be user-editable.
|
||||
|
@ -49,11 +57,120 @@ class ContextualDynamicContextTest extends WebTestBase {
|
|||
$node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
|
||||
$node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
|
||||
|
||||
// Now, on the front page, all article nodes should have contextual edit
|
||||
// links. The page node in between should not.
|
||||
// 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->nid . ':',
|
||||
'node:node:' . $node2->nid . ':',
|
||||
'node:node:' . $node3->nid . ':',
|
||||
'views_ui:admin/structure/views/view:frontpage:location=page&name=frontpage&display_id=page_1',
|
||||
);
|
||||
|
||||
// Editor user: can access contextual links and can edit articles.
|
||||
$this->drupalGet('node');
|
||||
$this->assertRaw('node/' . $node1->nid . '/edit', 'Edit link for oldest article node showing.');
|
||||
$this->assertNoRaw('node/' . $node2->nid . '/edit', 'No edit link for page nodes.');
|
||||
$this->assertRaw('node/' . $node3->nid . '/edit', 'Edit link for most recent article node showing.');
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$this->assertContextualLinkPlaceHolder($ids[$i]);
|
||||
}
|
||||
$this->renderContextualLinks(array(), 'node');
|
||||
$this->assertResponse(400);
|
||||
$this->assertRaw('No contextual ids specified.');
|
||||
$response = $this->renderContextualLinks($ids, 'node');
|
||||
$this->assertResponse(200);
|
||||
$json = drupal_json_decode($response);
|
||||
$this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="node-edit odd first last"><a href="' . base_path() . 'node/1/edit?destination=node">Edit</a></li></ul>');
|
||||
$this->assertIdentical($json[$ids[1]], '');
|
||||
$this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="node-edit odd first last"><a href="' . base_path() . 'node/3/edit?destination=node">Edit</a></li></ul>');
|
||||
$this->assertIdentical($json[$ids[3]], '');
|
||||
|
||||
// Authenticated user: can access contextual links, cannot edit articles.
|
||||
$this->drupalLogin($this->authenticated_user);
|
||||
$this->drupalGet('node');
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$this->assertContextualLinkPlaceHolder($ids[$i]);
|
||||
}
|
||||
$this->renderContextualLinks(array(), 'node');
|
||||
$this->assertResponse(400);
|
||||
$this->assertRaw('No contextual ids specified.');
|
||||
$response = $this->renderContextualLinks($ids, 'node');
|
||||
$this->assertResponse(200);
|
||||
$json = drupal_json_decode($response);
|
||||
$this->assertIdentical($json[$ids[0]], '');
|
||||
$this->assertIdentical($json[$ids[1]], '');
|
||||
$this->assertIdentical($json[$ids[2]], '');
|
||||
$this->assertIdentical($json[$ids[3]], '');
|
||||
|
||||
// Anonymous user: cannot access contextual links.
|
||||
$this->drupalLogin($this->anonymous_user);
|
||||
$this->drupalGet('node');
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$this->assertContextualLinkPlaceHolder($ids[$i]);
|
||||
}
|
||||
$this->renderContextualLinks(array(), 'node');
|
||||
$this->assertResponse(403);
|
||||
$this->renderContextualLinks($ids, 'node');
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a contextual link placeholder with the given id exists.
|
||||
*
|
||||
* @param string $id
|
||||
* A contextual link id.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function assertContextualLinkPlaceHolder($id) {
|
||||
$this->assertRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a contextual link placeholder with the given id does not exist.
|
||||
*
|
||||
* @param string $id
|
||||
* A contextual link id.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function assertNoContextualLinkPlaceHolder($id) {
|
||||
$this->assertNoRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id does not exist.', array('@id' => $id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server-rendered contextual links for the given contextual link ids.
|
||||
*
|
||||
* @param array $ids
|
||||
* An array of contextual link ids.
|
||||
* @param string $current_path
|
||||
* The Drupal path for the page for which the contextual links are rendered.
|
||||
*
|
||||
* @return string
|
||||
* The response body.
|
||||
*/
|
||||
protected function renderContextualLinks($ids, $current_path) {
|
||||
// Build POST values.
|
||||
$post = array();
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$post['ids[' . $i . ']'] = $ids[$i];
|
||||
}
|
||||
|
||||
// Serialize POST values.
|
||||
foreach ($post as $key => $value) {
|
||||
// Encode according to application/x-www-form-urlencoded
|
||||
// Both names and values needs to be urlencoded, according to
|
||||
// http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
|
||||
$post[$key] = urlencode($key) . '=' . urlencode($value);
|
||||
}
|
||||
$post = implode('&', $post);
|
||||
|
||||
// Perform HTTP request.
|
||||
return $this->curlExec(array(
|
||||
CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => $current_path))),
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $post,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\Tests\ContextualUnitTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual\Tests;
|
||||
|
||||
use Drupal\simpletest\UnitTestBase;
|
||||
|
||||
/**
|
||||
* Tests _contextual_links_to_id() & _contextual_id_to_links().
|
||||
*/
|
||||
class ContextualUnitTest extends UnitTestBase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Conversion to and from "contextual id"s (for placeholders)',
|
||||
'description' => 'Tests all edge cases of converting from #contextual_links to ids and vice versa.',
|
||||
'group' => 'Contextual',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides testcases for testContextualLinksToId() and
|
||||
*/
|
||||
function _contextual_links_id_testcases() {
|
||||
// Test branch conditions:
|
||||
// - one module.
|
||||
// - one dynamic path argument.
|
||||
// - no metadata.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'node' => array(
|
||||
'node',
|
||||
array('14031991'),
|
||||
array()
|
||||
),
|
||||
),
|
||||
'id' => 'node:node:14031991:',
|
||||
);
|
||||
|
||||
// Test branch conditions:
|
||||
// - one module.
|
||||
// - multiple dynamic path arguments.
|
||||
// - no metadata.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'foo' => array(
|
||||
'baz/in/ga',
|
||||
array('bar', 'baz', 'qux'),
|
||||
array()
|
||||
),
|
||||
),
|
||||
'id' => 'foo:baz/in/ga:bar/baz/qux:',
|
||||
);
|
||||
|
||||
// Test branch conditions:
|
||||
// - one module.
|
||||
// - one dynamic path argument.
|
||||
// - metadata.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'views_ui' => array(
|
||||
'admin/structure/views/view',
|
||||
array('frontpage'),
|
||||
array(
|
||||
'location' => 'page',
|
||||
'display' => 'page_1',
|
||||
)
|
||||
),
|
||||
),
|
||||
'id' => 'views_ui:admin/structure/views/view:frontpage:location=page&display=page_1',
|
||||
);
|
||||
|
||||
// Test branch conditions:
|
||||
// - multiple modules.
|
||||
// - multiple dynamic path arguments.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'node' => array(
|
||||
'node',
|
||||
array('14031991'),
|
||||
array()
|
||||
),
|
||||
'foo' => array(
|
||||
'baz/in/ga',
|
||||
array('bar', 'baz', 'qux'),
|
||||
array()
|
||||
),
|
||||
'edge' => array(
|
||||
'edge',
|
||||
array('20011988'),
|
||||
array()
|
||||
),
|
||||
),
|
||||
'id' => 'node:node:14031991:|foo:baz/in/ga:bar/baz/qux:|edge:edge:20011988:',
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests _contextual_links_to_id().
|
||||
*/
|
||||
function testContextualLinksToId() {
|
||||
$tests = $this->_contextual_links_id_testcases();
|
||||
foreach ($tests as $test) {
|
||||
$this->assertIdentical(_contextual_links_to_id($test['links']), $test['id']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests _contextual_id_to_links().
|
||||
*/
|
||||
function testContextualIdToLinks() {
|
||||
$tests = $this->_contextual_links_id_testcases();
|
||||
foreach ($tests as $test) {
|
||||
$this->assertIdentical(_contextual_id_to_links($test['id']), $test['links']);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -271,9 +271,9 @@ function fetchMissingMetadata (callback) {
|
|||
* An object with the following properties:
|
||||
* - String entity: an Edit entity identifier, e.g. "node/1" or
|
||||
* "custom_block/5".
|
||||
* - jQuery $el: element pointing to the contextual links for this entity.
|
||||
* - jQuery $region: element pointing to the contextual region for this
|
||||
* - DOM el: element pointing to the contextual links placeholder for this
|
||||
* entity.
|
||||
* - DOM region: element pointing to the contextual region for this entity.
|
||||
* @return Boolean
|
||||
* Returns true when a contextual the given contextual link metadata can be
|
||||
* removed from the queue (either because the contextual link has been set up
|
||||
|
@ -324,8 +324,9 @@ function initializeEntityContextualLink (contextualLink) {
|
|||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
// Set up contextual link view.
|
||||
var $links = $(contextualLink.el).find('.contextual-links');
|
||||
var contextualLinkView = new Drupal.edit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quick-edit"><a href=""></a></li>').prependTo(contextualLink.el),
|
||||
el: $('<li class="quick-edit"><a href=""></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.edit.app.model
|
||||
}, options));
|
||||
|
|
|
@ -306,11 +306,30 @@ class MenuTest extends WebTestBase {
|
|||
* Tests the contextual links on a menu block.
|
||||
*/
|
||||
public function testBlockContextualLinks() {
|
||||
$this->drupalLogin($this->drupalCreateUser(array('administer menu', 'access contextual links')));
|
||||
$this->drupalLogin($this->drupalCreateUser(array('administer menu', 'access contextual links', 'administer blocks')));
|
||||
$this->addMenuLink();
|
||||
$this->drupalPlaceBlock('system_menu_block:menu-tools', array('label' => 'Tools', 'module' => 'system'));
|
||||
$block = $this->drupalPlaceBlock('system_menu_block:menu-tools', array('label' => 'Tools', 'module' => 'system'));
|
||||
$this->drupalGet('test-page');
|
||||
$this->assertLinkByHref("admin/structure/menu/manage/tools/edit");
|
||||
|
||||
$id = 'block:admin/structure/block/manage:' . $block->id() . ':|menu:admin/structure/menu/manage:tools:';
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
|
||||
$this->assertRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
|
||||
|
||||
// Get server-rendered contextual links.
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
|
||||
$post = urlencode('ids[0]') . '=' . urlencode($id);
|
||||
$response = $this->curlExec(array(
|
||||
CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => 'test-page'))),
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $post,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
),
|
||||
));
|
||||
$this->assertResponse(200);
|
||||
$json = drupal_json_decode($response);
|
||||
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure odd first"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '/configure?destination=test-page">Configure block</a></li><li class="menu-edit even last"><a href="' . base_path() . 'admin/structure/menu/manage/tools/edit?destination=test-page">Edit menu</a></li></ul>');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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').attr('data-views-page-contextual-id');
|
||||
|
||||
$('[data-contextual-id="' + id + '"]')
|
||||
.closest(':has(.view)')
|
||||
.addClass('contextual-region');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -468,6 +468,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
|
||||
|
@ -480,10 +485,11 @@ function views_preprocess_html(&$variables) {
|
|||
// page.tpl.php, so we can only find it using JavaScript. We therefore remove
|
||||
// the "contextual-region" class from the <body> tag here and add
|
||||
// JavaScript that will insert it back in the correct place.
|
||||
if (!empty($variables['page']['#views_contextual_links_info'])) {
|
||||
if (!empty($variables['page']['#views_contextual_links'])) {
|
||||
$key = array_search('contextual-region', $variables['attributes']['class']);
|
||||
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.
|
||||
*
|
||||
|
@ -612,12 +606,15 @@ function views_add_contextual_links(&$render_element, $location, ViewExecutable
|
|||
// If the link was valid, attach information about it to the renderable
|
||||
// array.
|
||||
if ($valid) {
|
||||
$render_element['#contextual_links'][$module] = array($link['parent path'], $args);
|
||||
$render_element['#views_contextual_links_info'][$module] = array(
|
||||
'location' => $location,
|
||||
'view' => $view,
|
||||
'view_name' => $view->storage->id(),
|
||||
'view_display_id' => $display_id,
|
||||
$render_element['#views_contextual_links'] = TRUE;
|
||||
$render_element['#contextual_links'][$module] = array(
|
||||
$link['parent path'],
|
||||
$args,
|
||||
array(
|
||||
'location' => $location,
|
||||
'name' => $view->storage->id(),
|
||||
'display_id' => $display_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -284,8 +284,27 @@ class DisplayTest extends UITestBase {
|
|||
$this->drupalLogin($this->drupalCreateUser(array('administer views', 'access contextual links')));
|
||||
$view = entity_load('view', 'test_display');
|
||||
$view->enable()->save();
|
||||
|
||||
$this->drupalGet('test-display');
|
||||
$this->assertLinkByHref("admin/structure/views/view/{$view->id()}/edit/page_1");
|
||||
$id = 'views_ui:admin/structure/views/view:test_display:location=page&name=test_display&display_id=page_1';
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
|
||||
$this->assertRaw('<div data-contextual-id="'. $id . '"></div>', format_string('Contextual link placeholder with id @id exists.', array('@id' => $id)));
|
||||
|
||||
// Get server-rendered contextual links.
|
||||
// @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
|
||||
$post = urlencode('ids[0]') . '=' . urlencode($id);
|
||||
$response = $this->curlExec(array(
|
||||
CURLOPT_URL => url('contextual/render', array('absolute' => TRUE, 'query' => array('destination' => 'test-display'))),
|
||||
CURLOPT_POST => TRUE,
|
||||
CURLOPT_POSTFIELDS => $post,
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
),
|
||||
));
|
||||
$this->assertResponse(200);
|
||||
$json = drupal_json_decode($response);
|
||||
$this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="views-ui-edit odd first last"><a href="' . base_path() . 'admin/structure/views/view/test_display/edit/page_1?destination=test-display">Edit view</a></li></ul>');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -339,8 +339,8 @@ function views_ui_contextual_links_view_alter(&$element, $items) {
|
|||
// Append the display ID to the Views UI edit links, so that clicking on the
|
||||
// contextual link takes you directly to the correct display tab on the edit
|
||||
// screen.
|
||||
elseif (!empty($element['#links']['views-ui-edit']) && !empty($element['#element']['#views_contextual_links_info']['views_ui']['view_display_id'])) {
|
||||
$display_id = $element['#element']['#views_contextual_links_info']['views_ui']['view_display_id'];
|
||||
elseif (!empty($element['#links']['views-ui-edit'])) {
|
||||
$display_id = $element['#contextual_links']['views_ui'][2]['display_id'];
|
||||
$element['#links']['views-ui-edit']['href'] .= '/' . $display_id;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue