Issue #1874664 by Wim Leers, jessebeach, tkoleary, Gábor Hojtsy, quicksketch, Bojhan: Reconcile 'Edit' toolbar option with local tasks (tabs) and contextual links for editing.

8.0.x
Dries 2013-02-12 16:46:04 -05:00
parent 878a4162d9
commit 6d5c211392
33 changed files with 551 additions and 624 deletions

BIN
core/misc/edit-active.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

BIN
core/misc/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

View File

@ -7,6 +7,8 @@
"use strict";
var contextuals = [];
/**
* Attaches outline behavior for regions associated with contextual links.
*/
@ -14,7 +16,14 @@ Drupal.behaviors.contextual = {
attach: function (context) {
$('ul.contextual-links', context).once('contextual', function () {
var $this = $(this);
$this.data('drupal-contextual', new Drupal.contextual($this, $this.closest('.contextual-region')));
var contextual = new Drupal.contextual($this, $this.closest('.contextual-region'));
contextuals.push(contextual);
$this.data('drupal-contextual', contextual);
});
// Bind to edit mode changes.
$('body').once('contextual', function () {
$(document).on('drupalEditModeChanged.contextual', toggleEditMode);
});
}
};
@ -54,16 +63,33 @@ Drupal.contextual.prototype.init = function() {
.attr('aria-pressed', false)
.prependTo(this.$wrapper);
// The trigger behaviors are never detached or mutated.
this.$region
.on('click.contextual', '.contextual .trigger', $.proxy(this.triggerClickHandler, this))
.on('mouseleave.contextual', '.contextual', {show: false}, $.proxy(this.triggerLeaveHandler, this));
// Attach highlight behaviors.
this.attachHighlightBehaviors();
};
/**
* Attaches highlight-on-mouseenter behaviors.
*/
Drupal.contextual.prototype.attachHighlightBehaviors = function () {
// Bind behaviors through delegation.
var highlightRegion = $.proxy(this.highlightRegion, this);
this.$region
.on('click.contextual', '.contextual .trigger', $.proxy(this.triggerClickHandler, this))
.on('mouseenter.contextual', {highlight: true}, highlightRegion)
.on('mouseleave.contextual', {highlight: false}, highlightRegion)
.on('mouseleave.contextual', '.contextual', {show: false}, $.proxy(this.triggerLeaveHandler, this))
.on('click.contextual', '.contextual-links a', {highlight: false}, highlightRegion)
.on('focus.contextual', '.contextual-links a, .contextual .trigger', {highlight: true}, highlightRegion)
.on('blur.contextual', '.contextual-links a, .contextual .trigger', {highlight: false}, highlightRegion);
.on('mouseenter.contextual.highlight', {highlight: true}, highlightRegion)
.on('mouseleave.contextual.highlight', {highlight: false}, highlightRegion)
.on('click.contextual.highlight', '.contextual-links a', {highlight: false}, highlightRegion)
.on('focus.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: true}, highlightRegion)
.on('blur.contextual.highlight', '.contextual-links a, .contextual .trigger', {highlight: false}, highlightRegion);
};
/**
* Detaches unhighlight-on-mouseleave behaviors.
*/
Drupal.contextual.prototype.detachHighlightBehaviors = function () {
this.$region.off('.contextual.highlight');
};
/**
@ -139,6 +165,16 @@ Drupal.contextual.prototype.showLinks = function(show) {
};
/**
* Shows or hides all pencil icons and corresponding contextual regions.
*/
function toggleEditMode (event, data) {
for (var i = contextuals.length - 1; i >= 0; i--) {
contextuals[i][(data.status) ? 'detachHighlightBehaviors' : 'attachHighlightBehaviors']();
contextuals[i].$region.toggleClass('contextual-region-active', data.status);
}
}
/**
* Wraps contextual links.
*

View File

@ -5,6 +5,41 @@
* Adds contextual links to perform actions related to elements on a page.
*/
/**
* Implements hook_toolbar().
*/
function contextual_toolbar() {
if (!user_access('access contextual links')) {
return;
}
$tab['contextual'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => t('Edit'),
'#attributes' => array(
'class' => array('icon', 'icon-edit'),
'role' => 'button',
'aria-pressed' => 'false',
),
// @todo remove this once http://drupal.org/node/1908906 lands.
'#options' => array('attributes' => array()),
),
'#wrapper_attributes' => array(
'class' => array('element-hidden', 'contextual-toolbar-tab'),
),
'#attached' => array(
'library' => array(
array('contextual', 'drupal.contextual-toolbar'),
),
),
);
return $tab;
}
/**
* Implements hook_help().
*/
@ -45,7 +80,9 @@ function contextual_library_info() {
'website' => 'http://drupal.org/node/473268',
'version' => VERSION,
'js' => array(
$path . '/contextual.js' => array(),
// Add the JavaScript, with a group and weight such that it will run
// before modules/contextual/contextual.toolbar.js.
$path . '/contextual.js' => array('group' => JS_LIBRARY, 'weight' => -2),
),
'css' => array(
$path . '/contextual.base.css' => array(),
@ -57,6 +94,23 @@ function contextual_library_info() {
array('system', 'jquery.once'),
),
);
$libraries['drupal.contextual-toolbar'] = array(
'title' => 'Contextual Links Toolbar Tab',
'version' => VERSION,
'js' => array(
// Add the JavaScript, with a group and weight such that it will run
// before modules/overlay/overlay-parent.js.
$path . '/contextual.toolbar.js' => array('group' => JS_LIBRARY, 'weight' => -1),
),
'css' => array(
$path . '/contextual.toolbar.css' => array(),
),
'dependencies' => array(
array('system', 'jquery'),
array('system', 'jquery.once'),
array('system', 'backbone'),
),
);
return $libraries;
}

View File

