Commit 6d5c2113 authored by Dries's avatar Dries

Issue #1874664 by Wim Leers, jessebeach, tkoleary, Gábor Hojtsy, quicksketch,...

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 878a4162
......@@ -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;
cursor: pointer;
}
.no-touch .contextual .trigger:hover,
.contextual-links-active .trigger {
background-position: 2px -18px;
}
.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%);
}
/**
* @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;
}
/**
* @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;
}
/**
* @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.
*
......
This diff is collapsed.
......@@ -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':
......