core/misc/edit-active.png | 8 ++ core/misc/edit.png | 3 + core/modules/contextual/contextual.js | 50 ++++++++-- core/modules/contextual/contextual.module | 52 ++++++++++ core/modules/contextual/contextual.theme.css | 36 ++++--- core/modules/contextual/contextual.toolbar.css | 23 +++++ core/modules/contextual/contextual.toolbar.js | 100 ++++++++++++++++++++ core/modules/edit/css/edit.css | 50 ++-------- core/modules/edit/edit.info | 1 + core/modules/edit/edit.module | 44 +++------ core/modules/edit/images/icon-edit-active.png | 3 - core/modules/edit/images/icon-edit.png | 5 - core/modules/edit/js/app.js | 54 ++++++----- .../editingWidgets/drupalcontenteditablewidget.js | 8 +- .../edit/js/createjs/editingWidgets/formwidget.js | 14 +-- core/modules/edit/js/edit.js | 28 +++--- core/modules/edit/js/models/edit-app-model.js | 3 +- core/modules/edit/js/routers/edit-router.js | 59 ------------ .../{overlay-view.js => contextuallink-view.js} | 88 +++++++++-------- core/modules/edit/js/views/menu-view.js | 82 ---------------- core/modules/edit/js/views/modal-view.js | 24 +---- .../edit/js/views/propertyeditordecoration-view.js | 20 ++++ core/modules/edit/js/views/toolbar-view.js | 18 +++- .../node/lib/Drupal/node/NodeRenderController.php | 6 +- core/modules/node/node.module | 3 +- core/modules/system/system.module | 4 +- 26 files changed, 406 insertions(+), 380 deletions(-) diff --git a/core/misc/edit-active.png b/core/misc/edit-active.png new file mode 100644 index 0000000..a9143aa --- /dev/null +++ b/core/misc/edit-active.png @@ -0,0 +1,8 @@ +PNG + + IHDR!-IDATxڭmP]”@ )(%PGܕHX@ JH<(4p|O%}y Y~ +tu&0hA+\l'Sv'`^+yX +)DsqV+9%FaHrTCͦs"6 +sPa%"y{{Soր=!_\Ay}O`|l0GE+! &hdY0'/M`NR!C2Nh~CR˂sW9yiH. "D0G\Od2Q +R>Hq.APa`0.h߹#.~_az&.w&Ha*ѿ +⹆ M0Gia MHn0GCNsn+Cժ a"BO+}37IENDB` \ No newline at end of file diff --git a/core/misc/edit.png b/core/misc/edit.png new file mode 100644 index 0000000..d176245 --- /dev/null +++ b/core/misc/edit.png @@ -0,0 +1,3 @@ +PNG + + IHDR!-#IDATxڵ́@EєKH)%XHDDĀ(Fl,ᗐۻ0BI>80LҿfPԠȾjpŰG.E4־kdpa:Ǐǣ79Bp08OTYv;C n8!C fcp(#=^Uu%_V9T{l\Vy/X, E{n>Zwf5x@EQ  t:C[L&9jȳ/$UW5IENDB` \ No newline at end of file diff --git a/core/modules/contextual/contextual.js b/core/modules/contextual/contextual.js index dee63e6..dc1f4a5 100644 --- a/core/modules/contextual/contextual.js +++ b/core/modules/contextual/contextual.js @@ -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,15 +63,32 @@ 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('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('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. * * @return {String} diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index 30dcacb..22a6dcc 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -6,6 +6,41 @@ */ /** + * Implements hook_toolbar(). + */ +function contextual_toolbar() { + if (!user_access('access contextual links')) { + return; + } + + $tab['contextual'] = array( + '#type' => 'toolbar_item', + 'tab' => array( + '#type' => 'link', + '#title' => t('Edit'), + '#href' => '', + '#options' => array( + 'html' => FALSE, + 'attributes' => array( + 'class' => array('icon', 'icon-edit'), + ), + ), + ), + '#wrapper_attributes' => array( + 'id' => 'contextual-toolbar-tab', + 'class' => array('element-hidden'), + ), + '#attached' => array( + 'library' => array( + array('contextual', 'drupal.contextual-toolbar'), + ), + ), + ); + + return $tab; +} + +/** * Implements hook_help(). */ function contextual_help($path, $arg) { @@ -57,6 +92,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; } diff --git a/core/modules/contextual/contextual.theme.css b/core/modules/contextual/contextual.theme.css index aca019c..0aa78ef 100644 --- a/core/modules/contextual/contextual.theme.css +++ b/core/modules/contextual/contextual.theme.css @@ -10,7 +10,7 @@ position: absolute; right: 0; /* LTR */ top: 2px; - z-index: 999; + z-index: 500; } .contextual-region-active { outline: 1px dashed #d6d6d6; @@ -21,28 +21,32 @@ * 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; + 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 +56,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 +94,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%); } diff --git a/core/modules/contextual/contextual.toolbar.css b/core/modules/contextual/contextual.toolbar.css new file mode 100644 index 0000000..385dff8 --- /dev/null +++ b/core/modules/contextual/contextual.toolbar.css @@ -0,0 +1,23 @@ +/** + * @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 location. */ +#contextual-toolbar-tab { + float: right; +} + +/* @todo get rid of this declaration by making toolbar.module's CSS less specific */ +#contextual-toolbar-tab.element-hidden { + display: none; +} diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js new file mode 100644 index 0000000..57680a9 --- /dev/null +++ b/core/modules/contextual/contextual.toolbar.js @@ -0,0 +1,100 @@ +/** + * @file + * Attaches behaviors for the Contextual module's "Edit" toolbar tab. + */ + +(function ($, Backbone, Drupal, document) { + +"use strict"; + +/** + * Attaches contextual's "Edit" toolbar tab behavior. + * + * Events + * Contextual triggers a number of events that can be used by other scripts. + * - drupalEditModeChanged: This event is triggered when the edit mode changes. + */ +Drupal.behaviors.contextualToolbar = { + attach: function (context) { + $('body').once('contextualToolbar-init', function () { + var model = new Drupal.contextualToolbar.models.EditToggleModel(); + var view = new Drupal.contextualToolbar.views.EditToggleView({ + el: $('#contextual-toolbar-tab'), + model: model + }); + + // Update the model based on overlay events. + $(document) + .on('drupalOverlayOpen.contextualToolbar', function () { + model.set({ isViewing: true, 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 ($(context).find('.contextual-links').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 }); + }); + }); + } +}; + +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 + } +}); + +/** + * Backbone View to make the "Edit" toggle interactive. + */ +Drupal.contextualToolbar.views.EditToggleView = Backbone.View.extend({ + + events: { 'click': 'onClick' }, + + /** + * Implements Backbone Views' initialize() function. + */ + initialize: function () { + this.model.on('change', this.render, this); + }, + + /** + * Implements Backbone Views' render() function. + */ + render: function () { + // Render the visibility. + this.$el.toggleClass('element-hidden', !this.model.get('isVisible')); + + // Render the state. + var isViewing = this.model.get('isViewing'); + this.$el.find('a') + .toggleClass('active', !isViewing) + .attr('aria-pressed', !isViewing); + + return this; + }, + + onClick: function (event) { + this.model.set('isViewing', !this.model.get('isViewing')); + event.preventDefault(); + event.stopPropagation(); + } + +}); + +})(jQuery, Backbone, Drupal, document); diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 37e10eb..7b758b7 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -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"); -} -.toolbar .tray.edit.active { - z-index: 340; -} -.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. */ diff --git a/core/modules/edit/edit.info b/core/modules/edit/edit.info index 4074a7b..7c6d4b9 100644 --- a/core/modules/edit/edit.info +++ b/core/modules/edit/edit.info @@ -3,4 +3,5 @@ description = In-place content editing. package = Core core = 8.x version = VERSION +dependencies[] = contextual dependencies[] = field diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 584cace..fd4e096 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -39,39 +39,16 @@ function edit_permission() { } /** - * Implements hook_toolbar(). + * Implements hook_contextual_links_view_alter(). */ -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'), - ), - ), - ), - '#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); } /** @@ -91,15 +68,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, @@ -171,6 +145,16 @@ function edit_preprocess_field(&$variables) { } /** + * 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. * * @ingroup forms diff --git a/core/modules/edit/images/icon-edit-active.png b/core/modules/edit/images/icon-edit-active.png deleted file mode 100644 index ad84761..0000000 --- a/core/modules/edit/images/icon-edit-active.png +++ /dev/null @@ -1,3 +0,0 @@ -PNG - - IHDRj `PLTE[tRNS@ P00p`ϟDƙIDATxe DQ8Ϩ/BDU9xV+D\?x@qWcF8wicS B}?v;Vf.V$JgX=Kضp0XS"iRw\:LL\~;Z5wu 5E)LIENDB` \ No newline at end of file diff --git a/core/modules/edit/images/icon-edit.png b/core/modules/edit/images/icon-edit.png deleted file mode 100644 index 4f0dcc2..0000000 --- a/core/modules/edit/images/icon-edit.png +++ /dev/null @@ -1,5 +0,0 @@ -PNG - - IHDRj PLTE̻ʪ̡̜ˠ̣̽¼ǷʨªZ(e+tRNSϟ `π@`0p0p`0PϟϟcIDATxeW0{W -H"ʵ,y {Hpyo?mf,RBRxB vL;&LPJaRb\(Tbn(1wϔJ)ԈkS -58äT^4P6c}[i <ާ'-+HP>KIENDB` \ No newline at end of file diff --git a/core/modules/edit/js/app.js b/core/modules/edit/js/app.js index 00bba20..8181039 100644 --- a/core/modules/edit/js/app.js +++ b/core/modules/edit/js/app.js @@ -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,18 +111,30 @@ /** * 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')); @@ -136,6 +143,7 @@ this._releaseDocumentFocusManagement(); Drupal.edit.setMessage(Drupal.t('Edit mode is inactive.'), Drupal.t('Resume normal page navigation')); } + */ }, /** @@ -159,9 +167,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. diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js index 5671f39..caac604 100644 --- a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js @@ -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". diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js index 3238566..83dad4f 100644 --- a/core/modules/edit/js/createjs/editingWidgets/formwidget.js +++ b/core/modules/edit/js/createjs/editingWidgets/formwidget.js @@ -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': diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index cfaf76b..639ff75 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -28,7 +28,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 +81,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); } @@ -109,14 +102,21 @@ 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('
  • ') + .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. diff --git a/core/modules/edit/js/models/edit-app-model.js b/core/modules/edit/js/models/edit-app-model.js index b6ff36f..0c90fd0 100644 --- a/core/modules/edit/js/models/edit-app-model.js +++ b/core/modules/edit/js/models/edit-app-model.js @@ -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. diff --git a/core/modules/edit/js/routers/edit-router.js b/core/modules/edit/js/routers/edit-router.js deleted file mode 100644 index d160ad4..0000000 --- a/core/modules/edit/js/routers/edit-router.js +++ /dev/null @@ -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); diff --git a/core/modules/edit/js/views/overlay-view.js b/core/modules/edit/js/views/contextuallink-view.js similarity index 35% rename from core/modules/edit/js/views/overlay-view.js rename to core/modules/edit/js/views/contextuallink-view.js index 2113ab8..cb6a2f6 100644 --- a/core/modules/edit/js/views/overlay-view.js +++ b/core/modules/edit/js/views/contextuallink-view.js @@ -1,9 +1,6 @@ /** * @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. + * A Backbone View that a dynamic contextual link. */ (function ($, _, Backbone, Drupal) { @@ -11,7 +8,9 @@ Drupal.edit = Drupal.edit || {}; Drupal.edit.views = Drupal.edit.views || {}; -Drupal.edit.views.OverlayView = Backbone.View.extend({ +Drupal.edit.views.ContextualLinkView = Backbone.View.extend({ + + entity: null, events: { 'click': 'onClick' @@ -19,26 +18,19 @@ Drupal.edit.views.OverlayView = Backbone.View.extend({ /** * 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) { - _.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'); - }, + this.entity = options.entity; - /** - * Listens to app state changes. - */ - stateChange: function () { - if (this.model.get('isViewing')) { - this.remove(); - return; - } + // Initial render. this.render(); + + // Re-render whenever the app state's active entity changes. + this.model.on('change:activeEntity', this.render, this); }, /** @@ -49,38 +41,54 @@ Drupal.edit.views.OverlayView = Backbone.View.extend({ */ 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: 'overlay' }); + 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 { - this.model.set('isViewing', true); + updateActiveEntity(); } }, /** - * Reveal the overlay element. + * Render the "Quick edit" contextual link. */ 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(); - }); + var activeEntity = this.model.get('activeEntity'); + var string = (activeEntity !== this.entity) ? Drupal.t('Quick edit') : Drupal.t('Stop quick edit'); + this.$el.html('' + string + ''); + return this; } + }); })(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/menu-view.js b/core/modules/edit/js/views/menu-view.js deleted file mode 100644 index ac7c4e4..0000000 --- a/core/modules/edit/js/views/menu-view.js +++ /dev/null @@ -1,82 +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); - // @todo - // Re-implement hook_toolbar and the corresponding JavaScript behaviors - // once https://drupal.org/node/1847198 is resolved. The toolbar tray is - // necessary when the page request is processed because its render element - // has an #attached property with the Edit module library code assigned to - // it. Currently a toolbar tab is not passed as a renderable array, so - // #attached properties are not processed. The toolbar tray DOM element is - // unnecessary right now, so it is removed. - this.$el.find('#toolbar-tray-edit').remove(); - // Respond to clicks on other toolbar tabs. This temporary pending - // improvements to the toolbar module. - $('#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); diff --git a/core/modules/edit/js/views/modal-view.js b/core/modules/edit/js/views/modal-view.js index 2e3b49c..edd158a 100644 --- a/core/modules/edit/js/views/modal-view.js +++ b/core/modules/edit/js/views/modal-view.js @@ -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,7 +50,7 @@ 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'); @@ -90,17 +79,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(); } }); diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js index 0eb4e45..aad9832 100644 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ b/core/modules/edit/js/views/propertyeditordecoration-view.js @@ -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 () { diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js index 90f5db7..b052362 100644 --- a/core/modules/edit/js/views/toolbar-view.js +++ b/core/modules/edit/js/views/toolbar-view.js @@ -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', { diff --git a/core/modules/node/lib/Drupal/node/NodeRenderController.php b/core/modules/node/lib/Drupal/node/NodeRenderController.php index e829e40..6a6608a 100644 --- a/core/modules/node/lib/Drupal/node/NodeRenderController.php +++ b/core/modules/node/lib/Drupal/node/NodeRenderController.php @@ -88,11 +88,7 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang */ 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)); } } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index f2206a0..ff073ae 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -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( diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 322e416..b361134 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1998,7 +1998,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), ), ); @@ -2008,7 +2008,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'),