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.
parent
878a4162d9
commit
6d5c211392
Binary file not shown.
After Width: | Height: | Size: 226 B |
Binary file not shown.
After Width: | Height: | Size: 365 B |
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
.contextual .trigger {
|
||||
float: left;
|
||||
right: 0;
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
|
@ -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. */
|
||||
|
|
|
@ -3,4 +3,5 @@ description = In-place content editing.
|
|||
package = Core
|
||||
core = 8.x
|
||||
version = VERSION
|
||||
dependencies[] = contextual
|
||||
dependencies[] = field
|
||||
|
|
|
@ -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 |
|
@ -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);
|
||||
|
|
|
@ -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".
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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 () {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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),
|
||||
));
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Reference in New Issue