@ -16,6 +16,8 @@
*/
.contextual .trigger {
float: left;
right: 0;
left: 2px;
}
/**

View File

@ -10,39 +10,39 @@
position: absolute;
right: 0; /* LTR */
top: 2px;
z-index: 999;
}
.contextual-region-active {
outline: 1px dashed #d6d6d6;
outline-offset: 1px;
z-index: 500;
}
/**
* Contextual trigger.
*/
.contextual .trigger {
background: transparent url("images/gear-select.png") no-repeat 2px 0;
border: 1px solid transparent;
border-radius: 4px 4px 0 0;
background-attachment: scroll;
background-color: #fff;
background-image: url("../../misc/edit.png");
background-position: center center;
background-repeat: no-repeat;
background-size: 16px 16px;
border: 1px solid #ddd;
border-radius: 13px;
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
/* Override the .element-focusable height: auto */
height: 18px !important;
height: 28px !important;
float: right; /* LTR */
margin: 0;
overflow: hidden;
padding: 0 2px;
position: relative;
width: 34px;
right: 2px; /* LTR */
width: 28px;
text-indent: -9999px;
z-index: 2;
}
.no-touch .contextual .trigger:hover,
.contextual-links-active .trigger {
background-position: 2px -18px;
cursor: pointer;
}
.contextual-links-active .trigger {
background-color: #fff;
border-bottom: none;
border-color: #d6d6d6;
border-bottom-color: transparent;
border-radius: 13px 13px 0 0;
box-shadow: none;
}
/**
@ -52,7 +52,7 @@
*/
.contextual-region .contextual .contextual-links {
background-color: #fff;
border: 1px solid #d6d6d6;
border: 1px solid #ddd;
border-radius: 4px 0 4px 4px; /* LTR */
clear: both;
float: right; /* LTR */
@ -90,5 +90,7 @@
text-decoration: none;
}
.no-touch .contextual-region .contextual .contextual-links li a:hover {
background-color: #bfdcee;
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%);
}

View File

@ -0,0 +1,11 @@
/**
* @file
* RTL styling for contextual module's toolbar tab.
*/
.js .toolbar .bar .contextual-toolbar-tab.tab {
float: left;
}
.js .toolbar .bar .contextual-toolbar-tab button {
padding-right: 1.3333em;
}

View File

@ -0,0 +1,36 @@
/**
* @file
* Styling for contextual module's toolbar tab.
*/
/* Tab icon. */
.icon-edit:before {
background-image: url("../../misc/edit.png");
}
.icon-edit:active:before,
.active.icon-edit:before {
background-image: url("../../misc/edit-active.png");
}
/* Tab appearance. */
.js .toolbar .bar .contextual-toolbar-tab.tab {
float: right; /* LTR */
}
.js .toolbar .bar .contextual-toolbar-tab button {
padding-bottom: 1em;
padding-top: 1em;
/* Hide tab text. */
padding-left: 1.3333em; /* LTR */
text-indent: -9999px;
}
.js .toolbar .bar .contextual-toolbar-tab button.active {
background-image:-moz-linear-gradient(rgb(78,159,234) 0%,rgb(69,132,221) 100%);
background-image:-webkit-gradient(linear,color-stop(0, rgb(78,159,234)),color-stop(1, rgb(69,132,221)));
background-image: -webkit-linear-gradient(top, 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 */
.js .toolbar .bar .contextual-toolbar-tab.tab.element-hidden {
display: none;
}

View File

@ -0,0 +1,132 @@
/**
* @file
* Attaches behaviors for the Contextual module's edit toolbar tab.
*/
(function ($, Backbone, Drupal, document, localStorage) {
"use strict";
/**
* Attaches contextual's edit toolbar tab behavior.
*
* Events
* Contextual triggers an event that can be used by other scripts.
* - drupalEditModeChanged: Triggered when the edit mode changes.
*/
Drupal.behaviors.contextualToolbar = {
attach: function (context) {
$('body').once('contextualToolbar-init', function () {
var $contextuals = $(context).find('.contextual-links');
var $tab = $('.js .toolbar .bar .contextual-toolbar-tab');
var model = new Drupal.contextualToolbar.models.EditToggleModel({
isViewing: true
});
var view = new Drupal.contextualToolbar.views.EditToggleView({
el: $tab,
model: model
});
// Update the model based on overlay events.
$(document)
.on('drupalOverlayOpen.contextualToolbar', function () {
model.set('isVisible', false);
})
.on('drupalOverlayClose.contextualToolbar', function () {
model.set('isVisible', true);
});
// Update the model to show the edit tab if there's >=1 contextual link.
if ($contextuals.length > 0) {
model.set('isVisible', true);
}
// Allow other scripts to respond to edit mode changes.
model.on('change:isViewing', function (model, value) {
$(document).trigger('drupalEditModeChanged', { status: !value });
});
// Checks whether localStorage indicates we should start in edit mode
// rather than view mode.
// @see Drupal.contextualToolbar.views.EditToggleView.persist()
if (localStorage.getItem('Drupal.contextualToolbar.isViewing') !== null) {
model.set('isViewing', false);
}
});
}
};
Drupal.contextualToolbar = Drupal.contextualToolbar || { models: {}, views: {}};
/**
* Backbone Model for the edit toggle.
*/
Drupal.contextualToolbar.models.EditToggleModel = Backbone.Model.extend({
defaults: {
// Indicates whether the toggle is currently in "view" or "edit" mode.
isViewing: true,
// Indicates whether the toggle should be visible or hidden.
isVisible: false
}
});
/**
* Handles edit mode toggle interactions.
*/
Drupal.contextualToolbar.views.EditToggleView = Backbone.View.extend({
events: { 'click': 'onClick' },
/**
* Implements Backbone Views' initialize().
*/
initialize: function () {
this.model.on('change', this.render, this);
this.model.on('change:isViewing', this.persist, this);
},
/**
* Implements Backbone Views' render().
*/
render: function () {
var args = arguments;
// Render the visibility.
this.$el.toggleClass('element-hidden', !this.model.get('isVisible'));
// Render the state.
var isViewing = this.model.get('isViewing');
this.$el.find('button')
.toggleClass('active', !isViewing)
.attr('aria-pressed', !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.models.EditToggleModel model
* An EditToggleModel Backbone 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');
}
},
onClick: function (event) {
this.model.set('isViewing', !this.model.get('isViewing'));
event.preventDefault();
event.stopPropagation();
}
});
})(jQuery, Backbone, Drupal, document, localStorage);

View File

@ -71,47 +71,14 @@
/**
* Toolbar.
*/
.icon-edit:before {
background-image: url("../images/icon-edit.png");
}
.icon-edit:active:before,
.active .icon-edit:before {
background-image: url("../images/icon-edit-active.png");
}
.js .toolbar .bar .edit-toolbar-tab.tab {
float: right;
}
.toolbar .icon-edit.edit-nothing-editable-hidden {
display: none;
}
/* In-place editing doesn't work in the overlay, so always hide the tab. */
.overlay-open .toolbar .icon-edit {
display: none;
}
/**
* Edit mode: overlay + candidate editables + editables being edited.
* Candidate editables + editables being edited.
*
* Note: every class is prefixed with "edit-" to prevent collisions with modules
* or themes. In IPE-specific DOM subtrees, this is not necessary.
*/
#edit_overlay {
position: fixed;
z-index: 250;
width: 100%;
height: 100%;
background-color: #fff;
background-color: rgba(255,255,255,.5);
top: 0;
left: 0;
}
/* Editable. */
.edit-editable {
z-index: 300;
@ -127,6 +94,7 @@
/* Highlighted (hovered) editable. */
.edit-editable.edit-highlighted {
z-index: 305;
min-width: 200px;
}
.edit-field.edit-editable.edit-highlighted,
@ -184,16 +152,6 @@
background: #f5f5f5;
}
/* Modal active: prevent user from interacting with toolbar & editables. */
.edit-form-container.edit-belowoverlay,
.edit-toolbar-container.edit-belowoverlay,
.edit-validation-errors.edit-belowoverlay {
z-index: 210;
}
.edit-editable.edit-belowoverlay {
z-index: 200;
}
@ -279,6 +237,10 @@
bottom: 1px;
box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
background: #fff;
display: none;
}
.edit-highlighted .edit-toolbar-heightfaker {
display: block;
}
/* The toolbar; these are not necessarily visible. */

