Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
|
@ -0,0 +1,15 @@
|
|||
# Schema for the views plugins of the Contextual module.
|
||||
|
||||
views.field.contextual_links:
|
||||
type: views_field
|
||||
label: 'Contextual link'
|
||||
mapping:
|
||||
fields:
|
||||
type: sequence
|
||||
label: 'Fields'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Link'
|
||||
destination:
|
||||
type: boolean
|
||||
label: 'Include destination'
|
42
core/modules/contextual/contextual.api.php
Normal file
42
core/modules/contextual/contextual.api.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by Contextual module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Alter a contextual links element before it is rendered.
|
||||
*
|
||||
* This hook is invoked by contextual_pre_render_links(). The renderable array
|
||||
* of #type 'contextual_links', containing the entire contextual links data that
|
||||
* is passed in by reference. Further links may be added or existing links can
|
||||
* be altered.
|
||||
*
|
||||
* @param $element
|
||||
* A renderable array representing the contextual links.
|
||||
* @param $items
|
||||
* An associative array containing the original contextual link items, as
|
||||
* generated by
|
||||
* \Drupal\Core\Menu\ContextualLinkManagerInterface::getContextualLinksArrayByGroup(),
|
||||
* which were used to build $element['#links'].
|
||||
*
|
||||
* @see hook_contextual_links_alter()
|
||||
* @see hook_contextual_links_plugins_alter()
|
||||
* @see contextual_pre_render_links()
|
||||
* @see contextual_element_info()
|
||||
*/
|
||||
function hook_contextual_links_view_alter(&$element, $items) {
|
||||
// Add another class to all contextual link lists to facilitate custom
|
||||
// styling.
|
||||
$element['#attributes']['class'][] = 'custom-class';
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
6
core/modules/contextual/contextual.info.yml
Normal file
6
core/modules/contextual/contextual.info.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
name: 'Contextual Links'
|
||||
type: module
|
||||
description: 'Provides contextual links to perform actions related to elements on a page.'
|
||||
package: Core
|
||||
version: VERSION
|
||||
core: 8.x
|
46
core/modules/contextual/contextual.libraries.yml
Normal file
46
core/modules/contextual/contextual.libraries.yml
Normal file
|
@ -0,0 +1,46 @@
|
|||
drupal.contextual-links:
|
||||
version: VERSION
|
||||
js:
|
||||
# Ensure to run before contextual/drupal.context-toolbar.
|
||||
# Core.
|
||||
js/contextual.js: { weight: -2 }
|
||||
# Models.
|
||||
js/models/StateModel.js: { weight: -2 }
|
||||
# Views.
|
||||
js/views/AuralView.js: { weight: -2 }
|
||||
js/views/KeyboardView.js: { weight: -2 }
|
||||
js/views/RegionView.js: { weight: -2 }
|
||||
js/views/VisualView.js: { weight: -2 }
|
||||
css:
|
||||
component:
|
||||
css/contextual.module.css: {}
|
||||
theme:
|
||||
css/contextual.theme.css: {}
|
||||
css/contextual.icons.theme.css: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupalSettings
|
||||
- core/backbone
|
||||
- core/modernizr
|
||||
- core/jquery.once
|
||||
|
||||
drupal.contextual-toolbar:
|
||||
version: VERSION
|
||||
js:
|
||||
js/contextual.toolbar.js: {}
|
||||
# Models.
|
||||
js/toolbar/models/StateModel.js: {}
|
||||
# Views.
|
||||
js/toolbar/views/AuralView.js: {}
|
||||
js/toolbar/views/VisualView.js: {}
|
||||
css:
|
||||
component:
|
||||
css/contextual.toolbar.css: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/backbone
|
||||
- core/jquery.once
|
||||
- core/drupal.tabbingmanager
|
||||
- core/drupal.announce
|
206
core/modules/contextual/contextual.module
Normal file
206
core/modules/contextual/contextual.module
Normal file
|
@ -0,0 +1,206 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Adds contextual links to perform actions related to elements on a page.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_toolbar().
|
||||
*/
|
||||
function contextual_toolbar() {
|
||||
$items = [];
|
||||
$items['contextual'] = [
|
||||
'#cache' => [
|
||||
'contexts' => [
|
||||
'user.permissions',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
$items['contextual'] += array(
|
||||
'#type' => 'toolbar_item',
|
||||
'tab' => array(
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'button',
|
||||
'#value' => t('Edit'),
|
||||
'#attributes' => array(
|
||||
'class' => array('toolbar-icon', 'toolbar-icon-edit'),
|
||||
'role' => 'button',
|
||||
'aria-pressed' => 'false',
|
||||
),
|
||||
),
|
||||
'#wrapper_attributes' => array(
|
||||
'class' => array('hidden', 'contextual-toolbar-tab'),
|
||||
),
|
||||
'#attached' => array(
|
||||
'library' => array(
|
||||
'contextual/drupal.contextual-toolbar',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_page_attachments().
|
||||
*
|
||||
* 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_attachments(array &$page) {
|
||||
if (!\Drupal::currentUser()->hasPermission('access contextual links')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page['#attached']['library'][] = 'contextual/drupal.contextual-links';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function contextual_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
case 'help.page.contextual':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Contextual links module gives users with the <em>Use contextual links</em> permission quick access to tasks associated with certain areas of pages on your site. For example, a menu displayed as a block has links to edit the menu and configure the block. For more information, see <a href="!contextual">the online documentation for the Contextual Links module</a>.', array('!contextual' => 'https://www.drupal.org/documentation/modules/contextual')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Displaying contextual links') . '</dt>';
|
||||
$output .= '<dd>';
|
||||
$output .= t('Contextual links for an area on a page are displayed using a contextual links button. There are two ways to make the contextual links button visible:');
|
||||
$output .= '<ol>';
|
||||
$sample_picture = '<img src="' . file_create_url('core/misc/icons/bebebe/pencil.svg') . '" alt="' . t('contextual links button') . '" />';
|
||||
$output .= '<li>' . t('Hovering over the area of interest will temporarily make the contextual links button visible (which looks like a pencil in most themes, and is normally displayed in the upper right corner of the area). The icon typically looks like this: !picture', array('!picture' => $sample_picture)) . '</li>';
|
||||
$output .= '<li>' . t('If you have the <a href="!toolbar">Toolbar module</a> enabled, clicking the contextual links button in the toolbar (which looks like a pencil) will make all contextual links buttons on the page visible. Clicking this button again will toggle them to invisible.', array('!toolbar' => (\Drupal::moduleHandler()->moduleExists('toolbar')) ? \Drupal::url('help.page', array('name' => 'toolbar')) : '#')) . '</li>';
|
||||
$output .= '</ol>';
|
||||
$output .= t('Once the contextual links button for the area of interest is visible, click the button to display the links.');
|
||||
$output .= '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess().
|
||||
*
|
||||
* @see contextual_pre_render_placeholder()
|
||||
* @see contextual_page_attachments()
|
||||
* @see \Drupal\contextual\ContextualController::render()
|
||||
*/
|
||||
function contextual_preprocess(&$variables, $hook, $info) {
|
||||
// Determine the primary theme function argument.
|
||||
if (!empty($info['variables'])) {
|
||||
$keys = array_keys($info['variables']);
|
||||
$key = $keys[0];
|
||||
}
|
||||
elseif (!empty($info['render element'])) {
|
||||
$key = $info['render element'];
|
||||
}
|
||||
if (!empty($key) && isset($variables[$key])) {
|
||||
$element = $variables[$key];
|
||||
}
|
||||
|
||||
if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) {
|
||||
// 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_attachments() only adds the asset 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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']['metadata']['contextual-views-field-links'];
|
||||
$element['#links'] = Json::decode(rawurldecode($encoded_links));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes #contextual_links property value array to a string.
|
||||
*
|
||||
* Examples:
|
||||
* - node:node=1:langcode=en
|
||||
* - views_ui_edit:view=frontpage:location=page&view_name=frontpage&view_display_id=page_1&langcode=en
|
||||
* - menu:menu=tools:langcode=en|block:block=bartik.tools:langcode=en
|
||||
*
|
||||
* So, expressed in a pattern:
|
||||
* <group>:<route parameters>:<metadata>
|
||||
*
|
||||
* The route parameters and options are encoded as query strings.
|
||||
*
|
||||
* @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) {
|
||||
$ids = array();
|
||||
$langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
|
||||
foreach ($contextual_links as $group => $args) {
|
||||
$route_parameters = UrlHelper::buildQuery($args['route_parameters']);
|
||||
$args += ['metadata' => []];
|
||||
// Add the current URL language to metadata so a different ID will be
|
||||
// computed when URLs vary by language. This allows to store different
|
||||
// language-aware contextual links on the client side.
|
||||
$args['metadata'] += ['langcode' => $langcode];
|
||||
$metadata = UrlHelper::buildQuery($args['metadata']);
|
||||
$ids[] = "{$group}:{$route_parameters}:{$metadata}";
|
||||
}
|
||||
return implode('|', $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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($group, $route_parameters_raw, $metadata_raw) = explode(':', $context);
|
||||
parse_str($route_parameters_raw, $route_parameters);
|
||||
$metadata = array();
|
||||
parse_str($metadata_raw, $metadata);
|
||||
$contextual_links[$group] = array(
|
||||
'route_parameters' => $route_parameters,
|
||||
'metadata' => $metadata,
|
||||
);
|
||||
}
|
||||
return $contextual_links;
|
||||
}
|
3
core/modules/contextual/contextual.permissions.yml
Normal file
3
core/modules/contextual/contextual.permissions.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
access contextual links:
|
||||
title: 'Use contextual links'
|
||||
description: 'Use contextual links to perform actions related to elements on a page.'
|
8
core/modules/contextual/contextual.routing.yml
Normal file
8
core/modules/contextual/contextual.routing.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
contextual.render:
|
||||
path: '/contextual/render'
|
||||
defaults:
|
||||
_controller: '\Drupal\contextual\ContextualController::render'
|
||||
options:
|
||||
_theme: ajax_base_page
|
||||
requirements:
|
||||
_permission: 'access contextual links'
|
19
core/modules/contextual/contextual.views.inc
Normal file
19
core/modules/contextual/contextual.views.inc
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provide views data for contextual.module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_views_data_alter().
|
||||
*/
|
||||
function contextual_views_data_alter(&$data) {
|
||||
$data['views']['contextual_links'] = array(
|
||||
'title' => t('Contextual Links'),
|
||||
'help' => t('Display fields in a contextual links menu.'),
|
||||
'field' => array(
|
||||
'id' => 'contextual_links',
|
||||
),
|
||||
);
|
||||
}
|
39
core/modules/contextual/css/contextual.icons.theme.css
Normal file
39
core/modules/contextual/css/contextual.icons.theme.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @file
|
||||
* Styling for contextual module icons.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Toolbar tab icon.
|
||||
*/
|
||||
.toolbar-bar .toolbar-icon-edit:before {
|
||||
background-image: url(../../../misc/icons/bebebe/pencil.svg);
|
||||
}
|
||||
.toolbar-bar .toolbar-icon-edit:active:before,
|
||||
.toolbar-bar .toolbar-icon-edit.is-active:before {
|
||||
background-image: url(../../../misc/icons/ffffff/pencil.svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual trigger.
|
||||
*/
|
||||
.contextual .trigger {
|
||||
background-image: url(../../../misc/icons/bebebe/pencil.svg);
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 16px 16px;
|
||||
/* Override the .focusable height: auto */
|
||||
height: 26px !important;
|
||||
/* Override the .focusable height: auto */
|
||||
width: 26px !important;
|
||||
text-indent: -9999px;
|
||||
}
|
||||
|
||||
.contextual .trigger:hover {
|
||||
background-image: url(../../../misc/icons/787878/pencil.svg);
|
||||
}
|
||||
|
||||
.contextual .trigger:focus {
|
||||
background-image: url(../../../misc/icons/5181c6/pencil.svg);
|
||||
outline: none;
|
||||
}
|
18
core/modules/contextual/css/contextual.module.css
Normal file
18
core/modules/contextual/css/contextual.module.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @file
|
||||
* Generic base styles for contextual module.
|
||||
*/
|
||||
|
||||
.contextual-region {
|
||||
position: relative;
|
||||
}
|
||||
.contextual .trigger:focus {
|
||||
/* Override the .focusable position: static */
|
||||
position: relative !important;
|
||||
}
|
||||
.contextual-links {
|
||||
display: none;
|
||||
}
|
||||
.contextual.open .contextual-links {
|
||||
display: block;
|
||||
}
|
113
core/modules/contextual/css/contextual.theme.css
Normal file
113
core/modules/contextual/css/contextual.theme.css
Normal file
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* @file
|
||||
* Styling for contextual module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contextual links wrappers.
|
||||
*/
|
||||
.contextual {
|
||||
position: absolute;
|
||||
right: 0; /* LTR */
|
||||
top: 6px;
|
||||
z-index: 500;
|
||||
}
|
||||
[dir="rtl"] .contextual {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual region.
|
||||
*/
|
||||
.contextual-region.focus {
|
||||
outline: 1px dashed #d6d6d6;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual trigger.
|
||||
*/
|
||||
.contextual .trigger {
|
||||
background-attachment: scroll;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 13px;
|
||||
float: right; /* LTR */
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 2px;
|
||||
position: relative;
|
||||
right: 6px; /* LTR */
|
||||
cursor: pointer;
|
||||
}
|
||||
[dir="rtl"] .contextual .trigger {
|
||||
float: left;
|
||||
right: auto;
|
||||
left: 6px;
|
||||
}
|
||||
.contextual.open .trigger {
|
||||
border: 1px solid #ccc;
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 13px 13px 0 0;
|
||||
box-shadow: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual links.
|
||||
*
|
||||
* The following selectors are heavy to discourage theme overriding.
|
||||
*/
|
||||
.contextual-region .contextual .contextual-links {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px 0 4px 4px; /* LTR */
|
||||
clear: both;
|
||||
float: right; /* LTR */
|
||||
margin: 0;
|
||||
padding: 0.25em 0;
|
||||
position: relative;
|
||||
right: 6px; /* LTR */
|
||||
text-align: left; /* LTR */
|
||||
top: -1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[dir="rtl"] .contextual-region .contextual .contextual-links {
|
||||
border-radius: 0 4px 4px 4px;
|
||||
float: left;
|
||||
left: 6px;
|
||||
right: auto;
|
||||
text-align: right;
|
||||
}
|
||||
.contextual-region .contextual .contextual-links li {
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
list-style: none;
|
||||
list-style-image: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 100%;
|
||||
}
|
||||
.contextual-region .contextual .contextual-links a {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
display: block;
|
||||
font-family: sans-serif;
|
||||
font-size: small;
|
||||
line-height: 0.8em;
|
||||
margin: 0.25em 0;
|
||||
padding: 0.4em 0.6em;
|
||||
}
|
||||
.touch .contextual-region .contextual .contextual-links a {
|
||||
font-size: large;
|
||||
}
|
||||
.contextual-region .contextual .contextual-links a,
|
||||
.contextual-region .contextual .contextual-links a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.no-touch .contextual-region .contextual .contextual-links li a:hover {
|
||||
color: white;
|
||||
background-image: -webkit-linear-gradient(rgb(78,159,234) 0%, rgb(65,126,210) 100%);
|
||||
background-image: linear-gradient(rgb(78,159,234) 0%,rgb(65,126,210) 100%);
|
||||
}
|
30
core/modules/contextual/css/contextual.toolbar.css
Normal file
30
core/modules/contextual/css/contextual.toolbar.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @file
|
||||
* Styling for contextual module's toolbar tab.
|
||||
*/
|
||||
|
||||
/* Tab appearance. */
|
||||
.toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab {
|
||||
float: right; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab {
|
||||
float: left;
|
||||
}
|
||||
.toolbar .toolbar-bar .contextual-toolbar-tab .toolbar-item {
|
||||
margin: 0;
|
||||
/* Hide tab text. */
|
||||
padding-left: 1.3333em; /* LTR */
|
||||
text-indent: -9999px;
|
||||
}
|
||||
[dir="rtl"] .toolbar .toolbar-bar .contextual-toolbar-tab .toolbar-item {
|
||||
padding-right: 1.3333em;
|
||||
}
|
||||
.toolbar .toolbar-bar .contextual-toolbar-tab .toolbar-item.is-active {
|
||||
background-image:-webkit-linear-gradient(rgb(78,159,234) 0%, rgb(69,132,221) 100%);
|
||||
background-image:linear-gradient(rgb(78,159,234) 0%,rgb(69,132,221) 100%);
|
||||
}
|
||||
|
||||
/* @todo get rid of this declaration by making toolbar.module's CSS less specific */
|
||||
.toolbar .toolbar-bar .contextual-toolbar-tab.toolbar-tab.hidden {
|
||||
display: none;
|
||||
}
|
BIN
core/modules/contextual/images/gear-select.png
Normal file
BIN
core/modules/contextual/images/gear-select.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 506 B |
250
core/modules/contextual/js/contextual.js
Normal file
250
core/modules/contextual/js/contextual.js
Normal file
|
@ -0,0 +1,250 @@
|
|||
/**
|
||||
* @file
|
||||
* Attaches behaviors for the Contextual module.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings, _, Backbone, JSON, storage) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var options = $.extend(drupalSettings.contextual,
|
||||
// Merge strings on top of drupalSettings so that they are not mutable.
|
||||
{
|
||||
strings: {
|
||||
open: Drupal.t('Open'),
|
||||
close: Drupal.t('Close')
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 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, 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.path.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').eq(0).text().trim()
|
||||
});
|
||||
var viewOptions = $.extend({el: $contextual, model: model}, options);
|
||||
contextual.views.push({
|
||||
visual: new contextual.VisualView(viewOptions),
|
||||
aural: new contextual.AuralView(viewOptions),
|
||||
keyboard: new contextual.KeyboardView(viewOptions)
|
||||
});
|
||||
contextual.regionViews.push(new contextual.RegionView(
|
||||
$.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: $contextual,
|
||||
$region: $region,
|
||||
model: model
|
||||
});
|
||||
|
||||
// Fix visual collisions between contextual link triggers.
|
||||
adjustIfNestedAndOverlapping($contextual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a contextual link is nested & overlapping, if so: adjusts it.
|
||||
*
|
||||
* This only deals with two levels of nesting; deeper levels are not touched.
|
||||
*
|
||||
* @param {jQuery} $contextual
|
||||
* A contextual links placeholder DOM element, containing the actual
|
||||
* contextual links as rendered by the server.
|
||||
*/
|
||||
function adjustIfNestedAndOverlapping($contextual) {
|
||||
var $contextuals = $contextual
|
||||
// @todo confirm that .closest() is not sufficient
|
||||
.parents('.contextual-region').eq(-1)
|
||||
.find('.contextual');
|
||||
|
||||
// Early-return when there's no nesting.
|
||||
if ($contextuals.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the two contextual links overlap, then we move the second one.
|
||||
var firstTop = $contextuals.eq(0).offset().top;
|
||||
var secondTop = $contextuals.eq(1).offset().top;
|
||||
if (firstTop === secondTop) {
|
||||
var $nestedContextual = $contextuals.eq(1);
|
||||
|
||||
// Retrieve height of nested contextual link.
|
||||
var height = 0;
|
||||
var $trigger = $nestedContextual.find('.trigger');
|
||||
// Elements with the .visually-hidden class have no dimensions, so this
|
||||
// class must be temporarily removed to the calculate the height.
|
||||
$trigger.removeClass('visually-hidden');
|
||||
height = $nestedContextual.height();
|
||||
$trigger.addClass('visually-hidden');
|
||||
|
||||
// Adjust nested contextual link's position.
|
||||
$nestedContextual.css({top: $nestedContextual.position().top + height});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.contextual = {
|
||||
attach: function (context) {
|
||||
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'));
|
||||
});
|
||||
|
||||
// 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) {
|
||||
// Initialize after the current execution cycle, to make the AJAX
|
||||
// request for retrieving the uncached contextual links as soon as
|
||||
// possible, but also to ensure that other Drupal behaviors have had the
|
||||
// chance to set up an event listener on the Backbone collection
|
||||
// Drupal.contextual.collection.
|
||||
window.setTimeout(function () {
|
||||
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.
|
||||
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).
|
||||
$placeholders = $context.find('[data-contextual-id="' + contextualID + '"]');
|
||||
|
||||
// Initialize the contextual links.
|
||||
for (var i = 0; i < $placeholders.length; i++) {
|
||||
initContextual($placeholders.eq(i), html);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.contextual = {
|
||||
|
||||
/**
|
||||
* The {@link Drupal.contextual.View} instances associated with each list
|
||||
* element of contextual links.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
views: [],
|
||||
|
||||
/**
|
||||
* The {@link Drupal.contextual.RegionView} instances associated with each contextual
|
||||
* region element.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
regionViews: []
|
||||
};
|
||||
|
||||
/**
|
||||
* A Backbone.Collection of {@link Drupal.contextual.StateModel} instances.
|
||||
*
|
||||
* @type {Backbone.Collection}
|
||||
*/
|
||||
Drupal.contextual.collection = new Backbone.Collection([], {model: Drupal.contextual.StateModel});
|
||||
|
||||
/**
|
||||
* A trigger is an interactive element often bound to a click handler.
|
||||
*
|
||||
* @return {string}
|
||||
* A string representing a DOM fragment.
|
||||
*/
|
||||
Drupal.theme.contextualTrigger = function () {
|
||||
return '<button class="trigger visually-hidden focusable" type="button"></button>';
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, drupalSettings, _, Backbone, window.JSON, window.sessionStorage);
|
72
core/modules/contextual/js/contextual.toolbar.js
Normal file
72
core/modules/contextual/js/contextual.toolbar.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* @file
|
||||
* Attaches behaviors for the Contextual module's edit toolbar tab.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, Backbone) {
|
||||
|
||||
"use strict";
|
||||
|
||||
var strings = {
|
||||
tabbingReleased: Drupal.t('Tabbing is no longer constrained by the Contextual module.'),
|
||||
tabbingConstrained: Drupal.t('Tabbing is constrained to a set of @contextualsCount and the edit mode toggle.'),
|
||||
pressEsc: Drupal.t('Press the esc key to exit.')
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a contextual link: updates its DOM, sets up model and views.
|
||||
*
|
||||
* @param {HTMLElement} context
|
||||
* A contextual links DOM element as rendered by the server.
|
||||
*/
|
||||
function initContextualToolbar(context) {
|
||||
if (!Drupal.contextual || !Drupal.contextual.collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
var contextualToolbar = Drupal.contextualToolbar;
|
||||
var model = contextualToolbar.model = new contextualToolbar.StateModel({
|
||||
// Checks whether localStorage indicates we should start in edit mode
|
||||
// rather than view mode.
|
||||
// @see Drupal.contextualToolbar.VisualView.persist()
|
||||
isViewing: localStorage.getItem('Drupal.contextualToolbar.isViewing') !== 'false'
|
||||
}, {
|
||||
contextualCollection: Drupal.contextual.collection
|
||||
});
|
||||
|
||||
var viewOptions = {
|
||||
el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'),
|
||||
model: model,
|
||||
strings: strings
|
||||
};
|
||||
new contextualToolbar.VisualView(viewOptions);
|
||||
new contextualToolbar.AuralView(viewOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches contextual's edit toolbar tab behavior.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.contextualToolbar = {
|
||||
attach: function (context) {
|
||||
if ($('body').once('contextualToolbar-init').length) {
|
||||
initContextualToolbar(context);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.contextualToolbar = {
|
||||
|
||||
/**
|
||||
* The {@link Drupal.contextualToolbar.StateModel} instance.
|
||||
*
|
||||
* @type {?Drupal.contextualToolbar.StateModel}
|
||||
*/
|
||||
model: null
|
||||
};
|
||||
|
||||
})(jQuery, Drupal, Backbone);
|
128
core/modules/contextual/js/models/StateModel.js
Normal file
128
core/modules/contextual/js/models/StateModel.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone Model for the state of a contextual link's trigger, list & region.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Models the state of a contextual link's trigger, list & region.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*/
|
||||
Drupal.contextual.StateModel = Backbone.Model.extend(/** @lends Drupal.contextual.StateModel# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {string} title
|
||||
* @prop {bool} regionIsHovered
|
||||
* @prop {bool} hasFocus
|
||||
* @prop {bool} isOpen
|
||||
* @prop {bool} isLocked
|
||||
*/
|
||||
defaults: /** @lends Drupal.contextual.StateModel# */{
|
||||
|
||||
/**
|
||||
* The title of the entity to which these contextual links apply.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
title: '',
|
||||
|
||||
/**
|
||||
* Represents if the contextual region is being hovered.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
regionIsHovered: false,
|
||||
|
||||
/**
|
||||
* Represents if the contextual trigger or options have focus.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
hasFocus: false,
|
||||
|
||||
/**
|
||||
* Represents if the contextual options for an entity are available to
|
||||
* be selected (i.e. whether the list of options is visible).
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
isOpen: false,
|
||||
|
||||
/**
|
||||
* When the model is locked, the trigger remains active.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
isLocked: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens or closes the contextual link.
|
||||
*
|
||||
* If it is opened, then also give focus.
|
||||
*
|
||||
* @return {Drupal.contextual.StateModel}
|
||||
*/
|
||||
toggleOpen: function () {
|
||||
var newIsOpen = !this.get('isOpen');
|
||||
this.set('isOpen', newIsOpen);
|
||||
if (newIsOpen) {
|
||||
this.focus();
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes this contextual link.
|
||||
*
|
||||
* Does not call blur() because we want to allow a contextual link to have
|
||||
* focus, yet be closed for example when hovering.
|
||||
*
|
||||
* @return {Drupal.contextual.StateModel}
|
||||
*/
|
||||
close: function () {
|
||||
this.set('isOpen', false);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gives focus to this contextual link.
|
||||
*
|
||||
* Also closes + removes focus from every other contextual link.
|
||||
*
|
||||
* @return {Drupal.contextual.StateModel}
|
||||
*/
|
||||
focus: function () {
|
||||
this.set('hasFocus', true);
|
||||
var cid = this.cid;
|
||||
this.collection.each(function (model) {
|
||||
if (model.cid !== cid) {
|
||||
model.close().blur();
|
||||
}
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes focus from this contextual link, unless it is open.
|
||||
*
|
||||
* @return {Drupal.contextual.StateModel}
|
||||
*/
|
||||
blur: function () {
|
||||
if (!this.get('isOpen')) {
|
||||
this.set('hasFocus', false);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
121
core/modules/contextual/js/toolbar/models/StateModel.js
Normal file
121
core/modules/contextual/js/toolbar/models/StateModel.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone Model for the state of Contextual module's edit toolbar tab.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.contextualToolbar.StateModel = Backbone.Model.extend(/** @lends Drupal.contextualToolbar.StateModel# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {bool} isViewing
|
||||
* @prop {bool} isVisible
|
||||
* @prop {number} contextualCount
|
||||
* @prop {Drupal~TabbingContext} tabbingContext
|
||||
*/
|
||||
defaults: /** @lends Drupal.contextualToolbar.StateModel# */{
|
||||
|
||||
/**
|
||||
* Indicates whether the toggle is currently in "view" or "edit" mode.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
isViewing: true,
|
||||
|
||||
/**
|
||||
* Indicates whether the toggle should be visible or hidden. Automatically
|
||||
* calculated, depends on contextualCount.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
isVisible: false,
|
||||
|
||||
/**
|
||||
* Tracks how many contextual links exist on the page.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
contextualCount: 0,
|
||||
|
||||
/**
|
||||
* A TabbingContext object as returned by {@link Drupal~TabbingManager}:
|
||||
* the set of tabbable elements when edit mode is enabled.
|
||||
*
|
||||
* @type {?Drupal~TabbingContext}
|
||||
*/
|
||||
tabbingContext: null
|
||||
},
|
||||
|
||||
/**
|
||||
* Models the state of the edit mode toggle.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*
|
||||
* @param {object} attrs
|
||||
* @param {object} options
|
||||
* An object with the following option:
|
||||
* @param {Backbone.collection} options.contextualCollection
|
||||
* The collection of {@link Drupal.contextual.StateModel} models that
|
||||
* represent the contextual links on the page.
|
||||
*/
|
||||
initialize: function (attrs, options) {
|
||||
// Respond to new/removed contextual links.
|
||||
this.listenTo(options.contextualCollection, {
|
||||
'reset remove add': this.countContextualLinks,
|
||||
'add': this.lockNewContextualLinks
|
||||
});
|
||||
|
||||
this.listenTo(this, {
|
||||
// Automatically determine visibility.
|
||||
'change:contextualCount': this.updateVisibility,
|
||||
// Whenever edit mode is toggled, lock all contextual links.
|
||||
'change:isViewing': function (model, isViewing) {
|
||||
options.contextualCollection.each(function (contextualModel) {
|
||||
contextualModel.set('isLocked', !isViewing);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Tracks the number of contextual link models in the collection.
|
||||
*
|
||||
* @param {Drupal.contextual.StateModel} contextualModel
|
||||
* The contextual links model that was added or removed.
|
||||
* @param {Backbone.Collection} contextualCollection
|
||||
* The collection of contextual link models.
|
||||
*/
|
||||
countContextualLinks: function (contextualModel, contextualCollection) {
|
||||
this.set('contextualCount', contextualCollection.length);
|
||||
},
|
||||
|
||||
/**
|
||||
* Lock newly added contextual links if edit mode is enabled.
|
||||
*
|
||||
* @param {Drupal.contextual.StateModel} contextualModel
|
||||
* The contextual links model that was added.
|
||||
* @param {Backbone.Collection} [contextualCollection]
|
||||
* The collection of contextual link models.
|
||||
*/
|
||||
lockNewContextualLinks: function (contextualModel, contextualCollection) {
|
||||
if (!this.get('isViewing')) {
|
||||
contextualModel.set('isLocked', true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Automatically updates visibility of the view/edit mode toggle.
|
||||
*/
|
||||
updateVisibility: function () {
|
||||
this.set('isVisible', this.get('contextualCount') > 0);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
101
core/modules/contextual/js/toolbar/views/AuralView.js
Normal file
101
core/modules/contextual/js/toolbar/views/AuralView.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the aural view of the edit mode toggle.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, Backbone, _) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.contextualToolbar.AuralView = Backbone.View.extend(/** @lends Drupal.contextualToolbar.AuralView# */{
|
||||
|
||||
/**
|
||||
* Tracks whether the tabbing constraint announcement has been read once.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
announcedOnce: false,
|
||||
|
||||
/**
|
||||
* Renders the aural view of the edit mode toggle (screen reader support).
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
*/
|
||||
initialize: function (options) {
|
||||
this.options = options;
|
||||
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(this.model, 'change:isViewing', this.manageTabbing);
|
||||
|
||||
$(document).on('keyup', _.bind(this.onKeypress, this));
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.contextualToolbar.AuralView}
|
||||
*/
|
||||
render: function () {
|
||||
// Render the state.
|
||||
this.$el.find('button').attr('aria-pressed', !this.model.get('isViewing'));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Limits tabbing to the contextual links and edit mode toolbar tab.
|
||||
*/
|
||||
manageTabbing: function () {
|
||||
var tabbingContext = this.model.get('tabbingContext');
|
||||
// Always release an existing tabbing context.
|
||||
if (tabbingContext) {
|
||||
tabbingContext.release();
|
||||
Drupal.announce(this.options.strings.tabbingReleased);
|
||||
}
|
||||
// Create a new tabbing context when edit mode is enabled.
|
||||
if (!this.model.get('isViewing')) {
|
||||
tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual'));
|
||||
this.model.set('tabbingContext', tabbingContext);
|
||||
this.announceTabbingConstraint();
|
||||
this.announcedOnce = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Announces the current tabbing constraint.
|
||||
*/
|
||||
announceTabbingConstraint: function () {
|
||||
var strings = this.options.strings;
|
||||
Drupal.announce(Drupal.formatString(strings.tabbingConstrained, {
|
||||
'@contextualsCount': Drupal.formatPlural(Drupal.contextual.collection.length, '@count contextual link', '@count contextual links')
|
||||
}));
|
||||
Drupal.announce(strings.pressEsc);
|
||||
},
|
||||
|
||||
/**
|
||||
* Responds to esc and tab key press events.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
*/
|
||||
onKeypress: function (event) {
|
||||
// The first tab key press is tracked so that an annoucement about tabbing
|
||||
// constraints can be raised if edit mode is enabled when the page is
|
||||
// loaded.
|
||||
if (!this.announcedOnce && event.keyCode === 9 && !this.model.get('isViewing')) {
|
||||
this.announceTabbingConstraint();
|
||||
// Set announce to true so that this conditional block won't run again.
|
||||
this.announcedOnce = true;
|
||||
}
|
||||
// Respond to the ESC key. Exit out of edit mode.
|
||||
if (event.keyCode === 27) {
|
||||
this.model.set('isViewing', true);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, Drupal, Backbone, _);
|
80
core/modules/contextual/js/toolbar/views/VisualView.js
Normal file
80
core/modules/contextual/js/toolbar/views/VisualView.js
Normal file
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the visual view of the edit mode toggle.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.contextualToolbar.VisualView = Backbone.View.extend(/** @lends Drupal.contextualToolbar.VisualView# */{
|
||||
|
||||
/**
|
||||
* @return {object}
|
||||
*/
|
||||
events: function () {
|
||||
// Prevents delay and simulated mouse events.
|
||||
var touchEndToClick = function (event) {
|
||||
event.preventDefault();
|
||||
event.target.click();
|
||||
};
|
||||
|
||||
return {
|
||||
'click': function () {
|
||||
this.model.set('isViewing', !this.model.get('isViewing'));
|
||||
},
|
||||
'touchend': touchEndToClick
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the visual view of the edit mode toggle.
|
||||
*
|
||||
* Listens to mouse & touch and handles edit mode toggle interactions.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.listenTo(this.model, 'change:isViewing', this.persist);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.contextualToolbar.VisualView}
|
||||
*/
|
||||
render: function () {
|
||||
// Render the visibility.
|
||||
this.$el.toggleClass('hidden', !this.model.get('isVisible'));
|
||||
// Render the state.
|
||||
this.$el.find('button').toggleClass('is-active', !this.model.get('isViewing'));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Model change handler; persists the isViewing value to localStorage.
|
||||
*
|
||||
* `isViewing === true` is the default, so only stores in localStorage when
|
||||
* it's not the default value (i.e. false).
|
||||
*
|
||||
* @param {Drupal.contextualToolbar.StateModel} model
|
||||
* A {@link Drupal.contextualToolbar.StateModel} model.
|
||||
* @param {bool} isViewing
|
||||
* The value of the isViewing attribute in the model.
|
||||
*/
|
||||
persist: function (model, isViewing) {
|
||||
if (!isViewing) {
|
||||
localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false');
|
||||
}
|
||||
else {
|
||||
localStorage.removeItem('Drupal.contextualToolbar.isViewing');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
54
core/modules/contextual/js/views/AuralView.js
Normal file
54
core/modules/contextual/js/views/AuralView.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the aural view of a contextual link.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.contextual.AuralView = Backbone.View.extend(/** @lends Drupal.contextual.AuralView# */{
|
||||
|
||||
/**
|
||||
* Renders the aural view of a contextual link (i.e. screen reader support).
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
*/
|
||||
initialize: function (options) {
|
||||
this.options = options;
|
||||
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
|
||||
// Use aria-role form so that the number of items in the list is spoken.
|
||||
this.$el.attr('role', 'form');
|
||||
|
||||
// Initial render.
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
render: function () {
|
||||
var isOpen = this.model.get('isOpen');
|
||||
|
||||
// Set the hidden property of the links.
|
||||
this.$el.find('.contextual-links')
|
||||
.prop('hidden', !isOpen);
|
||||
|
||||
// Update the view of the trigger.
|
||||
this.$el.find('.trigger')
|
||||
.text(Drupal.t('@action @title configuration options', {
|
||||
'@action': (!isOpen) ? this.options.strings.open : this.options.strings.close,
|
||||
'@title': this.model.get('title')
|
||||
}))
|
||||
.attr('aria-pressed', isOpen);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
61
core/modules/contextual/js/views/KeyboardView.js
Normal file
61
core/modules/contextual/js/views/KeyboardView.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides keyboard interaction for a contextual link.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.contextual.KeyboardView = Backbone.View.extend(/** @lends Drupal.contextual.KeyboardView# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {
|
||||
'focus .trigger': 'focus',
|
||||
'focus .contextual-links a': 'focus',
|
||||
'blur .trigger': function () { this.model.blur(); },
|
||||
'blur .contextual-links a': function () {
|
||||
// Set up a timeout to allow a user to tab between the trigger and the
|
||||
// contextual links without the menu dismissing.
|
||||
var that = this;
|
||||
this.timer = window.setTimeout(function () {
|
||||
that.model.close().blur();
|
||||
}, 150);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Provides keyboard interaction for a contextual link.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
|
||||
/**
|
||||
* The timer is used to create a delay before dismissing the contextual
|
||||
* links on blur. This is only necessary when keyboard users tab into
|
||||
* contextual links without edit mode (i.e. without TabbingManager).
|
||||
* That means that if we decide to disable tabbing of contextual links
|
||||
* without edit mode, all this timer logic can go away.
|
||||
*
|
||||
* @type {NaN|number}
|
||||
*/
|
||||
this.timer = NaN;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets focus on the model; Clears the timer that dismisses the links.
|
||||
*/
|
||||
focus: function () {
|
||||
// Clear the timeout that might have been set by blurring a link.
|
||||
window.clearTimeout(this.timer);
|
||||
this.model.focus();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(Drupal, Backbone);
|
53
core/modules/contextual/js/views/RegionView.js
Normal file
53
core/modules/contextual/js/views/RegionView.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that renders the visual view of a contextual region element.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, Modernizr) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.contextual.RegionView = Backbone.View.extend(/** @lends Drupal.contextual.RegionView# */{
|
||||
|
||||
/**
|
||||
* @return {object}
|
||||
*/
|
||||
events: function () {
|
||||
var mapping = {
|
||||
mouseenter: function () { this.model.set('regionIsHovered', true); },
|
||||
mouseleave: function () {
|
||||
this.model.close().blur().set('regionIsHovered', false);
|
||||
}
|
||||
};
|
||||
// We don't want mouse hover events on touch.
|
||||
if (Modernizr.touch) {
|
||||
mapping = {};
|
||||
}
|
||||
return mapping;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the visual view of a contextual region element.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
this.listenTo(this.model, 'change:hasFocus', this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.contextual.RegionView}
|
||||
*/
|
||||
render: function () {
|
||||
this.$el.toggleClass('focus', this.model.get('hasFocus'));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, Modernizr);
|
76
core/modules/contextual/js/views/VisualView.js
Normal file
76
core/modules/contextual/js/views/VisualView.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the visual view of a contextual link.
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone, Modernizr) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.contextual.VisualView = Backbone.View.extend(/** @lends Drupal.contextual.VisualView# */{
|
||||
|
||||
/**
|
||||
* @return {object}
|
||||
*/
|
||||
events: function () {
|
||||
// Prevents delay and simulated mouse events.
|
||||
var touchEndToClick = function (event) {
|
||||
event.preventDefault();
|
||||
event.target.click();
|
||||
};
|
||||
var mapping = {
|
||||
'click .trigger': function () { this.model.toggleOpen(); },
|
||||
'touchend .trigger': touchEndToClick,
|
||||
'click .contextual-links a': function () { this.model.close().blur(); },
|
||||
'touchend .contextual-links a': touchEndToClick
|
||||
};
|
||||
// We only want mouse hover events on non-touch.
|
||||
if (!Modernizr.touch) {
|
||||
mapping.mouseenter = function () { this.model.focus(); };
|
||||
}
|
||||
return mapping;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the visual view of a contextual link. Listens to mouse & touch.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.contextual.VisualView}
|
||||
*/
|
||||
render: function () {
|
||||
var isOpen = this.model.get('isOpen');
|
||||
// The trigger should be visible when:
|
||||
// - the mouse hovered over the region,
|
||||
// - the trigger is locked,
|
||||
// - and for as long as the contextual menu is open.
|
||||
var isVisible = this.model.get('isLocked') || this.model.get('regionIsHovered') || isOpen;
|
||||
|
||||
this.$el
|
||||
// The open state determines if the links are visible.
|
||||
.toggleClass('open', isOpen)
|
||||
// Update the visibility of the trigger.
|
||||
.find('.trigger').toggleClass('visually-hidden', !isVisible);
|
||||
|
||||
// Nested contextual region handling: hide any nested contextual triggers.
|
||||
if ('isOpen' in this.model.changed) {
|
||||
this.$el.closest('.contextual-region')
|
||||
.find('.contextual .trigger:not(:first)')
|
||||
.toggle(!isOpen);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(Drupal, Backbone, Modernizr);
|
53
core/modules/contextual/src/ContextualController.php
Normal file
53
core/modules/contextual/src/ContextualController.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\ContextualController.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
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 implements ContainerAwareInterface {
|
||||
|
||||
use ContainerAwareTrait;
|
||||
|
||||
/**
|
||||
* 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] = $this->container->get('renderer')->renderRoot($element);
|
||||
}
|
||||
|
||||
return new JsonResponse($rendered);
|
||||
}
|
||||
|
||||
}
|
120
core/modules/contextual/src/Element/ContextualLinks.php
Normal file
120
core/modules/contextual/src/Element/ContextualLinks.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\Element\ContextualLinks.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual\Element;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Render\Element\RenderElement;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides a contextual_links element.
|
||||
*
|
||||
* @RenderElement("contextual_links")
|
||||
*/
|
||||
class ContextualLinks extends RenderElement {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo() {
|
||||
$class = get_class($this);
|
||||
return array(
|
||||
'#pre_render' => array(
|
||||
array($class, 'preRenderLinks'),
|
||||
),
|
||||
'#theme' => 'links__contextual',
|
||||
'#links' => array(),
|
||||
'#attributes' => array('class' => array('contextual-links')),
|
||||
'#attached' => array(
|
||||
'library' => array(
|
||||
'contextual/drupal.contextual-links',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-render callback: Builds a renderable array for contextual links.
|
||||
*
|
||||
* @param array $element
|
||||
* A renderable array containing a #contextual_links property, which is a
|
||||
* keyed array. Each key is the name of the group of contextual links to
|
||||
* render (based on the 'group' key in the *.links.contextual.yml files for
|
||||
* all enabled modules). The value contains an associative array containing
|
||||
* the following keys:
|
||||
* - route_parameters: The route parameters passed to the url generator.
|
||||
* - metadata: Any additional data needed in order to alter the link.
|
||||
* @code
|
||||
* array('#contextual_links' => array(
|
||||
* 'block' => array(
|
||||
* 'route_parameters' => array('block' => 'system.menu-tools'),
|
||||
* ),
|
||||
* 'menu' => array(
|
||||
* 'route_parameters' => array('menu' => 'tools'),
|
||||
* ),
|
||||
* ))
|
||||
* @endcode
|
||||
*
|
||||
* @return array
|
||||
* A renderable array representing contextual links.
|
||||
*/
|
||||
public static function preRenderLinks(array $element) {
|
||||
// Retrieve contextual menu links.
|
||||
$items = array();
|
||||
|
||||
$contextual_links_manager = static::contextualLinkManager();
|
||||
|
||||
foreach ($element['#contextual_links'] as $group => $args) {
|
||||
$args += array(
|
||||
'route_parameters' => array(),
|
||||
'metadata' => array(),
|
||||
);
|
||||
$items += $contextual_links_manager->getContextualLinksArrayByGroup($group, $args['route_parameters'], $args['metadata']);
|
||||
}
|
||||
|
||||
// Transform contextual links into parameters suitable for links.html.twig.
|
||||
$links = array();
|
||||
foreach ($items as $class => $item) {
|
||||
$class = Html::getClass($class);
|
||||
$links[$class] = array(
|
||||
'title' => $item['title'],
|
||||
'url' => Url::fromRoute(isset($item['route_name']) ? $item['route_name'] : '', isset($item['route_parameters']) ? $item['route_parameters'] : []),
|
||||
);
|
||||
}
|
||||
$element['#links'] = $links;
|
||||
|
||||
// Allow modules to alter the renderable contextual links element.
|
||||
static::moduleHandler()->alter('contextual_links_view', $element, $items);
|
||||
|
||||
// If there are no links, tell drupal_render() to abort rendering.
|
||||
if (empty($element['#links'])) {
|
||||
$element['#printed'] = TRUE;
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the contextual link manager.
|
||||
*
|
||||
* @return \Drupal\Core\Menu\ContextualLinkManager
|
||||
*/
|
||||
protected static function contextualLinkManager() {
|
||||
return \Drupal::service('plugin.manager.menu.contextual_link');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the module handler.
|
||||
*
|
||||
* @return \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected static function moduleHandler() {
|
||||
return \Drupal::moduleHandler();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\Element\ContextualLinksPlaceholder.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual\Element;
|
||||
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Core\Render\Element\RenderElement;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Provides a contextual_links_placeholder element.
|
||||
*
|
||||
* @RenderElement("contextual_links_placeholder")
|
||||
*/
|
||||
class ContextualLinksPlaceholder extends RenderElement {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInfo() {
|
||||
$class = get_class($this);
|
||||
return array(
|
||||
'#pre_render' => array(
|
||||
array($class, 'preRenderPlaceholder'),
|
||||
),
|
||||
'#id' => NULL,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 array $element
|
||||
* A structured array with #id containing a "contextual id".
|
||||
*
|
||||
* @return array
|
||||
* The passed-in element with a contextual link placeholder in '#markup'.
|
||||
*
|
||||
* @see _contextual_links_to_id()
|
||||
*/
|
||||
public static function preRenderPlaceholder(array $element) {
|
||||
$element['#markup'] = SafeMarkup::format('<div@attributes></div>', ['@attributes' => new Attribute(['data-contextual-id' => $element['#id']])]);
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\Plugin\views\field\ContextualLinks.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual\Plugin\views\field;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Routing\RedirectDestinationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\views\Plugin\views\field\FieldPluginBase;
|
||||
use Drupal\views\ResultRow;
|
||||
|
||||
/**
|
||||
* Provides a handler that adds contextual links.
|
||||
*
|
||||
* @ingroup views_field_handlers
|
||||
*
|
||||
* @ViewsField("contextual_links")
|
||||
*/
|
||||
class ContextualLinks extends FieldPluginBase {
|
||||
|
||||
use RedirectDestinationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function usesGroupBy() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function defineOptions() {
|
||||
$options = parent::defineOptions();
|
||||
|
||||
$options['fields'] = array('default' => array());
|
||||
$options['destination'] = array('default' => 1);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
|
||||
$all_fields = $this->view->display_handler->getFieldLabels();
|
||||
// Offer to include only those fields that follow this one.
|
||||
$field_options = array_slice($all_fields, 0, array_search($this->options['id'], array_keys($all_fields)));
|
||||
$form['fields'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Fields'),
|
||||
'#description' => $this->t('Fields to be included as contextual links.'),
|
||||
'#options' => $field_options,
|
||||
'#default_value' => $this->options['fields'],
|
||||
);
|
||||
$form['destination'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Include destination'),
|
||||
'#description' => $this->t('Include a "destination" parameter in the link to return the user to the original view upon completing the contextual action.'),
|
||||
'#options' => array(
|
||||
'0' => $this->t('No'),
|
||||
'1' => $this->t('Yes'),
|
||||
),
|
||||
'#default_value' => $this->options['destination'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function preRender(&$values) {
|
||||
// Add a row plugin css class for the contextual link.
|
||||
$class = 'contextual-region';
|
||||
if (!empty($this->view->style_plugin->options['row_class'])) {
|
||||
$this->view->style_plugin->options['row_class'] .= " $class";
|
||||
}
|
||||
else {
|
||||
$this->view->style_plugin->options['row_class'] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\views\Plugin\views\field\FieldPluginBase::render().
|
||||
*
|
||||
* Renders the contextual fields.
|
||||
*
|
||||
* @param \Drupal\views\ResultRow $values
|
||||
* The values retrieved from a single row of a view's query result.
|
||||
*
|
||||
* @see contextual_preprocess()
|
||||
* @see contextual_contextual_links_view_alter()
|
||||
*/
|
||||
public function render(ResultRow $values) {
|
||||
$links = array();
|
||||
foreach ($this->options['fields'] as $field) {
|
||||
$rendered_field = $this->view->style_plugin->getField($values->index, $field);
|
||||
if (empty($rendered_field)) {
|
||||
continue;
|
||||
}
|
||||
$title = $this->view->field[$field]->last_render_text;
|
||||
$path = '';
|
||||
if (!empty($this->view->field[$field]->options['alter']['path'])) {
|
||||
$path = $this->view->field[$field]->options['alter']['path'];
|
||||
}
|
||||
elseif (!empty($this->view->field[$field]->options['alter']['url']) && $this->view->field[$field]->options['alter']['url'] instanceof Url) {
|
||||
$path = $this->view->field[$field]->options['alter']['url']->toString();
|
||||
}
|
||||
if (!empty($title) && !empty($path)) {
|
||||
// Make sure that tokens are replaced for this paths as well.
|
||||
$tokens = $this->getRenderTokens(array());
|
||||
$path = strip_tags(Html::decodeEntities(strtr($path, $tokens)));
|
||||
|
||||
$links[$field] = array(
|
||||
'href' => $path,
|
||||
'title' => $title,
|
||||
);
|
||||
if (!empty($this->options['destination'])) {
|
||||
$links[$field]['query'] = $this->getDestinationArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Renders a contextual links placeholder.
|
||||
if (!empty($links)) {
|
||||
$contextual_links = array(
|
||||
'contextual' => array(
|
||||
'',
|
||||
array(),
|
||||
array(
|
||||
'contextual-views-field-links' => UrlHelper::encodePath(Json::encode($links)),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$element = array(
|
||||
'#type' => 'contextual_links_placeholder',
|
||||
'#id' => _contextual_links_to_id($contextual_links),
|
||||
);
|
||||
return drupal_render($element);
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() { }
|
||||
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\Tests\ContextualDynamicContextTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
|
||||
/**
|
||||
* Tests if contextual links are showing on the front page depending on
|
||||
* permissions.
|
||||
*
|
||||
* @group contextual
|
||||
*/
|
||||
class ContextualDynamicContextTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* A user with permission to access contextual links and edit content.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $editorUser;
|
||||
|
||||
/**
|
||||
* An authenticated user with permission to access contextual links.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $authenticatedUser;
|
||||
|
||||
/**
|
||||
* A simulated anonymous user with access only to node content.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $anonymousUser;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('contextual', 'node', 'views', 'views_ui', 'language');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
|
||||
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('it')->save();
|
||||
$this->rebuildContainer();
|
||||
|
||||
$this->editorUser = $this->drupalCreateUser(array('access content', 'access contextual links', 'edit any article content'));
|
||||
$this->authenticatedUser = $this->drupalCreateUser(array('access content', 'access contextual links'));
|
||||
$this->anonymousUser = $this->drupalCreateUser(array('access content'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 testDifferentPermissions() {
|
||||
$this->drupalLogin($this->editorUser);
|
||||
|
||||
// Create three nodes in the following order:
|
||||
// - An article, which should be user-editable.
|
||||
// - A page, which should not be user-editable.
|
||||
// - A second article, which should also be user-editable.
|
||||
$node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
|
||||
$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 links
|
||||
// placeholders, as should the view that contains them.
|
||||
$ids = [
|
||||
'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en',
|
||||
'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en',
|
||||
'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en',
|
||||
'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en',
|
||||
];
|
||||
|
||||
// Editor user: can access contextual links and can edit articles.
|
||||
$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 = Json::decode($response);
|
||||
$this->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="entitynodeedit-form"><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="entitynodeedit-form"><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.
|
||||
$this->drupalLogin($this->authenticatedUser);
|
||||
$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 = 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->anonymousUser);
|
||||
$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);
|
||||
|
||||
// Verify that link language is properly handled.
|
||||
$node3->addTranslation('it')->save();
|
||||
$id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it';
|
||||
$this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]);
|
||||
$this->assertContextualLinkPlaceHolder($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a contextual link placeholder with the given id exists.
|
||||
*
|
||||
* @param string $id
|
||||
* A contextual link id.
|
||||
*
|
||||
* @return bool
|
||||
* The result of the assertion.
|
||||
*/
|
||||
protected function assertContextualLinkPlaceHolder($id) {
|
||||
return $this->assertRaw('<div' . new Attribute(array('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
|
||||
* The result of the assertion.
|
||||
*/
|
||||
protected function assertNoContextualLinkPlaceHolder($id) {
|
||||
return $this->assertNoRaw('<div' . new Attribute(array('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) {
|
||||
$post = array();
|
||||
for ($i = 0; $i < count($ids); $i++) {
|
||||
$post['ids[' . $i . ']'] = $ids[$i];
|
||||
}
|
||||
return $this->drupalPostWithFormat('contextual/render', 'json', $post, array('query' => array('destination' => $current_path)));
|
||||
}
|
||||
}
|
135
core/modules/contextual/src/Tests/ContextualUnitTest.php
Normal file
135
core/modules/contextual/src/Tests/ContextualUnitTest.php
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\contextual\Tests\ContextualUnitTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\contextual\Tests;
|
||||
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests all edge cases of converting from #contextual_links to ids and vice
|
||||
* versa.
|
||||
*
|
||||
* @group contextual
|
||||
*/
|
||||
class ContextualUnitTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('contextual');
|
||||
|
||||
/**
|
||||
* Provides testcases for testContextualLinksToId() and
|
||||
*/
|
||||
function _contextual_links_id_testcases() {
|
||||
// Test branch conditions:
|
||||
// - one group.
|
||||
// - one dynamic path argument.
|
||||
// - no metadata.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'node' => array(
|
||||
'route_parameters' => array(
|
||||
'node' => '14031991',
|
||||
),
|
||||
'metadata' => array('langcode' => 'en'),
|
||||
),
|
||||
),
|
||||
'id' => 'node:node=14031991:langcode=en',
|
||||
);
|
||||
|
||||
// Test branch conditions:
|
||||
// - one group.
|
||||
// - multiple dynamic path arguments.
|
||||
// - no metadata.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'foo' => array(
|
||||
'route_parameters'=> array(
|
||||
'bar',
|
||||
'key' => 'baz',
|
||||
'qux',
|
||||
),
|
||||
'metadata' => array('langcode' => 'en'),
|
||||
),
|
||||
),
|
||||
'id' => 'foo:0=bar&key=baz&1=qux:langcode=en',
|
||||
);
|
||||
|
||||
// Test branch conditions:
|
||||
// - one group.
|
||||
// - one dynamic path argument.
|
||||
// - metadata.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'views_ui_edit' => array(
|
||||
'route_parameters' => array(
|
||||
'view' => 'frontpage'
|
||||
),
|
||||
'metadata' => array(
|
||||
'location' => 'page',
|
||||
'display' => 'page_1',
|
||||
'langcode' => 'en',
|
||||
),
|
||||
),
|
||||
),
|
||||
'id' => 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en',
|
||||
);
|
||||
|
||||
// Test branch conditions:
|
||||
// - multiple groups.
|
||||
// - multiple dynamic path arguments.
|
||||
$tests[] = array(
|
||||
'links' => array(
|
||||
'node' => array(
|
||||
'route_parameters' => array(
|
||||
'node' => '14031991',
|
||||
),
|
||||
'metadata' => array('langcode' => 'en'),
|
||||
),
|
||||
'foo' => array(
|
||||
'route_parameters' => array(
|
||||
'bar',
|
||||
'key' => 'baz',
|
||||
'qux',
|
||||
),
|
||||
'metadata' => array('langcode' => 'en'),
|
||||
),
|
||||
'edge' => array(
|
||||
'route_parameters' => array('20011988'),
|
||||
'metadata' => array('langcode' => 'en'),
|
||||
),
|
||||
),
|
||||
'id' => 'node:node=14031991:langcode=en|foo:0=bar&key=baz&1=qux:langcode=en|edge:0=20011988:langcode=en',
|
||||
);
|
||||
|
||||
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']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue