diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 6a5ac83..af5cb0e 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -69,8 +69,17 @@ transition: background, padding .2s ease; } - - +/** + * Entity toolbar. + */ +.edit-entity-toolbar-container { + background-color: red; + position:absolute; + -webkit-transition: all 0.2s; + transition: all 0.2s; + width: 20em; + z-index: 350; +} /** * Candidate editables + editables being edited. diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index c3f0d78..c16990f 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -79,12 +79,11 @@ function edit_library_info() { $path . '/js/models/FieldModel.js' => $options, // Views. $path . '/js/views/AppView.js' => $options, - $path . '/js/views/EntityView.js' => $options, - $path . '/js/views/EditorView.js' => $options, $path . '/js/views/PropertyEditorDecorationView.js' => $options, $path . '/js/views/ContextualLinkView.js' => $options, $path . '/js/views/ModalView.js' => $options, $path . '/js/views/ToolbarView.js' => $options, + $path . '/js/views/EntityToolbarView.js' => $options, // Backbone.sync implementation on top of Drupal forms. $path . '/js/backbone.drupalform.js' => $options, // Storage manager. @@ -110,8 +109,10 @@ function edit_library_info() { array('system', 'underscore'), array('system', 'backbone'), array('system', 'jquery.form'), + array('system', 'jquery.ui.position'), array('system', 'drupal.form'), array('system', 'drupal.ajax'), + array('system', 'drupal.debounce'), array('system', 'drupalSettings'), ), ); diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index 9d677a1..52088ec 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -16,7 +16,14 @@ Drupal.behaviors.edit = { views: { contextualLinks: {}, - entities: {} + decorators: {}, + editables: {}, + entities: {}, + propertyEditors: {}, + toolbars: { + entities: {}, + fields: {} + } }, collections: { @@ -124,11 +131,13 @@ Drupal.behaviors.edit = { }); that.collections.entities.add(entityModel); - // Create a View for the entity. - that.views.entities[id] = new Drupal.edit.EntityView($.extend({ + // Create a Toolbar for the entity. + // @todo, the toolbar should really be create when quick edit is launched + // for an entity, not up front for all of them. + that.views.toolbars.entities[id] = new Drupal.edit.EntityToolbarView({ el: this, model: entityModel - }, options)); + }); // Create a view for the contextual links. $this.find('.contextual-links') @@ -197,6 +206,8 @@ Drupal.behaviors.edit = { var field = new Drupal.edit.FieldModel({ entity: entity, $el: $element, + // Store the field in a collection in its entity's model. + collection: entity.get('fields'), editID: editID, label: Drupal.edit.metadataCache[editID].label, editor: Drupal.edit.metadataCache[editID].editor, diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js index 047ffe7..a527a0c 100644 --- a/core/modules/edit/js/models/EntityModel.js +++ b/core/modules/edit/js/models/EntityModel.js @@ -17,10 +17,56 @@ $.extend(Drupal.edit, { // edited. isActive: false, // A Drupal.edit.FieldCollection for all fields of this entity. - fields: null + fields: null, + // The model for the currently active field. The default is a stub object + // with an 'on' method so that it can be result to the default value + // when no fields are active on this entity without breaking listener + // attachment calls to this property's object. + fieldModel: (function () { + return { + on: function () {}, + off: function () {} + }; + }()) }, + + /** + * + */ initialize: function () { this.set('fields', new Drupal.edit.FieldCollection()); + + this.get('fields').on('change:state', this.stateChange, this); + }, + + /** + * Listens to FieldModel editor state changes. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of an editable element. Used to determine display and behavior. + */ + stateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + this.set('fieldModel', this.defaults.fieldModel); + break; + case 'candidate': + case 'highlighted': + break; + case 'activating': + this.set('fieldModel', model); + break; + case 'active': + case 'changed': + case 'saving': + case 'saved': + case 'invalid': + default: + break; + } } }) }); diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js index 83cd2f0..7703d2f 100644 --- a/core/modules/edit/js/models/FieldModel.js +++ b/core/modules/edit/js/models/FieldModel.js @@ -27,7 +27,7 @@ $.extend(Drupal.edit, { // - invalid // @see http://createjs.org/guide/#states state: 'inactive', - // A Drupal.edit.EntityModel. Its "properties" attribute, which is a + // A Drupal.edit.EntityModel. Its "fields" attribute, which is a // FieldCollection, is automatically updated to include this FieldModel. entity: null, // A place to store any decoration views. diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js index 7bef553..1c86fe4 100644 --- a/core/modules/edit/js/theme.js +++ b/core/modules/edit/js/theme.js @@ -75,6 +75,23 @@ Drupal.theme.editToolbarContainer = function(settings) { }; /** + * Theme function for a toolbar container of the Edit module. + * + * @param settings + * An object with the following keys: + * - id: the id to apply to the toolbar container. + * @return + * The corresponding HTML. + */ +Drupal.theme.editEntityToolbarContainer = function(settings) { + var html = ''; + html += '
'; + html += '
'; + html += '
'; + return html; +}; + +/** * Theme function for a toolbar toolgroup of the Edit module. * * @param settings diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js deleted file mode 100644 index 49bd4cb..0000000 --- a/core/modules/edit/js/views/EditorView.js +++ /dev/null @@ -1,17 +0,0 @@ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; - -$.extend(Drupal.edit, { - - // @todo provide a base class for formEditor/directEditor/…, migrate them away - // from editor.js - // @todo maybe migrate the editors in edit to the Editor module? - EditorView: Backbone.View.extend({ - - }) -}); - -}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js new file mode 100644 index 0000000..cbc46f1 --- /dev/null +++ b/core/modules/edit/js/views/EntityToolbarView.js @@ -0,0 +1,333 @@ +/** + * @file + * A Backbone View that provides an entity level toolbar. + */ +(function ($, Backbone, Drupal, debounce) { + +"use strict"; + +Drupal.edit.EntityToolbarView = Backbone.View.extend({ + + _loader: null, + _loaderVisibleStart: 0, + + events: function () { + var map = { + 'click.edit button.field-save': 'onClickSave', + 'click.edit button.field-close': 'onClickClose' + } + return map; + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function (options) { + var that = this; + + this.model.on('change:isActive', this.render, this); + + // When the fieldModel attribute of the EntityModel changes, set a listener + // on the active fieldModel that proxies its state changes to this view. + this.model.on('change:fieldModel', function (model, fieldModel) { + // Remove the listener from the previous fieldModel. + that.model.previous('fieldModel').off('change:state', that.fieldStateChange, that); + // Attach a change listener to the current active fieldModel. + fieldModel.on('change:state', that.fieldStateChange, that); + }); + + $(window).on('resize.edit scroll.edit', debounce($.proxy(this.windowChangeHandler, this), 150)); + + // Set the el into its own property. Eventually the el property will be + // replaced with the rendered toolbar. + this.$entity = this.$el; + + // Set the toolbar container to this view's el property. + this.buildToolbar(); + + this._loader = null; + this._loaderVisibleStart = 0; + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function (model, changeValue) { + + if (this.model.get('isActive')) { + // If the toolbar container doesn't exist, create it. + if ($('body').children('#edit-entity-toolbar').length === 0) { + $('body').append(this.$el); + } + // If render is being called and the toolbar is already visible, just + // reposition it. + this.position(); + this.show('ops'); + } + else { + this.$el.detach(); + } + + return this; + }, + + /** + * + */ + windowChangeHandler: function (event) { + this.position(); + }, + + /** + * + */ + position: function (element) { + var of = element || this.$entity; + this.$el + .position({ + my: 'left bottom', + at: 'left top', + of: of + }); + }, + + /** + * Determines the actions to take given a change of state. + * + * @param Drupal.edit.FieldModel model + * @param String state + * The state of the associated field. One of Drupal.edit.FieldModel.states. + */ + fieldStateChange: function (model, state) { + var from = model.previous('state'); + var to = state; + switch (to) { + case 'inactive': + break; + if (from) { + this.remove(); + if (this.model.get('editor') !== 'form') { + Backbone.syncDirectCleanUp(); + } + } + break; + case 'candidate': + break; + if (from === 'inactive') { + this.render(); + } + else { + if (this.model.get('editor') !== 'form') { + Backbone.syncDirectCleanUp(); + } + // 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(); + } + } + break; + case 'highlighted': + // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). + this.startHighlight(); + break; + case 'activating': + this.setLoadingIndicator(true); + break; + case 'active': + this.startEdit(); + this.setLoadingIndicator(false); + // Position the toolbar against the active field. + this.position(model.get('$el')); + /*if (this.getEditUISetting('fullWidthToolbar')) { + this.$el.addClass('edit-toolbar-fullwidth'); + }*/ + + /*if (this.getEditUISetting('padding')) { + this._pad(); + }*/ + /*if (this.getEditUISetting('unifiedToolbar')) { + this.insertWYSIWYGToolGroups(); + }*/ + break; + case 'changed': + this.$el + .find('button.save') + .addClass('blue-button') + .removeClass('gray-button'); + break; + case 'saving': + this.setLoadingIndicator(true); + break; + case 'saved': + this.setLoadingIndicator(false); + break; + case 'invalid': + this.setLoadingIndicator(false); + break; + } + }, + + /** + * Set the model state to 'saving' when the save button is clicked. + * + * @param jQuery event + */ + onClickSave: function (event) { + event.stopPropagation(); + event.preventDefault(); + this.model.set('state', 'saving'); + }, + + /** + * Sets the model state to candidate when the cancel button is clicked. + * + * @param jQuery event + */ + onClickClose: function (event) { + event.stopPropagation(); + event.preventDefault(); + this.model.get('fields') + .each(function (fieldModel) { + fieldModel.set('state', 'candidate', { reason: 'cancel' }); + }); + this.model.set('isActive', false); + }, + + /** + * + */ + buildToolbar: function () { + var $toolbar; + $toolbar = $(Drupal.theme('editEntityToolbarContainer', { + id: 'edit-entity-toolbar' + })); + + $toolbar + .find('.edit-toolbar') + // Append the "ops" toolgroup into the toolbar. + .append(Drupal.theme('editToolgroup', { + classes: 'ops', + buttons: [ + { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, + { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } + ] + })); + + // Give the toolbar a sensible starting position so that it doesn't + // animiate on to the screen from a far off corner. + $toolbar + .css({ + left: this.$entity.offset().left, + top: this.$entity.offset().top + }); + + this.setElement($toolbar); + }, + + /** + * + */ + startEdit: function () { + this.$el.addClass('edit-editing'); + }, + + /** + * + */ + startHighlight: function () { + // Retrieve the label to show for this field. + var label = this.model.get('label'); + + this.$el + .addClass('edit-highlighted') + .find('.edit-toolbar') + // Append the "info" toolgroup into the toolbar. + .append(Drupal.theme('editToolgroup', { + classes: 'info edit-animate-only-background-and-padding', + buttons: [ + { label: label, classes: 'blank-button label' } + ] + })); + + // Animations. + var that = this; + setTimeout(function () { + that.show('info'); + }, 0); + }, + + /** + * Indicates in the 'info' toolgroup that we're waiting for a server reponse. + * + * Prevents flickering loading indicator by only showing it after 0.6 seconds + * and if it is shown, only hiding it after another 0.6 seconds. + * + * @param Boolean enabled + * Whether the loading indicator should be displayed or not. + */ + setLoadingIndicator: function (enabled) { + var that = this; + if (enabled) { + this._loader = setTimeout(function() { + that.addClass('info', 'loading'); + that._loaderVisibleStart = new Date().getTime(); + }, 600); + } + else { + var currentTime = new Date().getTime(); + clearTimeout(this._loader); + if (this._loaderVisibleStart) { + setTimeout(function() { + that.removeClass('info', 'loading'); + }, this._loaderVisibleStart + 600 - currentTime); + } + this._loader = null; + this._loaderVisibleStart = 0; + } + }, + + /** + * Adds classes to a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + addClass: function (toolgroup, classes) { + this._find(toolgroup).addClass(classes); + }, + + /** + * Removes classes from a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + removeClass: function (toolgroup, classes) { + this._find(toolgroup).removeClass(classes); + }, + + /** + * Finds a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + _find: function (toolgroup) { + return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); + }, + + /** + * Shows a toolgroup. + * + * @param String toolgroup + * A toolgroup name. + */ + show: function (toolgroup) { + this._find(toolgroup).removeClass('edit-animate-invisible'); + } +}); + +})(jQuery, Backbone, Drupal, Drupal.debounce); diff --git a/core/modules/edit/js/views/EntityView.js b/core/modules/edit/js/views/EntityView.js deleted file mode 100644 index b41d23b..0000000 --- a/core/modules/edit/js/views/EntityView.js +++ /dev/null @@ -1,36 +0,0 @@ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -Drupal.edit = Drupal.edit || {}; - -$.extend(Drupal.edit, { - - /** - * - */ - EntityView: Backbone.View.extend({ - - events: {}, - - /** - * Implements Backbone Views' initialize() function. - */ - initialize: function (options) { - this.strings = this.options.strings; - this.model.on('change:isActive', this.render, this); - }, - - /** - * Implements Backbone.View.prototype.render(). - */ - render: function (model, value, options) { - var isActive = this.model.get('isActive'); - this.$el.toggleClass('edit-active', isActive); - - return this; - } - }) -}); - -}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/views/ToolbarView.js b/core/modules/edit/js/views/ToolbarView.js index df81a7a..08dcb78 100644 --- a/core/modules/edit/js/views/ToolbarView.js +++ b/core/modules/edit/js/views/ToolbarView.js @@ -12,16 +12,11 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ $field: null, - _loader: null, - _loaderVisibleStart: 0, - _id: null, events: { 'click.edit button.label': 'onClickInfoLabel', - 'mouseleave.edit': 'onMouseLeave', - 'click.edit button.field-save': 'onClickSave', - 'click.edit button.field-close': 'onClickClose' + 'mouseleave.edit': 'onMouseLeave' }, /** @@ -31,13 +26,8 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ this.$field = options.$field; this.editorView = options.editorView; - this._loader = null; - this._loaderVisibleStart = 0; - // Generate a DOM-compatible ID for the form container DOM element. this._id = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_'); - - this.model.on('change:state', this.stateChange, this); }, /** @@ -66,81 +56,6 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ }, /** - * Determines the actions to take given a change of state. - * - * @param Drupal.edit.FieldModel model - * @param String state - * The state of the associated field. One of Drupal.edit.FieldModel.states. - */ - stateChange: function (model, state) { - var from = model.previous('state'); - var to = state; - switch (to) { - case 'inactive': - if (from) { - this.remove(); - if (this.model.get('editor') !== 'form') { - Backbone.syncDirectCleanUp(); - } - } - break; - case 'candidate': - if (from === 'inactive') { - this.render(); - } - else { - if (this.model.get('editor') !== 'form') { - Backbone.syncDirectCleanUp(); - } - // 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(); - } - } - break; - case 'highlighted': - // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title). - this.startHighlight(); - break; - case 'activating': - this.setLoadingIndicator(true); - break; - case 'active': - this.startEdit(); - this.setLoadingIndicator(false); - if (this.getEditUISetting('fullWidthToolbar')) { - this.$el.addClass('edit-toolbar-fullwidth'); - } - - if (this.getEditUISetting('padding')) { - this._pad(); - } - if (this.getEditUISetting('unifiedToolbar')) { - this.insertWYSIWYGToolGroups(); - } - break; - case 'changed': - this.$el - .find('button.save') - .addClass('blue-button') - .removeClass('gray-button'); - break; - case 'saving': - this.setLoadingIndicator(true); - break; - case 'saved': - this.setLoadingIndicator(false); - break; - case 'invalid': - this.setLoadingIndicator(false); - break; - } - }, - - /** * Redirects the click.edit-event to the editor DOM element. * * @param jQuery event @@ -168,101 +83,6 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ }, /** - * Set the model state to 'saving' when the save button is clicked. - * - * @param jQuery event - */ - onClickSave: function (event) { - event.stopPropagation(); - event.preventDefault(); - this.model.set('state', 'saving'); - }, - - /** - * Sets the model state to candidate when the cancel button is clicked. - * - * @param jQuery event - */ - onClickClose: function (event) { - event.stopPropagation(); - event.preventDefault(); - this.model.set('state', 'candidate', { reason: 'cancel' }); - }, - - /** - * Indicates in the 'info' toolgroup that we're waiting for a server reponse. - * - * Prevents flickering loading indicator by only showing it after 0.6 seconds - * and if it is shown, only hiding it after another 0.6 seconds. - * - * @param Boolean enabled - * Whether the loading indicator should be displayed or not. - */ - setLoadingIndicator: function (enabled) { - var that = this; - if (enabled) { - this._loader = setTimeout(function() { - that.addClass('info', 'loading'); - that._loaderVisibleStart = new Date().getTime(); - }, 600); - } - else { - var currentTime = new Date().getTime(); - clearTimeout(this._loader); - if (this._loaderVisibleStart) { - setTimeout(function() { - that.removeClass('info', 'loading'); - }, this._loaderVisibleStart + 600 - currentTime); - } - this._loader = null; - this._loaderVisibleStart = 0; - } - }, - - /** - * - */ - startHighlight: function () { - // Retrieve the lavel to show for this field. - var label = this.model.get('label'); - - this.$el - .addClass('edit-highlighted') - .find('.edit-toolbar') - // Append the "info" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'info edit-animate-only-background-and-padding', - buttons: [ - { label: label, classes: 'blank-button label' } - ] - })); - - // Animations. - var that = this; - setTimeout(function () { - that.show('info'); - }, 0); - }, - - /** - * - */ - startEdit: function () { - this.$el - .addClass('edit-editing') - .find('.edit-toolbar') - // Append the "ops" toolgroup into the toolbar. - .append(Drupal.theme('editToolgroup', { - classes: 'ops', - buttons: [ - { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' }, - { label: '' + Drupal.t('Close') + '', classes: 'field-close close gray-button' } - ] - })); - this.show('ops'); - }, - - /** * Retrieves a setting of the editor-specific Edit UI integration. * * @param String setting @@ -368,46 +188,6 @@ Drupal.edit.ToolbarView = Backbone.View.extend({ */ getMainWysiwygToolgroupId: function () { return 'edit-wysiwyg-main-toolgroup-for-' + this._id; - }, - - /** - * Shows a toolgroup. - * - * @param String toolgroup - * A toolgroup name. - */ - show: function (toolgroup) { - this._find(toolgroup).removeClass('edit-animate-invisible'); - }, - - /** - * Adds classes to a toolgroup. - * - * @param String toolgroup - * A toolgroup name. - */ - addClass: function (toolgroup, classes) { - this._find(toolgroup).addClass(classes); - }, - - /** - * Removes classes from a toolgroup. - * - * @param String toolgroup - * A toolgroup name. - */ - removeClass: function (toolgroup, classes) { - this._find(toolgroup).removeClass(classes); - }, - - /** - * Finds a toolgroup. - * - * @param String toolgroup - * A toolgroup name. - */ - _find: function (toolgroup) { - return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup); } }); diff --git a/core/modules/editor/js/editor.js b/core/modules/editor/js/editor.js index 6483170..f07aa73 100644 --- a/core/modules/editor/js/editor.js +++ b/core/modules/editor/js/editor.js @@ -83,8 +83,12 @@ Drupal.behaviors.editor = { var $this = $(this); var activeFormatID = $this.val(); var field = behavior.findFieldForFormatSelector($this); - - Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); + if ('activeFormatID' in settings.editor.formats) { + Drupal.editorDetach(field, settings.editor.formats[activeFormatID], trigger); + } + else { + console.log('%c editor.js: The format ' + activeFormatID + ' does not have an editor.', 'background-color: red; color: white;'); + } }); },