View File

@ -3,4 +3,5 @@ description = In-place content editing.
package = Core
core = 8.x
version = VERSION
dependencies[] = contextual
dependencies[] = field

View File

@ -39,42 +39,23 @@ function edit_permission() {
}
/**
* Implements hook_toolbar().
* Implements hook_contextual_links_view_alter().
*
* In-place editing builds upon contextual.module, but doesn't actually add its
* "Quick edit" contextual link in PHP (i.e. here) because:
* - that would require to add a local task menu item in the menu system, which
* doesn't make any sense, since there is no corresponding page;
* - it should only work when JavaScript is enabled, because only then in-place
* editing is possible.
*/
function edit_toolbar() {
function edit_contextual_links_view_alter(&$element, $items) {
if (!user_access('access in-place editing')) {
return;
}
$tab['edit'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#title' => t('Edit'),
'#href' => '',
'#options' => array(
'html' => FALSE,
'attributes' => array(
'id' => 'toolbar-tab-edit',
'class' => array('icon', 'icon-edit', 'edit-nothing-editable-hidden'),
),
),
),
'#wrapper_attributes' => array(
'class' => array('edit-toolbar-tab'),
),
'#attached' => array(
'library' => array(
array('edit', 'edit'),
),
),
);
// Include the attachments and settings for all available editors.
$attachments = drupal_container()->get('edit.editor.selector')->getAllEditorAttachments();
$tab['edit']['#attached'] = NestedArray::mergeDeep($tab['edit']['#attached'], $attachments);
return $tab;
$element['#attached'] = NestedArray::mergeDeep($element['#attached'], $attachments);
}
/**
@ -94,15 +75,12 @@ function edit_library_info() {
// Core.
$path . '/js/edit.js' => $options,
$path . '/js/app.js' => $options,
// Routers.
$path . '/js/routers/edit-router.js' => $options,
// Models.
$path . '/js/models/edit-app-model.js' => $options,
// Views.
$path . '/js/views/propertyeditordecoration-view.js' => $options,
$path . '/js/views/menu-view.js' => $options,
$path . '/js/views/contextuallink-view.js' => $options,
$path . '/js/views/modal-view.js' => $options,
$path . '/js/views/overlay-view.js' => $options,
$path . '/js/views/toolbar-view.js' => $options,
// Backbone.sync implementation on top of Drupal forms.
$path . '/js/backbone.drupalform.js' => $options,
@ -173,6 +151,16 @@ function edit_preprocess_field(&$variables) {
$variables['attributes']['data-edit-id'] = $entity->entityType() . '/' . $entity->id() . '/' . $element['#field_name'] . '/' . $element['#language'] . '/' . $element['#view_mode'];
}
/**
* Implements hook_preprocess_HOOK() for node.tpl.php.
*
* @todo Move towards hook_preprocess_entity() once that's available.
*/
function edit_preprocess_node(&$variables) {
$node = $variables['elements']['#node'];
$variables['attributes']['data-edit-entity'] = 'node/' . $node->nid;
}
/**
* Form constructor for the field editing form.
*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

View File

@ -47,20 +47,8 @@
editableNs: 'createeditable'
});
// Instantiate OverlayView.
var overlayView = new Drupal.edit.views.OverlayView({
el: (Drupal.theme('editOverlay', {})),
model: this.model
});
// Instantiate MenuView.
var editMenuView = new Drupal.edit.views.MenuView({
el: this.el,
model: this.model
});
// When view/edit mode is toggled in the menu, update the editor widgets.
this.model.on('change:isViewing', this.appStateChange);
this.model.on('change:activeEntity', this.appStateChange);
},
/**
@ -74,7 +62,7 @@
*/
findEditableProperties: function($context) {
var that = this;
var newState = (this.model.get('isViewing')) ? 'inactive' : 'candidate';
var activeEntity = this.model.get('activeEntity');
this.domService.findSubjectElements($context).each(function() {
var $element = $(this);
@ -103,10 +91,17 @@
.on('destroyedPropertyEditor.edit', function(event, editor) {
that.undecorateEditor(editor);
that.$entityElements = that.$entityElements.not($(this));
})
// Transition the new PropertyEditor into the current state.
.createEditable('setState', newState);
// Transition the new PropertyEditor into the default state.
.createEditable('setState', 'inactive');
// If the new PropertyEditor is for the entity that's currently being
// edited, then transition it to the 'candidate' state.
// (This happens when a field was modified and is re-rendered.)
var entityOfProperty = $element.createEditable('option', 'model');
if (entityOfProperty.getSubjectUri() === activeEntity) {
$element.createEditable('setState', 'candidate');
}
// Add this new EditableEntity widget element to the list.
that.$entityElements = that.$entityElements.add($element);
@ -116,26 +111,29 @@
/**
* Sets the state of PropertyEditor widgets when edit mode begins or ends.
*
* Should be called whenever EditAppModel's "isViewing" changes.
* Should be called whenever EditAppModel's "activeEntity" changes.
*/
appStateChange: function() {
// @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133, https://github.com/bergie/create/issues/140)
// We're currently setting the state on EditableEntity widgets instead of
// PropertyEditor widgets, because of
// https://github.com/bergie/create/issues/133.
var newState = (this.model.get('isViewing')) ? 'inactive' : 'candidate';
var activeEntity = this.model.get('activeEntity');
var $editableFieldsForEntity = $('[data-edit-id^="' + activeEntity + '/"]');
// First, change the status of all PropertyEditor widgets to 'inactive'.
this.$entityElements.each(function() {
$(this).createEditable('setState', newState);
$(this).createEditable('setState', 'inactive', null, {reason: 'stop'});
});
// Then, change the status of PropertyEditor widgets of the currently
// active entity to 'candidate'.
$editableFieldsForEntity.each(function() {
$(this).createEditable('setState', 'candidate');
});
// Manage the page's tab indexes.
if (newState === 'candidate') {
this._manageDocumentFocus();
Drupal.edit.setMessage(Drupal.t('In place edit mode is active'), Drupal.t('Page navigation is limited to editable items.'), Drupal.t('Press escape to exit'));
}
else if (newState === 'inactive') {
this._releaseDocumentFocusManagement();
Drupal.edit.setMessage(Drupal.t('Edit mode is inactive.'), Drupal.t('Resume normal page navigation'));
}
},
/**
@ -159,9 +157,9 @@
// If the app is in view mode, then reject all state changes except for
// those to 'inactive'.
if (this.model.get('isViewing')) {
if (to !== 'inactive') {
accept = false;
if (context && context.reason === 'stop') {
if (from === 'candidate' && to === 'inactive') {
accept = true;
}
}
// Handling of edit mode state changes is more granular.
@ -314,7 +312,6 @@
// Keep track of the active editor in the global state.
if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== editor) {
this.model.set('activeEditor', editor);
Drupal.edit.setMessage(Drupal.t('An editor is active'));
}
else if (this.model.get('activeEditor') === editor && to === 'candidate') {
// Discarded if it transitions from a changed state to 'candidate'.
@ -387,142 +384,8 @@
// Don't call .remove() on the decoration view, because that would remove
// a potentially rerendered field.
delete editor.decorationView;
},
/**
* Makes elements other than the editables unreachable via the tab key.
*
* @todo refactoring.
*
* This method is currently overloaded, handling elements of state modeling
* and application control. The state of the application is spread between
* this view, its model and aspects of the UI widgets in Create.js. In order
* to drive focus management from the application state (and have it
* influence that state of the application), we need to distall state out
* of Create.js components.
*
* This method introduces behaviors that support accessibility of the edit
* application. Although not yet integrated into the application properly,
* it does provide us with the opportunity to collect feedback from
* users who will interact with edit primarily through keyboard input. We
* want this feedback sooner than we can have a refactored application.
*/
_manageDocumentFocus: function () {
var editablesSelector = '.edit-candidate.edit-editable';
var inputsSelector = 'a:visible, button:visible, input:visible, textarea:visible, select:visible';
var $editables = $(editablesSelector)
.attr({
'tabindex': 0,
'role': 'button'
});
// Instantiate a variable to hold the editable element in the set.
var $currentEditable;
// We're using simple function scope to manage 'this' for the internal
// handler, so save this as that.
var that = this;
// Turn on focus management.
$(document).on('keydown.edit', function (event) {
var activeEditor, editableEntity, predicate;
// Handle esc key press. Close any active editors.
if (event.keyCode === 27) {
event.preventDefault();
activeEditor = that.model.get('activeEditor');
if (activeEditor) {
editableEntity = activeEditor.options.widget;
predicate = activeEditor.options.property;
editableEntity.setState('candidate', predicate, { reason: 'overlay' });
}
else {
$(editablesSelector).trigger('tabOut.edit');
// This should move into the state management for the app model.
location.hash = "#view";
that.model.set('isViewing', true);
}
return;
}
// Handle enter or space key presses.
if (event.keyCode === 13 || event.keyCode === 32) {
if ($currentEditable && $currentEditable.is(editablesSelector)) {
$currentEditable.trigger('click');
// Squelch additional handlers.
event.preventDefault();
return;
}
}
// Handle tab key presses.
if (event.keyCode === 9) {
var context = '';
// Include the view mode toggle with the editables selector.
var selector = editablesSelector + ', #toolbar-tab-edit';
activeEditor = that.model.get('activeEditor');
var $confirmDialog = $('#edit_modal');
// If the edit modal is active, that is the tabbing context.
if ($confirmDialog.length) {
context = $confirmDialog;
selector = inputsSelector;
if (!$currentEditable || $currentEditable.is(editablesSelector)) {
$currentEditable = $(selector, context).eq(-1);
}
}
// If an editor is active, then the tabbing context is the editor and
// its toolbar.
else if (activeEditor) {
context = $(activeEditor.$formContainer).add(activeEditor.toolbarView.$el);
// Include the view mode toggle with the editables selector.
selector = inputsSelector;
if (!$currentEditable || $currentEditable.is(editablesSelector)) {
$currentEditable = $(selector, context).eq(-1);
}
}
// Otherwise the tabbing context is the list of editable predicates.
var $editables = $(selector, context);
if (!$currentEditable) {
$currentEditable = $editables.eq(-1);
}
var count = $editables.length - 1;
var index = $editables.index($currentEditable);
// Navigate backwards.
if (event.shiftKey) {
// Beginning of the set, loop to the end.
if (index === 0) {
index = count;
}
else {
index -= 1;
}
}
// Navigate forewards.
else {
// End of the set, loop to the start.
if (index === count) {
index = 0;
}
else {
index += 1;
}
}
// Tab out of the current editable.
$currentEditable.trigger('tabOut.edit');
// Update the current editable.
$currentEditable = $editables
.eq(index)
.focus()
.trigger('tabIn.edit');
// Squelch additional handlers.
event.preventDefault();
event.stopPropagation();
}
});
// Set focus on the edit button initially.
$('#toolbar-tab-edit').focus();
},
/**
* Removes key management and edit accessibility features from the DOM.
*/
_releaseDocumentFocusManagement: function () {
$(document).off('keydown.edit');
$('.edit-allowed.edit-field').removeAttr('tabindex role');
}
});
})(jQuery, _, Backbone, Drupal, VIE);

