core/modules/edit/css/edit.css | 36 +------- core/modules/edit/edit.module | 11 +++ core/modules/edit/js/app.js | 42 ++++++--- core/modules/edit/js/models/edit-app-model.js | 1 + core/modules/edit/js/views/contextuallink-view.js | 94 ++++++++++++++++++++ core/modules/edit/js/views/menu-view.js | 22 ++++- .../edit/js/views/propertyeditordecoration-view.js | 22 +++++ core/modules/edit/js/views/toolbar-view.js | 28 ------ 8 files changed, 179 insertions(+), 77 deletions(-) diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 436e024..fcf8893 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -1,32 +1,4 @@ /** - * Pencil icon. - */ -.edit-toolbar-container .pencil { - background: #fff url(../../../misc/edit.png) no-repeat center center; - background-size: 16px 16px; - border: 1px solid #ddd; - border-radius: 13px; - -moz-box-shadow: 1px 1px 2px rgba(0,0,0,0.3); - -webkit-box-shadow: 1px 1px 2px rgba(0,0,0,0.3); - box-shadow: 1px 1px 2px rgba(0,0,0,0.3); - height: 26px; - width: 26px; - margin: 0; - outline: none; - overflow: hidden; - padding:0; - text-indent: 34px; - position: absolute; - right: 2px; /* LTR */ - top: 2px; -} - -.edit-toolbar-container.edit-editing .pencil { - display: none; -} - - -/** * Animations. */ .edit-animate-invisible { @@ -149,18 +121,12 @@ } .edit-field.edit-editable, .edit-field .edit-editable { - /** - * In the latest design, there's no need to indicate candidates, since they - * now use pencil icons. - * This will probably be necessary again before release. - */ + box-shadow: 0 0 1px 1px #4d9de9; } /* Highlighted (hovered) editable. */ .edit-editable.edit-highlighted { z-index: 305; -} -.edit-editable.edit-highlighted { min-width: 200px; } .edit-field.edit-editable.edit-highlighted, diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 584cace..cb67596 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -98,6 +98,7 @@ function edit_library_info() { // 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, @@ -171,6 +172,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/js/app.js b/core/modules/edit/js/app.js index 00bba20..10c936b 100644 --- a/core/modules/edit/js/app.js +++ b/core/modules/edit/js/app.js @@ -60,7 +60,7 @@ }); // 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 +74,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 +103,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 +123,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 +155,7 @@ this._releaseDocumentFocusManagement(); Drupal.edit.setMessage(Drupal.t('Edit mode is inactive.'), Drupal.t('Resume normal page navigation')); } + */ }, /** @@ -159,9 +179,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/models/edit-app-model.js b/core/modules/edit/js/models/edit-app-model.js index b6ff36f..1e4dcc6 100644 --- a/core/modules/edit/js/models/edit-app-model.js +++ b/core/modules/edit/js/models/edit-app-model.js @@ -12,6 +12,7 @@ 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/views/contextuallink-view.js b/core/modules/edit/js/views/contextuallink-view.js new file mode 100644 index 0000000..cb6a2f6 --- /dev/null +++ b/core/modules/edit/js/views/contextuallink-view.js @@ -0,0 +1,94 @@ +/** + * @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); + }, + + /** + * 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('' + 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 index 47e2955..d5e34d4 100644 --- a/core/modules/edit/js/views/menu-view.js +++ b/core/modules/edit/js/views/menu-view.js @@ -35,16 +35,32 @@ Drupal.edit.views.MenuView = Backbone.View.extend({ this.model.set('isViewing', true); }, this)); - // Add quick edit links to all contextual menus where editing the full node is possible. - $('ul.contextual-links li.node-edit').before('
  • ' + Drupal.t('Quick edit') + '
  • '); - // 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(); + + // Add "Quick edit" links to all contextual menus where editing the full + // node is possible. + // @todo Generalize this to work for all entities. + var that = this; + $('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: that.model, + entity: $editContextualLink.parents('[data-edit-entity]').attr('data-edit-entity') + }); + }); }, /** * Listens to app state changes. + * + * @todo This stuff is all for the "Edit mode" toggle, which doesn't really + * belong in Edit.module anymore. */ stateChange: function() { var isViewing = this.model.get('isViewing'); diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js index aabd72c..aad9832 100644 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ b/core/modules/edit/js/views/propertyeditordecoration-view.js @@ -17,6 +17,9 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({ _widthAttributeIsEmpty: null, events: { + 'mouseenter.edit' : 'onMouseEnter', + 'mouseleave.edit' : 'onMouseLeave', + 'click': 'onClick', 'tabIn.edit': 'onMouseEnter', 'tabOut.edit': 'onMouseLeave' }, @@ -36,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(); }, /** @@ -111,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 41e15ad..b052362 100644 --- a/core/modules/edit/js/views/toolbar-view.js +++ b/core/modules/edit/js/views/toolbar-view.js @@ -30,9 +30,6 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ 'mouseleave.edit': 'onMouseLeave', 'click.edit button.field-save': 'onClickSave', 'click.edit button.field-close': 'onClickClose', - 'mouseenter .pencil': 'onPencilMouseEnter', - 'mouseleave .pencil': 'onPencilMouseLeave', - 'click .pencil': 'onPencilClick' }, /** @@ -406,10 +403,6 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({ this.$el.insertBefore(this.editor.element); } - // @todo: replace "Edit" with a proper (ARIA-like) string. - // @todo: ->