View File

@ -29,13 +29,6 @@
_initialize: function() {
var that = this;
// Sets the state to 'activated' upon clicking the element.
this.element.on("click.edit", function(event) {
event.stopPropagation();
event.preventDefault();
that.options.activated();
});
// Sets the state to 'changed' whenever the content has changed.
var before = jQuery.trim(this.element.text());
this.element.on('keyup paste', function (event) {
@ -68,6 +61,7 @@
case 'highlighted':
break;
case 'activating':
this.options.activated();
break;
case 'active':
// Sets the "contenteditable" attribute to "true".

View File

@ -29,15 +29,7 @@
/**
* Implements Create's _initialize() method.
*/
_initialize: function() {
// Sets the state to 'activating' upon clicking the element.
var that = this;
this.element.on("click.edit", function(event) {
event.stopPropagation();
event.preventDefault();
that.options.activating();
});
},
_initialize: function() {},
/**
* Makes this PropertyEditor widget react to state changes.
@ -49,15 +41,11 @@
case 'candidate':
if (from !== 'inactive') {
this.disable();
if (from !== 'highlighted') {
this.element.removeClass('edit-belowoverlay');
}
}
break;
case 'highlighted':
break;
case 'activating':
this.element.addClass('edit-belowoverlay');
this.enable();
break;
case 'active':

View File

@ -6,16 +6,6 @@
"use strict";
/**
* The edit ARIA live message area.
*
* @todo Eventually the messages area should be converted into a Backbone View
* that will respond to changes in the application's model. For the initial
* implementation, we will call the Drupal.edit.setMessage method when an aural
* message should be read by the user agent.
*/
var $messages;
Drupal.edit = Drupal.edit || {};
Drupal.edit.metadataCache = Drupal.edit.metadataCache || {};
@ -28,7 +18,7 @@ Drupal.behaviors.edit = {
var $fields = $context.find('[data-edit-id]');
// Initialize the Edit app.
$context.find('#toolbar-tab-edit').once('edit-init', Drupal.edit.init);
$('body').once('edit-init', Drupal.edit.init);
var annotateField = function(field) {
if (_.has(Drupal.edit.metadataCache, field.editID)) {
@ -81,13 +71,6 @@ Drupal.behaviors.edit = {
// Annotate the remaining fields based on the updated access cache.
_.each(remainingFieldsToAnnotate, annotateField);
// As soon as there is at least one editable field, show the Edit
// tab in the toolbar.
if ($fields.filter('.edit-allowed').length) {
$('.toolbar .icon-edit.edit-nothing-editable-hidden')
.removeClass('edit-nothing-editable-hidden');
}
// Find editable fields, make them editable.
Drupal.edit.app.findEditableProperties($context);
}
@ -98,9 +81,6 @@ Drupal.behaviors.edit = {
};
Drupal.edit.init = function() {
// Append a messages element for appending interaction updates for screen
// readers.
$messages = $(Drupal.theme('editMessageBox')).appendTo($(this).parent());
// Instantiate EditAppView, which is the controller of it all. EditAppModel
// instance tracks global state (viewing/editing in-place).
var appModel = new Drupal.edit.models.EditAppModel();
@ -109,32 +89,25 @@ Drupal.edit.init = function() {
model: appModel
});
// Instantiate EditRouter.
var editRouter = new Drupal.edit.routers.EditRouter({
appModel: appModel
// Add "Quick edit" links to all contextual menus where editing the full
// node is possible.
// @todo Generalize this to work for all entities.
$('ul.contextual-links li.node-edit')
.before('<li class="quick-edit"></li>')
.each(function() {
// Instantiate ContextualLinkView.
var $editContextualLink = $(this).prev();
var editContextualLinkView = new Drupal.edit.views.ContextualLinkView({
el: $editContextualLink.get(0),
model: appModel,
entity: $editContextualLink.parents('[data-edit-entity]').attr('data-edit-entity')
});
});
// Start Backbone's history/route handling.
Backbone.history.start();
// For now, we work with a singleton app, because for Drupal.behaviors to be
// able to discover new editable properties that get AJAXed in, it must know
// with which app instance they should be associated.
Drupal.edit.app = app;
};
/**
* Places the message in the edit ARIA live message area.
*
* The message will be read by speaking User Agents.
*
* @param {String} message
* A string to be inserted into the message area.
*/
Drupal.edit.setMessage = function(message) {
var args = Array.prototype.slice.call(arguments);
args.unshift('editMessage');
$messages.html(Drupal.theme.apply(this, args));
};
})(jQuery, _, Backbone, Drupal, drupalSettings);

View File

@ -10,8 +10,7 @@ Drupal.edit = Drupal.edit || {};
Drupal.edit.models = Drupal.edit.models || {};
Drupal.edit.models.EditAppModel = Backbone.Model.extend({
defaults: {
// We always begin in view mode.
isViewing: true,
activeEntity: null,
highlightedEditor: null,
activeEditor: null,
// Reference to a ModalView-instance if a transition requires confirmation.

View File

@ -1,59 +0,0 @@
/**
* @file
* A Backbone Router enabling URLs to make the user enter edit mode directly.
*/
(function(Backbone, Drupal) {
"use strict";
Drupal.edit = Drupal.edit || {};
Drupal.edit.routers = {};
Drupal.edit.routers.EditRouter = Backbone.Router.extend({
appModel: null,
routes: {
"edit": "edit",
"view": "view",
"": "view"
},
initialize: function(options) {
this.appModel = options.appModel;
var that = this;
this.appModel.on('change:isViewing', function() {
that.navigate(that.appModel.get('isViewing') ? '#view' : '#edit');
});
},
edit: function() {
this.appModel.set('isViewing', false);
},
view: function(query, page) {
var that = this;
// If there's an active editor, attempt to set its state to 'candidate', and
// then act according to the user's choice.
var activeEditor = this.appModel.get('activeEditor');
if (activeEditor) {
var editableEntity = activeEditor.options.widget;
var predicate = activeEditor.options.property;
editableEntity.setState('candidate', predicate, { reason: 'menu' }, function(accepted) {
if (accepted) {
that.appModel.set('isViewing', true);
}
else {
that.appModel.set('isViewing', false);
}
});
}
// Otherwise, we can switch to view mode directly.
else {
that.appModel.set('isViewing', true);
}
}
});
})(Backbone, Drupal);

View File

@ -152,29 +152,4 @@ Drupal.theme.editFormContainer = function(settings) {
return html;
};
/**
* A region to post messages that a screen reading UA will announce.
*
* @return {String}
* A string representing a DOM fragment.
*/
Drupal.theme.editMessageBox = function() {
return '<div id="edit-messages" class="element-invisible" role="region" aria-live="polite"></div>';
};
/**
* Wrap message strings in p tags.
*
* @return {String}
* A string representing a DOM fragment.
*/
Drupal.theme.editMessage = function() {
var messages = Array.prototype.slice.call(arguments);
var output = '';
for (var i = 0; i < messages.length; i++) {
output += '<p>' + messages[i] + '</p>';
}
return output;
};
})(jQuery, Drupal);

View File

@ -0,0 +1,109 @@
/**
* @file
* A Backbone View that a dynamic contextual link.
*/
(function ($, _, Backbone, Drupal) {
"use strict";
Drupal.edit = Drupal.edit || {};
Drupal.edit.views = Drupal.edit.views || {};
Drupal.edit.views.ContextualLinkView = Backbone.View.extend({
entity: null,
events: {
'click': 'onClick'
},
/**
* Implements Backbone Views' initialize() function.
*
* @param options
* An object with the following keys:
* - entity: the entity ID (e.g. node/1) of the entity
*/
initialize: function (options) {
this.entity = options.entity;
// Initial render.
this.render();
// Re-render whenever the app state's active entity changes.
this.model.on('change:activeEntity', this.render, this);
// Hide the contextual links whenever an in-place editor is active.
this.model.on('change:activeEditor', this.toggleContextualLinksVisibility, this);
},
/**
* Equates clicks anywhere on the overlay to clicking the active editor's (if
* any) "close" button.
*
* @param {Object} event
*/
onClick: function (event) {
event.preventDefault();
var that = this;
var updateActiveEntity = function() {
// The active entity is the current entity, i.e. stop editing the current
// entity.
if (that.model.get('activeEntity') === that.entity) {
that.model.set('activeEntity', null);
}
// The active entity is different from the current entity, i.e. start
// editing this entity instead of the previous one.
else {
that.model.set('activeEntity', that.entity);
}
};
// If there's an active editor, attempt to set its state to 'candidate', and
// only then do what the user asked.
// (Only when all PropertyEditor widgets of an entity are in the 'candidate'
// state, it is possible to stop editing it.)
var activeEditor = this.model.get('activeEditor');
if (activeEditor) {
var editableEntity = activeEditor.options.widget;
var predicate = activeEditor.options.property;
editableEntity.setState('candidate', predicate, { reason: 'stop or switch' }, function(accepted) {
if (accepted) {
updateActiveEntity();
}
else {
// No change.
}
});
}
// Otherwise, we can immediately do what the user asked.
else {
updateActiveEntity();
}
},
/**
* Render the "Quick edit" contextual link.
*/
render: function () {
var activeEntity = this.model.get('activeEntity');
var string = (activeEntity !== this.entity) ? Drupal.t('Quick edit') : Drupal.t('Stop quick edit');
this.$el.html('<a href="">' + string + '</a>');
return this;
},
/**
* Model change handler; hides the contextual links if an editor is active.
*
* @param Drupal.edit.models.EditAppModel model
* An EditAppModel model.
* @param jQuery|null activeEditor
* The active in-place editor (jQuery object) or, if none, null.
*/
toggleContextualLinksVisibility: function (model, activeEditor) {
this.$el.parents('.contextual').toggle(activeEditor === null);
}
});
})(jQuery, _, Backbone, Drupal);

View File

@ -1,74 +0,0 @@
/**
* @file
* A Backbone View that provides the app-level interactive menu.
*/
(function($, _, Backbone, Drupal) {
"use strict";
Drupal.edit = Drupal.edit || {};
Drupal.edit.views = Drupal.edit.views || {};
Drupal.edit.views.MenuView = Backbone.View.extend({
events: {
'click #toolbar-tab-edit': 'editClickHandler'
},
/**
* Implements Backbone Views' initialize() function.
*/
initialize: function() {
_.bindAll(this, 'stateChange');
this.model.on('change:isViewing', this.stateChange);
// Respond to clicks on other toolbar tabs.
// @todo This temporary pending improvements to the toolbar module.
// @see https://drupal.org/node/1860434
$('#toolbar-administration').on('click.edit', '.bar a:not(#toolbar-tab-edit)', _.bind(function (event) {
this.model.set('isViewing', true);
}, this));
// We have to call stateChange() here because URL fragments are not passed
// to the server, thus the wrong anchor may be marked as active.
this.stateChange();
},
/**
* Listens to app state changes.
*/
stateChange: function() {
var isViewing = this.model.get('isViewing');
// Toggle the state of the Toolbar Edit tab based on the isViewing state.
this.$el.find('#toolbar-tab-edit')
.toggleClass('active', !isViewing)
.attr('aria-pressed', !isViewing);
// Manage the toolbar state until
// https://drupal.org/node/1847198 is resolved
if (!isViewing) {
// Remove the 'toolbar-tray-open' class from the body element.
this.$el.removeClass('toolbar-tray-open');
// Deactivate any other active tabs and trays.
this.$el
.find('.bar a', '#toolbar-administration')
.not('#toolbar-tab-edit')
.add('.tray', '#toolbar-administration')
.removeClass('active');
// Set the height of the toolbar.
if ('toolbar' in Drupal) {
Drupal.toolbar.setHeight();
}
}
},
/**
* Handles clicks on the edit tab of the toolbar.
*
* @param {Object} event
*/
editClickHandler: function (event) {
var isViewing = this.model.get('isViewing');
// Toggle the href of the Toolbar Edit tab based on the isViewing state. The
// href value should represent to state to be entered.
this.$el.find('#toolbar-tab-edit').attr('href', (isViewing) ? '#edit' : '#view');
this.model.set('isViewing', !isViewing);
}
});
})(jQuery, _, Backbone, Drupal);

View File

@ -43,17 +43,6 @@ Drupal.edit.views.ModalView = Backbone.View.extend({
* Implements Backbone Views' render() function.
*/
render: function() {
// Step 1: move certain UI elements below the overlay.
var editor = this.model.get('activeEditor');
this.$elementsToHide = $([])
.add((editor.element.hasClass('edit-belowoverlay')) ? null : editor.element)
.add(editor.toolbarView.$el)
.add((editor.options.editorName === 'form') ? editor.$formContainer : editor.element.next('.edit-validation-errors'));
this.$elementsToHide.addClass('edit-belowoverlay');
// Step 2: the modal. When the user makes a choice, the UI elements that
// were moved below the overlay will be restored, and the callback will be
// called.
this.setElement(Drupal.theme('editModal', {}));
this.$el.appendTo('body');
// Template.
@ -61,13 +50,11 @@ Drupal.edit.views.ModalView = Backbone.View.extend({
var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons}));
this.$('.actions').append($actions);
// Step 3; show the modal with an animation.
// Show the modal with an animation.
var that = this;
setTimeout(function() {
that.$el.removeClass('edit-animate-invisible');
}, 0);
Drupal.edit.setMessage(Drupal.t('Confirmation dialog open'));
},
/**
@ -90,17 +77,6 @@ Drupal.edit.views.ModalView = Backbone.View.extend({
var action = $(event.target).attr('data-edit-modal-action');
return this.callback(action);
},
/**
* Overrides Backbone Views' remove() function.
*/
remove: function() {
// Move the moved UI elements on top of the overlay again.
this.$elementsToHide.removeClass('edit-belowoverlay');
// Remove the modal itself.
this.$el.remove();
}
});

View File

@ -1,86 +0,0 @@
/**
* @file
* A Backbone View that provides the app-level overlay.
*
* The overlay sits on top of the existing content, the properties that are
* candidates for editing sit on top of the overlay.
*/
(function ($, _, Backbone, Drupal) {
"use strict";
Drupal.edit = Drupal.edit || {};
Drupal.edit.views = Drupal.edit.views || {};
Drupal.edit.views.OverlayView = Backbone.View.extend({
events: {
'click': 'onClick'
},
/**
* Implements Backbone Views' initialize() function.
*/
initialize: function (options) {
_.bindAll(this, 'stateChange');
this.model.on('change:isViewing', this.stateChange);
// Add the overlay to the page.
this.$el
.addClass('edit-animate-slow edit-animate-invisible')
.hide()
.appendTo('body');
},
/**
* Listens to app state changes.
*/
stateChange: function () {
if (this.model.get('isViewing')) {
this.remove();
return;
}
this.render();
},
/**
* Equates clicks anywhere on the overlay to clicking the active editor's (if
* any) "close" button.
*
* @param {Object} event
*/
onClick: function (event) {
event.preventDefault();
var activeEditor = this.model.get('activeEditor');
if (activeEditor) {
var editableEntity = activeEditor.options.widget;
var predicate = activeEditor.options.property;
editableEntity.setState('candidate', predicate, { reason: 'overlay' });
}
else {
this.model.set('isViewing', true);
}
},
/**
* Reveal the overlay element.
*/
render: function () {
this.$el
.show()
.css('top', $('#navbar').outerHeight())
.removeClass('edit-animate-invisible');
},
/**
* Hide the overlay element.
*/
remove: function () {
var that = this;
this.$el
.addClass('edit-animate-invisible')
.on(Drupal.edit.util.constants.transitionEnd, function (event) {
that.$el.hide();
});
}
});
})(jQuery, _, Backbone, Drupal);

View File

@ -19,6 +19,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
events: {
'mouseenter.edit' : 'onMouseEnter',
'mouseleave.edit' : 'onMouseLeave',
'click': 'onClick',
'tabIn.edit': 'onMouseEnter',
'tabOut.edit': 'onMouseLeave'
},
@ -38,7 +39,12 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
this.editor = options.editor;
this.toolbarId = options.toolbarId;
this.predicate = this.editor.options.property;
this.$el.css('background-color', this._getBgColor(this.$el));
// Only start listening to events as soon as we're no longer in the 'inactive' state.
this.undelegateEvents();
},
/**
@ -113,13 +119,27 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
});
},
/**
* Clicks: transition to 'activating' stage.
*
* @param event
*/
onClick: function(event) {
var editableEntity = this.editor.options.widget;
editableEntity.setState('activating', this.predicate);
event.preventDefault();
event.stopPropagation();
},
decorate: function () {
this.$el.addClass('edit-animate-fast edit-candidate edit-editable');
this.delegateEvents();
},
undecorate: function () {
this.$el
.removeClass('edit-candidate edit-editable edit-highlighted edit-editing');
this.undelegateEvents();
},
startHighlight: function () {

View File

@ -29,7 +29,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
'click.edit button.label': 'onClickInfoLabel',
'mouseleave.edit': 'onMouseLeave',
'click.edit button.field-save': 'onClickSave',
'click.edit button.field-close': 'onClickClose'
'click.edit button.field-close': 'onClickClose',
},
/**
@ -66,19 +66,26 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
stateChange: function(from, to) {
switch (to) {
case 'inactive':
// Nothing happens in this stage.
if (from) {
this.remove();
}
break;
case 'candidate':
if (from !== 'inactive') {
if (from === 'inactive') {
this.render();
}
else {
// Remove all toolgroups; they're no longer necessary.
this.$el
.removeClass('edit-highlighted edit-editing')
.find('.edit-toolbar .edit-toolgroup').remove();
if (from !== 'highlighted' && this.getEditUISetting('padding')) {
this._unpad();
}
this.remove();
}
break;
case 'highlighted':
// As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title).
this.render();
this.startHighlight();
break;
case 'activating':
@ -275,6 +282,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
}
this.$el
.addClass('edit-highlighted')
.find('.edit-toolbar')
// Append the "info" toolgroup into the toolbar.
.append(Drupal.theme('editToolgroup', {

View File

@ -48,6 +48,13 @@ class FilterFormatAccessTest extends WebTestBase {
*/
protected $disallowed_format;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('contextual');
public static function getInfo() {
return array(
'name' => 'Filter format access',
@ -88,6 +95,7 @@ class FilterFormatAccessTest extends WebTestBase {
$this->web_user = $this->drupalCreateUser(array(
'create page content',
'edit any page content',
'access contextual links',
filter_permission_name($this->allowed_format),
));
@ -96,6 +104,7 @@ class FilterFormatAccessTest extends WebTestBase {
'administer filters',
'create page content',
'edit any page content',
'access contextual links',
filter_permission_name($this->allowed_format),
filter_permission_name($this->disallowed_format),
));

View File

@ -88,11 +88,7 @@ class NodeRenderController extends EntityRenderController {
*/
protected function alterBuild(array &$build, EntityInterface $entity, EntityDisplay $display, $view_mode, $langcode = NULL) {
parent::alterBuild($build, $entity, $display, $view_mode, $langcode);
// Add contextual links for this node, except when the node is already being
// displayed on its own page. Modules may alter this behavior (for example,
// to restrict contextual links to certain view modes) by implementing
// hook_node_view_alter().
if (!empty($entity->nid) && !($view_mode == 'full' && node_is_page($entity))) {
if (!empty($entity->nid)) {
$build['#contextual_links']['node'] = array('node', array($entity->nid));
}
}

View File

@ -14,6 +14,13 @@ class PageEditTest extends NodeTestBase {
protected $web_user;
protected $admin_user;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node', 'contextual');
public static function getInfo() {
return array(
'name' => 'Node edit',
@ -25,8 +32,8 @@ class PageEditTest extends NodeTestBase {
function setUp() {
parent::setUp();
$this->web_user = $this->drupalCreateUser(array('edit own page content', 'create page content'));
$this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes'));
$this->web_user = $this->drupalCreateUser(array('edit own page content', 'create page content', 'access contextual links'));
$this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'access contextual links'));
}
/**
@ -50,14 +57,11 @@ class PageEditTest extends NodeTestBase {
// Check that "edit" link points to correct page.
$this->clickLink(t('Edit'));
$edit_url = url("node/$node->nid/edit", array('absolute' => TRUE));
$edit_url = url("node/$node->nid/edit", array('absolute' => TRUE, 'query' => array('destination' => 'node/1')));
$actual_url = $this->getURL();
$this->assertEqual($edit_url, $actual_url, 'On edit page.');
// Check that the title and body fields are displayed with the correct values.
$active = '<span class="element-invisible">' . t('(active tab)') . '</span>';
$link_text = t('!local-task-title!active', array('!local-task-title' => t('Edit'), '!active' => $active));
$this->assertText(strip_tags($link_text), 0, 'Edit tab found and marked active.');
$this->assertFieldByName($title_key, $edit[$title_key], 'Title field displayed.');
$this->assertFieldByName($body_key, $edit[$body_key], 'Body field displayed.');

View File

@ -1769,7 +1769,7 @@ function node_menu() {
'access arguments' => array('update', 1),
'weight' => 0,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
'context' => MENU_CONTEXT_INLINE,
'file' => 'node.pages.inc',
);
$items['node/%node/delete'] = array(
@ -1791,6 +1791,7 @@ function node_menu() {
'access arguments' => array(1),
'weight' => 2,
'type' => MENU_LOCAL_TASK,
'context' => MENU_CONTEXT_INLINE,
'file' => 'node.pages.inc',
);
$items['node/%node/revisions/%node_revision/view'] = array(

View File

@ -1999,7 +1999,7 @@ function system_library_info() {
'website' => 'http://underscorejs.org/',
'version' => '1.4.0',
'js' => array(
'core/misc/underscore/underscore.js' => array('group' => JS_LIBRARY),
'core/misc/underscore/underscore.js' => array('group' => JS_LIBRARY, 'weight' => -20),
),
);
@ -2009,7 +2009,7 @@ function system_library_info() {
'website' => 'http://backbonejs.org/',
'version' => '0.9.2',
'js' => array(
'core/misc/backbone/backbone.js' => array('group' => JS_LIBRARY),
'core/misc/backbone/backbone.js' => array('group' => JS_LIBRARY, 'weight' => -19),
),
'dependencies' => array(
array('system', 'underscore'),

View File

@ -12,6 +12,13 @@ namespace Drupal\taxonomy\Tests;
*/
class TermTest extends TaxonomyTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('taxonomy', 'contextual');
public static function getInfo() {
return array(
'name' => 'Taxonomy term functions and forms',
@ -22,7 +29,7 @@ class TermTest extends TaxonomyTestBase {
function setUp() {
parent::setUp();
$this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access'));
$this->admin_user = $this->drupalCreateUser(array('administer taxonomy', 'bypass node access', 'access contextual links'));
$this->drupalLogin($this->admin_user);
$this->vocabulary = $this->createVocabulary();