diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css index 1ceea82..37e7578 100644 --- a/core/modules/edit/css/edit.css +++ b/core/modules/edit/css/edit.css @@ -121,13 +121,6 @@ */ } -[data-edit-entity].edit-active { - outline: 2px dotted red; -} - - - - /** * Edit mode: modal. */ diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 221eec4..c3f0d78 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -73,11 +73,18 @@ function edit_library_info() { 'js' => array( // Core. $path . '/js/edit.js' => $options, + // Models. + $path . '/js/models/AppModel.js' => $options, + $path . '/js/models/EntityModel.js' => $options, + $path . '/js/models/FieldModel.js' => $options, // Views. - $path . '/js/views/propertyeditordecoration-view.js' => $options, - $path . '/js/views/contextuallink-view.js' => $options, - $path . '/js/views/modal-view.js' => $options, - $path . '/js/views/toolbar-view.js' => $options, + $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, // Backbone.sync implementation on top of Drupal forms. $path . '/js/backbone.drupalform.js' => $options, // Storage manager. diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index f89fc03..90e4fdd 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -196,8 +196,8 @@ Drupal.behaviors.edit = { // For now, this assumption is acceptable. var entity = that.collections.entities.where({id: entityId})[0]; - // The EditableModel stores the state of an editable entity field. - var editableModel = new Drupal.edit.EditableModel({ + // The FieldModel stores the state of an editable entity field. + var editableModel = new Drupal.edit.FieldModel({ entity: entity, $el: $element, editID: editID, @@ -281,476 +281,4 @@ Drupal.behaviors.edit = { } }; -$.extend(Drupal.edit, { - - /** - * - */ - AppModel: Backbone.Model.extend({ - defaults: { - activeEntity: null, - highlightedEditor: null, - activeEditor: null, - // Reference to a ModalView-instance if a transition requires confirmation. - activeModal: null - } - }), - - /** - * - */ - AppView: Backbone.View.extend({ - vie: null, - domService: null, - - // Configuration for state handling. - states: [], - activeEditorStates: [], - singleEditorStates: [], - - // State. - $entityElements: null, - editables: [], - entityViews: [], - - /** - * Implements Backbone Views' initialize() function. - */ - initialize: function (options) { - this.entitiesCollection = options.entitiesCollection; - - _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange'); - - // Instantiate configuration for state handling. - this.states = [ - null, 'inactive', 'candidate', 'highlighted', - 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' - ]; - this.activeEditorStates = ['activating', 'active']; - this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); - - this.entitiesCollection.on('change:isActive', this.appStateChange, this); - }, - - /** - * Handles setup/teardown and state changes when the active entity changes. - */ - appStateChange: function (entityModel, isActive) { - var app = this; - if (isActive) { - // Move all fields of this entity from the 'inactive' state to the - // 'candidate' state. - entityModel.get('fields').each(function (fieldModel) { - // First, set up decoration views. - app.decorate(fieldModel); - // Second, change the field's state. - fieldModel.set('state', 'candidate'); - }); - } - else { - // Move all fields of this entity from whatever state they are in to - // the 'inactive' state. - entityModel.get('fields').each(function (fieldModel) { - // First, change the field's state. - fieldModel.set('state', 'inactive', { reason: 'stop' }); - // Second, tear down decoration views. - app.undecorate(fieldModel); - }); - } - }, - - /** - * Accepts or reject editor (PropertyEditor) state changes. - * - * This is what ensures that the app is in control of what happens. - * - * @param String from - * The previous state. - * @param String to - * The new state. - * @param null|Object context - * The context that is trying to trigger the state change. - * @param Function callback - * The callback function that should receive the state acceptance result. - */ - acceptEditorStateChange: function(from, to, context, callback) { - var accept = true; - - console.log("accept? %s → %s (reason: %s)", from, to, (context && context.reason) ? context.reason : 'NONE'); - - // If the app is in view mode, then reject all state changes except for - // those to 'inactive'. - if (context && context.reason === 'stop') { - if (from === 'candidate' && to === 'inactive') { - accept = true; - } - } - // Handling of edit mode state changes is more granular. - else { - // In general, enforce the states sequence. Disallow going back from a - // "later" state to an "earlier" state, except in explicitly allowed - // cases. - if (_.indexOf(this.states, from) > _.indexOf(this.states, to)) { - accept = false; - // Allow: activating/active -> candidate. - // Necessary to stop editing a property. - if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { - accept = true; - } - // Allow: changed/invalid -> candidate. - // Necessary to stop editing a property when it is changed or invalid. - else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { - accept = true; - } - // Allow: highlighted -> candidate. - // Necessary to stop highlighting a property. - else if (from === 'highlighted' && to === 'candidate') { - accept = true; - } - // Allow: saved -> candidate. - // Necessary when successfully saved a property. - else if (from === 'saved' && to === 'candidate') { - accept = true; - } - // Allow: invalid -> saving. - // Necessary to be able to save a corrected, invalid property. - else if (from === 'invalid' && to === 'saving') { - accept = true; - } - } - - // If it's not against the general principle, then here are more - // disallowed cases to check. - if (accept) { - // Ensure only one editor (field) at a time may be higlighted or active. - if (from === 'candidate' && _.indexOf(this.singleEditorStates, to) !== -1) { - if (this.model.get('highlightedEditor') || this.model.get('activeEditor')) { - accept = false; - } - } - // Reject going from activating/active to candidate because of a - // mouseleave. - else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { - if (context && context.reason === 'mouseleave') { - accept = false; - } - } - // When attempting to stop editing a changed/invalid property, ask for - // confirmation. - else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { - if (context && context.reason === 'mouseleave') { - accept = false; - } - else { - // Check whether the transition has been confirmed? - if (context && context.confirmed) { - accept = true; - } - // Confirm this transition. - else { - // Do not accept this change right now, instead open a modal - // that will ask the user to confirm his choice. - accept = false; - // The callback will be called from the helper function. - this._confirmStopEditing(callback); - } - } - } - } - } - - return accept; - }, - - // @todo rename to decorateField - decorate: function (fieldModel) { - var editID = fieldModel.get('editID'); - var $el = fieldModel.get('$el'); - - // Create a new Editor. - var editorName = fieldModel.get('editor'); - var editorView = new Drupal.edit.editors[editorName]({ - el: $el, - model: fieldModel - }); - - // Toolbars are rendered "on-demand" (highlighting or activating). - // They are a sibling element before the editor's DOM element. - var toolbarView = new Drupal.edit.ToolbarView({ - model: fieldModel, - $field: $el, - editorView: editorView - }); - - // Decorate the editor's DOM element depending on its state. - var decorationView = new Drupal.edit.PropertyEditorDecorationView({ - el: $el, - model: fieldModel, - editorView: editorView, - toolbarId: toolbarView.getId() - }); - - // Create references in the field model; necessary for undecorate() and - // necessary for some EditorView implementations. - fieldModel.set('editorView', editorView); - fieldModel.set('toolbarView', toolbarView); - fieldModel.set('decorationView', decorationView); - }, - - // @todo rename to undecorateField - undecorate: function (fieldModel) { - // Unbind event handlers; remove toolbar element; delete toolbar view. - var toolbarView = fieldModel.get('toolbarView'); - toolbarView.undelegateEvents(); - toolbarView.remove(); - fieldModel.unset('toolbarView'); - - // Unbind event handlers; delete decoration view. Don't remove the element - // because that would remove the field itself. - var decorationView = fieldModel.get('decorationView'); - decorationView.undelegateEvents(); - fieldModel.unset('decorationView'); - - // Unbind event handlers; delete editor view. Don't remove the element - // because that would remove the field itself. - var editorView = fieldModel.get('editorView'); - editorView.undelegateEvents(); - fieldModel.unset('editorView'); - }, - - /** - * Asks the user to confirm whether he wants to stop editing via a modal. - * - * @see acceptEditorStateChange() - */ - _confirmStopEditing: function () { - // Only instantiate if there isn't a modal instance visible yet. - if (!this.model.get('activeModal')) { - var that = this; - var modal = new Drupal.edit.ModalView({ - model: this.model, - message: Drupal.t('You have unsaved changes'), - buttons: [ - { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') }, - { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') } - ], - callback: function(action) { - // The active modal has been removed. - that.model.set('activeModal', null); - // Set the state that matches the user's action. - var targetState = (action === 'discard') ? 'candidate' : 'saving'; - that.model.get('activeEditor').set('state', 'candidate', { confirmed: true }); - } - }); - this.model.set('activeModal', modal); - // The modal will set the activeModal property on the model when rendering - // to prevent multiple modals from being instantiated. - modal.render(); - } - }, - - /** - * Reacts to editor (PropertyEditor) state changes; tracks global state. - * - * @param from - * The previous state. - * @param to - * The new state. - * @param editor - * The PropertyEditor widget object. - */ - editorStateChange: function(fieldModel, state) { - var from = fieldModel.previous('state'); - var to = state; - - console.log("%c [APP] %s → %s %s", "background-color: black; color: white", from, to, fieldModel.get('editID')); - - // Keep track of the highlighted editor in the global state. - if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== fieldModel) { - this.model.set('highlightedEditor', fieldModel); - } - else if (this.model.get('highlightedEditor') === fieldModel && to === 'candidate') { - this.model.set('highlightedEditor', null); - } - - // Keep track of the active editor in the global state. - if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== fieldModel) { - this.model.set('activeEditor', fieldModel); - } - else if (this.model.get('activeEditor') === fieldModel && to === 'candidate') { - // Discarded if it transitions from a changed state to 'candidate'. - if (from === 'changed' || from === 'invalid') { - // @todo FIX THIS! - // Retrieve the storage widget from DOM. - // var createStorageWidget = this.$el.data('DrupalCreateStorage'); - // Revert changes in the model, this will trigger the direct editable - // content to be reset and redrawn. - // createStorageWidget.revertChanges(fieldModel.options.entity); - } - this.model.set('activeEditor', null); - } - } - }), - - /** - * - */ - EntityModel: Backbone.Model.extend({ - defaults: { - // An entity ID, of the form "/", e.g. "node/1". - id: null, - // Indicates whether this instance of this entity is currently being - // edited. - isActive: false, - // A Drupal.edit.EditablesCollection for all fields of this entity. - fields: null - }, - initialize: function () { - this.set('fields', new Drupal.edit.EditablesCollection()); - } - }), - - /** - * - */ - 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; - } - }), - - /** - * - */ - // @todo rename to FieldModel? - EditableModel: Backbone.Model.extend({ - defaults: { - // @todo Try to get rid of this, but it's hard; see appStateChange() - $el: null, - - // Possible states: - // - inactive - // - candidate - // - highlighted - // - activating - // - active - // - changed - // - saving - // - saved - // - saved - // - invalid - // @see http://createjs.org/guide/#states - state: 'inactive', - // A Drupal.edit.EntityModel. Its "properties" attribute, which is an - // EditablesCollection, is automatically updated to include this - // EditableModel. - entity: null, - // A place to store any decoration views. - decorationViews: {}, - - - // - // Data set by the metadata callback. - // - // The ID of the in-place editor to use. - editor: null, - // The label to use. - label: null, - - // - // Data derived from the information in the DOM. - // - - // The edit ID, format: `::::`. - editID: null, - // The full HTML representation of this field (with the element that has - // the data-edit-id as the outer element). Used to propagate changes from - // this field instance to other instances of the same field. - html: null, - - // - // Callbacks. - // - - // Callback function for validating changes between states. Receives the - // previous state, new state, context, and a callback - acceptStateChange: null - }, - initialize: function () { - this.get('entity').get('fields').add(this); - this.on('change:state', function(model, state) { - console.log('%c [MOD] ' + this.previous('state') + ' → ' + state + ' ' + model.get('editID'), 'background-color: blue; color: white'); - }); - this.on('error', function(model, error) { - console.log('%c' + model.get("editID") + " " + error, 'color: orange'); - }); - }, - validate: function (attrs, options) { - // We only care about validating the 'state' attribute. - if (!_.has(attrs, 'state')) { - return; - } - - var current = this.get('state'); - var next = attrs.state; - if (current !== next) { - // Ensure it's a valid state. - if (_.indexOf(this.constructor.states, next) === -1) { - return '"' + next + '" is an invalid state'; - } - // Check if the acceptStateChange callback accepts it. - if (!this.get('acceptStateChange')(current, next, options)) { - return 'state change not accepted'; - } - } - }, - // @see VIE.prototype.EditService.prototype.getElementSubject() - // Parses the `/` part from the edit ID. - getEntityID: function() { - return this.get('editID').split('/').slice(0, 2).join('/'); - }, - // @see VIE.prototype.EditService.prototype.getElementPredicate() - // Parses the `//` part from the edit ID. - getFieldID: function() { - return this.get('editID').split('/').slice(2, 5).join('/'); - } - }, { - states: [ - 'inactive', 'candidate', 'highlighted', - 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' - ], - activeEditorStates: ['activating', 'active'], - singleEditorStates: ['highlighted', 'activating', 'active'] - }), - - // A collection of EditableModels. - // @todo link back to the entity at the collection level (not the collection element level)? - EditablesCollection: Backbone.Collection.extend({ - model: Drupal.edit.EditableModel - }), - - // @todo provide a base class for formEditor/directEditor/…, migrate them away - // from editor.js - EditorView: Backbone.View.extend({ - - }) -}); - })(jQuery, _, Backbone, Drupal, drupalSettings); diff --git a/core/modules/edit/js/editors/directEditor.js b/core/modules/edit/js/editors/directEditor.js index 1a269d8..eaced30 100644 --- a/core/modules/edit/js/editors/directEditor.js +++ b/core/modules/edit/js/editors/directEditor.js @@ -14,14 +14,14 @@ Drupal.edit.editors.direct = Backbone.View.extend({ /** * Implements getEditUISettings() method. */ - getEditUISettings: function() { + getEditUISettings: function () { return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; }, /** - * + * Implements Backbone.View.prototype.initialize(). */ - _initialize: function() { + initialize: function () { var that = this; // Sets the state to 'changed' whenever the content has changed. @@ -41,9 +41,13 @@ Drupal.edit.editors.direct = Backbone.View.extend({ }, /** - * Makes this PropertyEditor widget react to state changes. + * Determines the actions to take given a change of state. + * + * @param Backbone.Model model + * @param String state + * The state of an editable element. Used to determine display and behavior. */ - stateChange: function(model, state) { + stateChange: function (model, state) { var from = model.previous('state'); var to = state; switch (value) { diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js index b62bfe3..3ab135e 100644 --- a/core/modules/edit/js/editors/formEditor.js +++ b/core/modules/edit/js/editors/formEditor.js @@ -16,12 +16,12 @@ Drupal.edit.editors.form = Backbone.View.extend({ /** * Implements getEditUISettings() method. */ - getEditUISettings: function() { + getEditUISettings: function () { return { padding: false, unifiedToolbar: false, fullWidthToolbar: false }; }, /** - * + * Implements Backbone.View.prototype.initialize(). */ initialize: function (options) { this.model.on('change:state', this.stateChange, this); @@ -115,7 +115,7 @@ Drupal.edit.editors.form = Backbone.View.extend({ * glue code, along with the logic for inserting updated content as well as * showing validation error messages, the latter of which is certainly okay. */ - save: function() { + save: function () { var that = this; var editor = this; var editableEntity = this.el; @@ -180,9 +180,13 @@ Drupal.edit.editors.form = Backbone.View.extend({ }, /** - * Makes this PropertyEditor widget react to state changes. + * Determines the actions to take given a change of state. + * + * @param Backbone.Model model + * @param String state + * The state of an editable element. Used to determine display and behavior. */ - stateChange: function(model, state) { + stateChange: function (model, state) { var from = model.previous('state'); var to = state; switch (to) { diff --git a/core/modules/edit/js/models/AppModel.js b/core/modules/edit/js/models/AppModel.js new file mode 100644 index 0000000..8699a92 --- /dev/null +++ b/core/modules/edit/js/models/AppModel.js @@ -0,0 +1,23 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + AppModel: Backbone.Model.extend({ + defaults: { + activeEntity: null, + highlightedEditor: null, + activeEditor: null, + // Reference to a ModalView-instance if a transition requires confirmation. + activeModal: null + } + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js new file mode 100644 index 0000000..ff58421 --- /dev/null +++ b/core/modules/edit/js/models/EntityModel.js @@ -0,0 +1,28 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + EntityModel: Backbone.Model.extend({ + defaults: { + // An entity ID, of the form "/", e.g. "node/1". + id: null, + // Indicates whether this instance of this entity is currently being + // edited. + isActive: false, + // A Drupal.edit.EditablesCollection for all fields of this entity. + fields: null + }, + initialize: function () { + this.set('fields', new Drupal.edit.EditablesCollection()); + } + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js new file mode 100644 index 0000000..6968648 --- /dev/null +++ b/core/modules/edit/js/models/FieldModel.js @@ -0,0 +1,119 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * @todo rename to FieldModel? + */ + FieldModel: Backbone.Model.extend({ + defaults: { + // @todo Try to get rid of this, but it's hard; see appStateChange() + $el: null, + + // Possible states: + // - inactive + // - candidate + // - highlighted + // - activating + // - active + // - changed + // - saving + // - saved + // - saved + // - invalid + // @see http://createjs.org/guide/#states + state: 'inactive', + // A Drupal.edit.EntityModel. Its "properties" attribute, which is an + // EditablesCollection, is automatically updated to include this + // FieldModel. + entity: null, + // A place to store any decoration views. + decorationViews: {}, + + + // + // Data set by the metadata callback. + // + // The ID of the in-place editor to use. + editor: null, + // The label to use. + label: null, + + // + // Data derived from the information in the DOM. + // + + // The edit ID, format: `::::`. + editID: null, + // The full HTML representation of this field (with the element that has + // the data-edit-id as the outer element). Used to propagate changes from + // this field instance to other instances of the same field. + html: null, + + // + // Callbacks. + // + + // Callback function for validating changes between states. Receives the + // previous state, new state, context, and a callback + acceptStateChange: null + }, + initialize: function () { + this.get('entity').get('fields').add(this); + this.on('change:state', function(model, state) { + console.log('%c [MOD] ' + this.previous('state') + ' → ' + state + ' ' + model.get('editID'), 'background-color: blue; color: white'); + }); + this.on('error', function(model, error) { + console.log('%c' + model.get("editID") + " " + error, 'color: orange'); + }); + }, + validate: function (attrs, options) { + // We only care about validating the 'state' attribute. + if (!_.has(attrs, 'state')) { + return; + } + + var current = this.get('state'); + var next = attrs.state; + if (current !== next) { + // Ensure it's a valid state. + if (_.indexOf(this.constructor.states, next) === -1) { + return '"' + next + '" is an invalid state'; + } + // Check if the acceptStateChange callback accepts it. + if (!this.get('acceptStateChange')(current, next, options)) { + return 'state change not accepted'; + } + } + }, + // @see VIE.prototype.EditService.prototype.getElementSubject() + // Parses the `/` part from the edit ID. + getEntityID: function() { + return this.get('editID').split('/').slice(0, 2).join('/'); + }, + // @see VIE.prototype.EditService.prototype.getElementPredicate() + // Parses the `//` part from the edit ID. + getFieldID: function() { + return this.get('editID').split('/').slice(2, 5).join('/'); + } + }, { + states: [ + 'inactive', 'candidate', 'highlighted', + 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' + ], + activeEditorStates: ['activating', 'active'], + singleEditorStates: ['highlighted', 'activating', 'active'] + }), + + // A collection of FieldModels. + // @todo link back to the entity at the collection level (not the collection element level)? + EditablesCollection: Backbone.Collection.extend({ + model: Drupal.edit.FieldModel + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js new file mode 100644 index 0000000..d45a19f --- /dev/null +++ b/core/modules/edit/js/views/AppView.js @@ -0,0 +1,306 @@ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit = Drupal.edit || {}; + +$.extend(Drupal.edit, { + + /** + * + */ + AppView: Backbone.View.extend({ + vie: null, + domService: null, + + // Configuration for state handling. + states: [], + activeEditorStates: [], + singleEditorStates: [], + + // State. + $entityElements: null, + editables: [], + entityViews: [], + + /** + * Implements Backbone Views' initialize() function. + */ + initialize: function (options) { + this.entitiesCollection = options.entitiesCollection; + + _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange'); + + // Instantiate configuration for state handling. + this.states = [ + null, 'inactive', 'candidate', 'highlighted', + 'activating', 'active', 'changed', 'saving', 'saved', 'invalid' + ]; + this.activeEditorStates = ['activating', 'active']; + this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates); + + this.entitiesCollection.on('change:isActive', this.appStateChange, this); + }, + + /** + * Handles setup/teardown and state changes when the active entity changes. + */ + appStateChange: function (entityModel, isActive) { + var app = this; + if (isActive) { + // Move all fields of this entity from the 'inactive' state to the + // 'candidate' state. + entityModel.get('fields').each(function (fieldModel) { + // First, set up decoration views. + app.decorate(fieldModel); + // Second, change the field's state. + fieldModel.set('state', 'candidate'); + }); + } + else { + // Move all fields of this entity from whatever state they are in to + // the 'inactive' state. + entityModel.get('fields').each(function (fieldModel) { + // First, change the field's state. + fieldModel.set('state', 'inactive', { reason: 'stop' }); + // Second, tear down decoration views. + app.undecorate(fieldModel); + }); + } + }, + + /** + * Accepts or reject editor (PropertyEditor) state changes. + * + * This is what ensures that the app is in control of what happens. + * + * @param String from + * The previous state. + * @param String to + * The new state. + * @param null|Object context + * The context that is trying to trigger the state change. + * @param Function callback + * The callback function that should receive the state acceptance result. + */ + acceptEditorStateChange: function(from, to, context, callback) { + var accept = true; + + console.log("accept? %s → %s (reason: %s)", from, to, (context && context.reason) ? context.reason : 'NONE'); + + // If the app is in view mode, then reject all state changes except for + // those to 'inactive'. + if (context && context.reason === 'stop') { + if (from === 'candidate' && to === 'inactive') { + accept = true; + } + } + // Handling of edit mode state changes is more granular. + else { + // In general, enforce the states sequence. Disallow going back from a + // "later" state to an "earlier" state, except in explicitly allowed + // cases. + if (_.indexOf(this.states, from) > _.indexOf(this.states, to)) { + accept = false; + // Allow: activating/active -> candidate. + // Necessary to stop editing a property. + if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { + accept = true; + } + // Allow: changed/invalid -> candidate. + // Necessary to stop editing a property when it is changed or invalid. + else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { + accept = true; + } + // Allow: highlighted -> candidate. + // Necessary to stop highlighting a property. + else if (from === 'highlighted' && to === 'candidate') { + accept = true; + } + // Allow: saved -> candidate. + // Necessary when successfully saved a property. + else if (from === 'saved' && to === 'candidate') { + accept = true; + } + // Allow: invalid -> saving. + // Necessary to be able to save a corrected, invalid property. + else if (from === 'invalid' && to === 'saving') { + accept = true; + } + } + + // If it's not against the general principle, then here are more + // disallowed cases to check. + if (accept) { + // Ensure only one editor (field) at a time may be higlighted or active. + if (from === 'candidate' && _.indexOf(this.singleEditorStates, to) !== -1) { + if (this.model.get('highlightedEditor') || this.model.get('activeEditor')) { + accept = false; + } + } + // Reject going from activating/active to candidate because of a + // mouseleave. + else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') { + if (context && context.reason === 'mouseleave') { + accept = false; + } + } + // When attempting to stop editing a changed/invalid property, ask for + // confirmation. + else if ((from === 'changed' || from === 'invalid') && to === 'candidate') { + if (context && context.reason === 'mouseleave') { + accept = false; + } + else { + // Check whether the transition has been confirmed? + if (context && context.confirmed) { + accept = true; + } + // Confirm this transition. + else { + // Do not accept this change right now, instead open a modal + // that will ask the user to confirm his choice. + accept = false; + // The callback will be called from the helper function. + this._confirmStopEditing(callback); + } + } + } + } + } + + return accept; + }, + + // @todo rename to decorateField + decorate: function (fieldModel) { + var editID = fieldModel.get('editID'); + var $el = fieldModel.get('$el'); + + // Create a new Editor. + var editorName = fieldModel.get('editor'); + var editorView = new Drupal.edit.editors[editorName]({ + el: $el, + model: fieldModel + }); + + // Toolbars are rendered "on-demand" (highlighting or activating). + // They are a sibling element before the editor's DOM element. + var toolbarView = new Drupal.edit.ToolbarView({ + model: fieldModel, + $field: $el, + editorView: editorView + }); + + // Decorate the editor's DOM element depending on its state. + var decorationView = new Drupal.edit.PropertyEditorDecorationView({ + el: $el, + model: fieldModel, + editorView: editorView, + toolbarId: toolbarView.getId() + }); + + // Create references in the field model; necessary for undecorate() and + // necessary for some EditorView implementations. + fieldModel.set('editorView', editorView); + fieldModel.set('toolbarView', toolbarView); + fieldModel.set('decorationView', decorationView); + }, + + // @todo rename to undecorateField + undecorate: function (fieldModel) { + // Unbind event handlers; remove toolbar element; delete toolbar view. + var toolbarView = fieldModel.get('toolbarView'); + toolbarView.undelegateEvents(); + toolbarView.remove(); + fieldModel.unset('toolbarView'); + + // Unbind event handlers; delete decoration view. Don't remove the element + // because that would remove the field itself. + var decorationView = fieldModel.get('decorationView'); + decorationView.undelegateEvents(); + fieldModel.unset('decorationView'); + + // Unbind event handlers; delete editor view. Don't remove the element + // because that would remove the field itself. + var editorView = fieldModel.get('editorView'); + editorView.undelegateEvents(); + fieldModel.unset('editorView'); + }, + + /** + * Asks the user to confirm whether he wants to stop editing via a modal. + * + * @see acceptEditorStateChange() + */ + _confirmStopEditing: function () { + // Only instantiate if there isn't a modal instance visible yet. + if (!this.model.get('activeModal')) { + var that = this; + var modal = new Drupal.edit.ModalView({ + model: this.model, + message: Drupal.t('You have unsaved changes'), + buttons: [ + { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') }, + { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') } + ], + callback: function(action) { + // The active modal has been removed. + that.model.set('activeModal', null); + // Set the state that matches the user's action. + var targetState = (action === 'discard') ? 'candidate' : 'saving'; + that.model.get('activeEditor').set('state', 'candidate', { confirmed: true }); + } + }); + this.model.set('activeModal', modal); + // The modal will set the activeModal property on the model when rendering + // to prevent multiple modals from being instantiated. + modal.render(); + } + }, + + /** + * Reacts to editor (PropertyEditor) state changes; tracks global state. + * + * @param from + * The previous state. + * @param to + * The new state. + * @param editor + * The PropertyEditor widget object. + */ + editorStateChange: function(fieldModel, state) { + var from = fieldModel.previous('state'); + var to = state; + + console.log("%c [APP] %s → %s %s", "background-color: black; color: white", from, to, fieldModel.get('editID')); + + // Keep track of the highlighted editor in the global state. + if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== fieldModel) { + this.model.set('highlightedEditor', fieldModel); + } + else if (this.model.get('highlightedEditor') === fieldModel && to === 'candidate') { + this.model.set('highlightedEditor', null); + } + + // Keep track of the active editor in the global state. + if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== fieldModel) { + this.model.set('activeEditor', fieldModel); + } + else if (this.model.get('activeEditor') === fieldModel && to === 'candidate') { + // Discarded if it transitions from a changed state to 'candidate'. + if (from === 'changed' || from === 'invalid') { + // @todo FIX THIS! + // Retrieve the storage widget from DOM. + // var createStorageWidget = this.$el.data('DrupalCreateStorage'); + // Revert changes in the model, this will trigger the direct editable + // content to be reset and redrawn. + // createStorageWidget.revertChanges(fieldModel.options.entity); + } + this.model.set('activeEditor', null); + } + } + }) +}); + +}(jQuery, _, Backbone, Drupal)); diff --git a/core/modules/edit/js/views/ContextualLinkView.js b/core/modules/edit/js/views/ContextualLinkView.js new file mode 100644 index 0000000..a3d4650 --- /dev/null +++ b/core/modules/edit/js/views/ContextualLinkView.js @@ -0,0 +1,78 @@ +/** + * @file + * A Backbone View that a dynamic contextual link. + */ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.ContextualLinkView = Backbone.View.extend({ + + entity: null, + + events: { + 'click .quick-edit a': 'onClick' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - appModel: the application state model + * - strings: the strings for the "Quick edit" link + */ + initialize: function (options) { + this.appModel = options.appModel; + this.strings = options.strings; + + // Build the DOM elements. + this.$el + .find('li.node-edit, li.taxonomy-edit, li.comment-edit, li.custom-block-edit') + .before('
  • '); + + // Initial render. + this.render(); + + // Re-render whenever this entity's isActive attribute changes. + this.model.on('change:isActive', this.render, this); + + // Hide the contextual links whenever an in-place editor is active. + this.appModel.on('change:activeEditor', this.toggleContextualLinksVisibility, this); + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + var isActive = this.model.get('isActive'); + this.$el.find('.quick-edit a').text((!isActive) ? this.strings.quickEdit : this.strings.stopQuickEdit); + return this; + }, + + /** + * Equates clicks anywhere on the overlay to closing the active editor. + * + * @param jQuery event + */ + onClick: function (event) { + event.preventDefault(); + + this.model.set('isActive', !this.model.get('isActive')); + }, + + /** + * Hides the contextual links if an editor is active. + * + * @param Drupal.edit.models.EditAppModel model + * An EditAppModel model. + * @param jQuery|null activeEditor + * The active in-place editor (jQuery object) or, if none, null. + */ + toggleContextualLinksVisibility: function (model, activeEditor) { + this.$el.parents('.contextual').toggle(activeEditor === null); + } + +}); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js new file mode 100644 index 0000000..49bd4cb --- /dev/null +++ b/core/modules/edit/js/views/EditorView.js @@ -0,0 +1,17 @@ +(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/EntityView.js b/core/modules/edit/js/views/EntityView.js new file mode 100644 index 0000000..b41d23b --- /dev/null +++ b/core/modules/edit/js/views/EntityView.js @@ -0,0 +1,36 @@ +(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/ModalView.js b/core/modules/edit/js/views/ModalView.js new file mode 100644 index 0000000..15497cc --- /dev/null +++ b/core/modules/edit/js/views/ModalView.js @@ -0,0 +1,80 @@ +/** + * @file + * A Backbone View that provides an interactive modal. + */ +(function ($, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.ModalView = Backbone.View.extend({ + + message: null, + buttons: null, + callback: null, + $elementsToHide: null, + + events: { + 'click button': 'onButtonClick' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - message: a message to show in the modal. + * - buttons: a set of buttons with 'action's defined, ready to be passed to + * Drupal.theme.editButtons(). + * - callback: a callback that will receive the 'action' of the clicked + * button. + * + * @see Drupal.theme.editModal() + * @see Drupal.theme.editButtons() + */ + initialize: function (options) { + this.message = options.message; + this.buttons = options.buttons; + this.callback = options.callback; + }, + + /** + * Implements Backbone.View.prototype.render(). + */ + render: function () { + this.setElement(Drupal.theme('editModal', {})); + this.$el.appendTo('body'); + // Template. + this.$('.main p').text(this.message); + var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons})); + this.$('.actions').append($actions); + + // Show the modal with an animation. + var that = this; + setTimeout(function() { + that.$el.removeClass('edit-animate-invisible'); + }, 0); + }, + + /** + * Passes the clicked button action to the callback; closes the modal. + * + * @param jQuery event + */ + onButtonClick: function (event) { + event.stopPropagation(); + event.preventDefault(); + + // Remove after animation. + var that = this; + this.$el + .addClass('edit-animate-invisible') + .on(Drupal.edit.util.constants.transitionEnd, function(e) { + that.remove(); + }); + + var action = $(event.target).attr('data-edit-modal-action'); + return this.callback(action); + } +}); + +})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/PropertyEditorDecorationView.js b/core/modules/edit/js/views/PropertyEditorDecorationView.js new file mode 100644 index 0000000..4cfcdb7 --- /dev/null +++ b/core/modules/edit/js/views/PropertyEditorDecorationView.js @@ -0,0 +1,393 @@ +/** + * @file + * A Backbone View that decorates a Property Editor widget. + * + * It listens to state changes of the property editor. + */ +(function ($, Backbone, Drupal) { + +"use strict"; + +Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ + toolbarId: null, + + _widthAttributeIsEmpty: null, + + events: { + 'mouseenter.edit' : 'onMouseEnter', + 'mouseleave.edit' : 'onMouseLeave', + 'click': 'onClick', + 'tabIn.edit': 'onMouseEnter', + 'tabOut.edit': 'onMouseLeave' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + * + * @param Object options + * An object with the following keys: + * - editor: the editor object with an 'options' object that has these keys: + * * entity: the VIE entity for the property. + * * property: the predicate of the property. + * * widget: the parent EditableEntity widget. + * * editorName: the name of the PropertyEditor widget + * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. + */ + initialize: function (options) { + this.editorView = options.editorView; + + this.toolbarId = options.toolbarId; + + this.model.on('change:state', this.stateChange, this); + }, + + /** + * Listens to editor state changes. + * + * @param Backbone.Model 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': + if (from !== null) { + this.undecorate(); + if (from === 'invalid') { + this._removeValidationErrors(); + } + } + break; + case 'candidate': + this.decorate(); + if (from !== 'inactive') { + this.stopHighlight(); + if (from !== 'highlighted') { + this.stopEdit(); + if (from === 'invalid') { + this._removeValidationErrors(); + } + } + } + break; + case 'highlighted': + this.startHighlight(); + break; + case 'activating': + // NOTE: this state is not used by every editor! It's only used by those + // that need to interact with the server. + this.prepareEdit(); + break; + case 'active': + if (from !== 'activating') { + this.prepareEdit(); + } + this.startEdit(); + break; + case 'changed': + break; + case 'saving': + if (from === 'invalid') { + this._removeValidationErrors(); + } + break; + case 'saved': + break; + case 'invalid': + break; + } + }, + + /** + * Starts hover; transitions to 'highlight' state. + * + * @param jQuery event + */ + onMouseEnter: function (event) { + var that = this; + this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { + that.model.set('state', 'highlighted'); + event.stopPropagation(); + }); + }, + + /** + * Stops hover; transitions to 'candidate' state. + * + * @param jQuery event + */ + onMouseLeave: function (event) { + var that = this; + this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { + that.model.set('state', 'candidate', { reason: 'mouseleave' }); + event.stopPropagation(); + }); + }, + + /** + * Transition to 'activating' stage. + * + * @param jQuery event + */ + onClick: function (event) { + this.model.set('state', 'activating'); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Adds classes used to indicate an elements editable state. + */ + decorate: function () { + this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); + }, + + /** + * Removes classes used to indicate an elements editable state. + */ + undecorate: function () { + this.$el.removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); + }, + + /** + * Adds that class that indicates that an element is highlighted. + */ + startHighlight: function () { + // Animations. + var that = this; + // Use a timeout to grab the next available animation frame. + setTimeout(function () { + that.$el.addClass('edit-highlighted'); + }, 0); + }, + + /** + * Removes the class that indicates that an element is highlighted. + */ + stopHighlight: function () { + this.$el.removeClass('edit-highlighted'); + }, + + /** + * Removes the class that indicates that an element as editable. + */ + prepareEdit: function () { + this.$el.addClass('edit-editing'); + + // While editing, do not show any other editors. + $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); + }, + + /** + * Updates the display of the editable element once editing has begun. + */ + startEdit: function () { + if (this.getEditUISetting('padding')) { + this._pad(); + } + }, + + /** + * Removes the class that indicates that an element is being edited. + * + * Reapplies the class that indicates that a candidate editable element is + * again available to be edited. + */ + stopEdit: function () { + this.$el.removeClass('edit-highlighted edit-editing'); + + // Make the other editors show up again. + // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) + // Revisit this. + $('.edit-candidate').addClass('edit-editable'); + + if (this.getEditUISetting('padding')) { + this._unpad(); + } + }, + + /** + * Retrieves a setting of the editor-specific Edit UI integration. + * + * @param String setting + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function (setting) { + return Drupal.edit.util.getEditUISetting(this.editorView, setting); + }, + + /** + * Adds padding around the editable element in order to make it pop visually. + */ + _pad: function () { + var self = this; + + // Add 5px padding for readability. This means we'll freeze the current + // width and *then* add 5px padding, hence ensuring the padding is added "on + // the outside". + // 1) Freeze the width (if it's not already set); don't use animations. + if (this.$el[0].style.width === "") { + this._widthAttributeIsEmpty = true; + this.$el + .addClass('edit-animate-disable-width') + .css('width', this.$el.width()) + .css('background-color', this._getBgColor(this.$el)); + } + + // 2) Add padding; use animations. + var posProp = this._getPositionProperties(this.$el); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + self.$el.removeClass('edit-animate-disable-width'); + + // Pad the editable. + self.$el + .css({ + 'position': 'relative', + 'top': posProp.top - 5 + 'px', + 'left': posProp.left - 5 + 'px', + 'padding-top' : posProp['padding-top'] + 5 + 'px', + 'padding-left' : posProp['padding-left'] + 5 + 'px', + 'padding-right' : posProp['padding-right'] + 5 + 'px', + 'padding-bottom': posProp['padding-bottom'] + 5 + 'px', + 'margin-bottom': posProp['margin-bottom'] - 10 + 'px' + }); + }, 0); + }, + + /** + * Removes the padding around the element being edited when editing ceases. + */ + _unpad: function () { + var self = this; + + // 1) Set the empty width again. + if (this._widthAttributeIsEmpty) { + this.$el + .addClass('edit-animate-disable-width') + .css('width', '') + .css('background-color', ''); + } + + // 2) Remove padding; use animations (these will run simultaneously with) + // the fading out of the toolbar as its gets removed). + var posProp = this._getPositionProperties(this.$el); + setTimeout(function() { + // Re-enable width animations (padding changes affect width too!). + self.$el.removeClass('edit-animate-disable-width'); + + // Unpad the editable. + self.$el + .css({ + 'position': 'relative', + 'top': posProp.top + 5 + 'px', + 'left': posProp.left + 5 + 'px', + 'padding-top' : posProp['padding-top'] - 5 + 'px', + 'padding-left' : posProp['padding-left'] - 5 + 'px', + 'padding-right' : posProp['padding-right'] - 5 + 'px', + 'padding-bottom': posProp['padding-bottom'] - 5 + 'px', + 'margin-bottom': posProp['margin-bottom'] + 10 + 'px' + }); + }, 0); + }, + + /** + * Gets the background color of an element (or the inherited one). + * + * @param DOM $e + */ + _getBgColor: function ($e) { + var c; + + if ($e === null || $e[0].nodeName === 'HTML') { + // Fallback to white. + return 'rgb(255, 255, 255)'; + } + c = $e.css('background-color'); + // TRICKY: edge case for Firefox' "transparent" here; this is a + // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 + if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') { + return this._getBgColor($e.parent()); + } + return c; + }, + + /** + * Gets the top and left properties of an element. + * + * Convert extraneous values and information into numbers ready for + * subtraction. + * + * @param DOM $e + */ + _getPositionProperties: function ($e) { + var p, + r = {}, + props = [ + 'top', 'left', 'bottom', 'right', + 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', + 'margin-bottom' + ]; + + var propCount = props.length; + for (var i = 0; i < propCount; i++) { + p = props[i]; + r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10); + } + return r; + }, + + /** + * Replaces blank or 'auto' CSS "position: " values with "0px". + * + * @param String pos + * (optional) The value for a CSS position declaration. + */ + _replaceBlankPosition: function (pos) { + if (pos === 'auto' || !pos) { + pos = '0px'; + } + return pos; + }, + + /** + * Ignores hovering to/from the given closest element. + * + * When a hover occurs to/from another element, invoke the callback. + * + * @param jQuery event + * @param jQuery closest + * A jQuery-wrapped DOM element or compatibale jQuery input. The element + * whose mouseenter and mouseleave events should be ignored. + * @param Function callback + */ + _ignoreHoveringVia: function (event, closest, callback) { + if ($(event.relatedTarget).closest(closest).length > 0) { + event.stopPropagation(); + } + else { + callback(); + } + }, + + /** + * Removes validation error's markup changes, if any. + * + * Note: this only needs to happen for type=direct, because for type=direct, + * the property DOM element itself is modified; this is not the case for + * type=form. + */ + _removeValidationErrors: function () { + if (this.model.get('editor') !== 'form') { + this.$el + .removeClass('edit-validation-error') + .next('.edit-validation-errors') + .remove(); + } + } +}); + +})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/ToolbarView.js b/core/modules/edit/js/views/ToolbarView.js new file mode 100644 index 0000000..cc5e5ee --- /dev/null +++ b/core/modules/edit/js/views/ToolbarView.js @@ -0,0 +1,414 @@ +/** + * @file + * A Backbone View that provides an interactive toolbar (1 per property editor). + * + * It listens to state changes of the property editor. It also triggers state + * changes in response to user interactions with the toolbar, including saving. + */ +(function ($, _, Backbone, Drupal) { + +"use strict"; + +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' + }, + + /** + * Implements Backbone.View.prototype.initialize(). + */ + initialize: function (options) { + 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); + }, + + /** + * Implements Backbone.View.prototype.render(). + * + * Depending on whether the display property of the $el for which a + * toolbar is being inserted into the DOM, it will be inserted differently. + */ + render: function () { + // Render toolbar. + this.setElement($(Drupal.theme('editToolbarContainer', { + id: this._id + }))); + + // Insert in DOM. + if (this.$field.css('display') === 'inline') { + this.$el.prependTo(this.$field.offsetParent()); + var pos = this.$field.position(); + this.$el.css('left', pos.left).css('top', pos.top); + } + else { + this.$el.insertBefore(this.$field); + } + + return this; + }, + + /** + * Listens to editor state changes. + * + * @param Backbone.Model 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': + 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 + */ + onClickInfoLabel: function (event) { + event.stopPropagation(); + event.preventDefault(); + // Redirects the event to the editor DOM element. + this.$field.trigger('click.edit'); + }, + + /** + * Controls mouseleave events. + * + * A mouseleave to the editor doesn't matter; a mouseleave to something else + * counts as a mouseleave on the editor itself. + * + * @param jQuery event + */ + onMouseLeave: function (event) { + if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) { + this.$field.trigger('mouseleave.edit'); + } + event.stopPropagation(); + }, + + /** + * 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 + * + * @see Drupal.edit.util.getEditUISetting(). + */ + getEditUISetting: function (setting) { + return Drupal.edit.util.getEditUISetting(this.editorView, setting); + }, + + /** + * Adjusts the toolbar to accomodate padding on the PropertyEditor widget. + * + * @see PropertyEditorDecorationView._pad(). + */ + _pad: function () { + // The whole toolbar must move to the top when the property's DOM element + // is displayed inline. + if (this.$field.css('display') === 'inline') { + this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); + } + + // The toolbar must move to the top and the left. + var $hf = this.$el.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '6px', left: '-5px' }); + + if (this.getEditUISetting('fullWidthToolbar')) { + $hf.css({ width: this.$field.width() + 10 }); + } + }, + + /** + * Undoes the changes made by _pad(). + * + * @see PropertyEditorDecorationView._unpad(). + */ + _unpad: function () { + // Move the toolbar back to its original position. + var $hf = this.$el.find('.edit-toolbar-heightfaker'); + $hf.css({ bottom: '1px', left: '' }); + + if (this.getEditUISetting('fullWidthToolbar')) { + $hf.css({ width: '' }); + } + }, + + /** + * + */ + insertWYSIWYGToolGroups: function () { + this.$el + .find('.edit-toolbar') + .append(Drupal.theme('editToolgroup', { + id: this.getFloatedWysiwygToolgroupId(), + classes: 'wysiwyg-floated', + buttons: [] + })) + .append(Drupal.theme('editToolgroup', { + id: this.getMainWysiwygToolgroupId(), + classes: 'wysiwyg-main', + buttons: [] + })); + + // Animate the toolgroups into visibility. + var that = this; + setTimeout(function () { + that.show('wysiwyg-floated'); + that.show('wysiwyg-main'); + }, 0); + }, + + /** + * Retrieves the ID for this toolbar's container. + * + * Only used to make sane hovering behavior possible. + * + * @return String + * A string that can be used as the ID for this toolbar's container. + */ + getId: function () { + return 'edit-toolbar-for-' + this._id; + }, + + /** + * Retrieves the ID for this toolbar's floating WYSIWYG toolgroup. + * + * Used to provide an abstraction for any WYSIWYG editor to plug in. + * + * @return String + * A string that can be used as the ID. + */ + getFloatedWysiwygToolgroupId: function () { + return 'edit-wysiwyg-floated-toolgroup-for-' + this._id; + }, + + /** + * Retrieves the ID for this toolbar's main WYSIWYG toolgroup. + * + * Used to provide an abstraction for any WYSIWYG editor to plug in. + * + * @return String + * A string that can be used as the ID. + */ + 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); + } +}); + +})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/contextuallink-view.js b/core/modules/edit/js/views/contextuallink-view.js deleted file mode 100644 index 76ef876..0000000 --- a/core/modules/edit/js/views/contextuallink-view.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @file - * A Backbone View that a dynamic contextual link. - */ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -Drupal.edit.ContextualLinkView = Backbone.View.extend({ - - entity: null, - - events: { - 'click .quick-edit a': 'onClick' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - appModel: the application state model - * - strings: the strings for the "Quick edit" link - */ - initialize: function (options) { - this.appModel = options.appModel; - this.strings = options.strings; - - // Build the DOM elements. - this.$el - .find('li.node-edit, li.taxonomy-edit, li.comment-edit, li.custom-block-edit') - .before('
  • '); - - // Initial render. - this.render(); - - // Re-render whenever this entity's isActive attribute changes. - this.model.on('change:isActive', this.render, this); - - // Hide the contextual links whenever an in-place editor is active. - this.appModel.on('change:activeEditor', this.toggleContextualLinksVisibility, this); - }, - - /** - * Equates clicks anywhere on the overlay to clicking the active editor's (if - * any) "close" button. - * - * @param {Object} event - */ - onClick: function (event) { - event.preventDefault(); - - this.model.set('isActive', !this.model.get('isActive')); - }, - - /** - * Render the "Quick edit" contextual link. - */ - render: function () { - var isActive = this.model.get('isActive'); - this.$el.find('.quick-edit a').text((!isActive) ? this.strings.quickEdit : this.strings.stopQuickEdit); - return this; - }, - - /** - * Model change handler; hides the contextual links if an editor is active. - * - * @param Drupal.edit.models.EditAppModel model - * An EditAppModel model. - * @param jQuery|null activeEditor - * The active in-place editor (jQuery object) or, if none, null. - */ - toggleContextualLinksVisibility: function (model, activeEditor) { - this.$el.parents('.contextual').toggle(activeEditor === null); - } - -}); - -})(jQuery, _, Backbone, Drupal); diff --git a/core/modules/edit/js/views/modal-view.js b/core/modules/edit/js/views/modal-view.js deleted file mode 100644 index 51ec5ce..0000000 --- a/core/modules/edit/js/views/modal-view.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @file - * A Backbone View that provides an interactive modal. - */ -(function($, Backbone, Drupal) { - -"use strict"; - -Drupal.edit.ModalView = Backbone.View.extend({ - - message: null, - buttons: null, - callback: null, - $elementsToHide: null, - - events: { - 'click button': 'onButtonClick' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - message: a message to show in the modal. - * - buttons: a set of buttons with 'action's defined, ready to be passed to - * Drupal.theme.editButtons(). - * - callback: a callback that will receive the 'action' of the clicked - * button. - * - * @see Drupal.theme.editModal() - * @see Drupal.theme.editButtons() - */ - initialize: function(options) { - this.message = options.message; - this.buttons = options.buttons; - this.callback = options.callback; - }, - - /** - * Implements Backbone Views' render() function. - */ - render: function() { - this.setElement(Drupal.theme('editModal', {})); - this.$el.appendTo('body'); - // Template. - this.$('.main p').text(this.message); - var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons})); - this.$('.actions').append($actions); - - // Show the modal with an animation. - var that = this; - setTimeout(function() { - that.$el.removeClass('edit-animate-invisible'); - }, 0); - }, - - /** - * When the user clicks on any of the buttons, the modal should be removed - * and the result should be passed to the callback. - * - * @param event - */ - onButtonClick: function(event) { - event.stopPropagation(); - event.preventDefault(); - - // Remove after animation. - var that = this; - this.$el - .addClass('edit-animate-invisible') - .on(Drupal.edit.util.constants.transitionEnd, function(e) { - that.remove(); - }); - - var action = $(event.target).attr('data-edit-modal-action'); - return this.callback(action); - } -}); - -})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js deleted file mode 100644 index b3f04ba..0000000 --- a/core/modules/edit/js/views/propertyeditordecoration-view.js +++ /dev/null @@ -1,354 +0,0 @@ -/** - * @file - * A Backbone View that decorates a Property Editor widget. - * - * It listens to state changes of the property editor. - */ -(function($, Backbone, Drupal) { - -"use strict"; - -Drupal.edit.PropertyEditorDecorationView = Backbone.View.extend({ - toolbarId: null, - - _widthAttributeIsEmpty: null, - - events: { - 'mouseenter.edit' : 'onMouseEnter', - 'mouseleave.edit' : 'onMouseLeave', - 'click': 'onClick', - 'tabIn.edit': 'onMouseEnter', - 'tabOut.edit': 'onMouseLeave' - }, - - /** - * Implements Backbone Views' initialize() function. - * - * @param options - * An object with the following keys: - * - editor: the editor object with an 'options' object that has these keys: - * * entity: the VIE entity for the property. - * * property: the predicate of the property. - * * widget: the parent EditableEntity widget. - * * editorName: the name of the PropertyEditor widget - * - toolbarId: the ID attribute of the toolbar as rendered in the DOM. - */ - initialize: function(options) { - this.editorView = options.editorView; - - this.toolbarId = options.toolbarId; - - this.model.on('change:state', this.stateChange, this); - }, - - /** - * Listens to editor state changes. - */ - stateChange: function(model, state) { - var from = model.previous('state'); - var to = state; - switch (to) { - case 'inactive': - if (from !== null) { - this.undecorate(); - if (from === 'invalid') { - this._removeValidationErrors(); - } - } - break; - case 'candidate': - this.decorate(); - if (from !== 'inactive') { - this.stopHighlight(); - if (from !== 'highlighted') { - this.stopEdit(); - if (from === 'invalid') { - this._removeValidationErrors(); - } - } - } - break; - case 'highlighted': - this.startHighlight(); - break; - case 'activating': - // NOTE: this state is not used by every editor! It's only used by those - // that need to interact with the server. - this.prepareEdit(); - break; - case 'active': - if (from !== 'activating') { - this.prepareEdit(); - } - this.startEdit(); - break; - case 'changed': - break; - case 'saving': - if (from === 'invalid') { - this._removeValidationErrors(); - } - break; - case 'saved': - break; - case 'invalid': - break; - } - }, - - /** - * Starts hover: transition to 'highlight' state. - * - * @param event - */ - onMouseEnter: function(event) { - var that = this; - this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { - that.model.set('state', 'highlighted'); - event.stopPropagation(); - }); - }, - - /** - * Stops hover: back to 'candidate' state. - * - * @param event - */ - onMouseLeave: function(event) { - var that = this; - this._ignoreHoveringVia(event, '#' + this.toolbarId, function () { - that.model.set('state', 'candidate', { reason: 'mouseleave' }); - event.stopPropagation(); - }); - }, - - /** - * Clicks: transition to 'activating' stage. - * - * @param event - */ - onClick: function(event) { - this.model.set('state', 'activating'); - event.preventDefault(); - event.stopPropagation(); - }, - - decorate: function () { - this.$el.addClass('edit-animate-fast edit-candidate edit-editable'); - }, - - undecorate: function () { - this.$el - .removeClass('edit-candidate edit-editable edit-highlighted edit-editing'); - }, - - startHighlight: function () { - // Animations. - var that = this; - setTimeout(function() { - that.$el.addClass('edit-highlighted'); - }, 0); - }, - - stopHighlight: function() { - this.$el - .removeClass('edit-highlighted'); - }, - - prepareEdit: function() { - this.$el.addClass('edit-editing'); - - // While editing, don't show *any* other editors. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Revisit this. - $('.edit-candidate').not('.edit-editing').removeClass('edit-editable'); - }, - - startEdit: function() { - if (this.getEditUISetting('padding')) { - this._pad(); - } - }, - - stopEdit: function() { - this.$el.removeClass('edit-highlighted edit-editing'); - - // Make the other editors show up again. - // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133) - // Revisit this. - $('.edit-candidate').addClass('edit-editable'); - - if (this.getEditUISetting('padding')) { - this._unpad(); - } - }, - - /** - * Retrieves a setting of the editor-specific Edit UI integration. - * - * @see Drupal.edit.util.getEditUISetting(). - */ - getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editorView, setting); - }, - - _pad: function () { - var self = this; - - // Add 5px padding for readability. This means we'll freeze the current - // width and *then* add 5px padding, hence ensuring the padding is added "on - // the outside". - // 1) Freeze the width (if it's not already set); don't use animations. - if (this.$el[0].style.width === "") { - this._widthAttributeIsEmpty = true; - this.$el - .addClass('edit-animate-disable-width') - .css('width', this.$el.width()) - .css('background-color', this._getBgColor(this.$el)); - } - - // 2) Add padding; use animations. - var posProp = this._getPositionProperties(this.$el); - setTimeout(function() { - // Re-enable width animations (padding changes affect width too!). - self.$el.removeClass('edit-animate-disable-width'); - - // Pad the editable. - self.$el - .css({ - 'position': 'relative', - 'top': posProp.top - 5 + 'px', - 'left': posProp.left - 5 + 'px', - 'padding-top' : posProp['padding-top'] + 5 + 'px', - 'padding-left' : posProp['padding-left'] + 5 + 'px', - 'padding-right' : posProp['padding-right'] + 5 + 'px', - 'padding-bottom': posProp['padding-bottom'] + 5 + 'px', - 'margin-bottom': posProp['margin-bottom'] - 10 + 'px' - }); - }, 0); - }, - - _unpad: function () { - var self = this; - - // 1) Set the empty width again. - if (this._widthAttributeIsEmpty) { - this.$el - .addClass('edit-animate-disable-width') - .css('width', '') - .css('background-color', ''); - } - - // 2) Remove padding; use animations (these will run simultaneously with) - // the fading out of the toolbar as its gets removed). - var posProp = this._getPositionProperties(this.$el); - setTimeout(function() { - // Re-enable width animations (padding changes affect width too!). - self.$el.removeClass('edit-animate-disable-width'); - - // Unpad the editable. - self.$el - .css({ - 'position': 'relative', - 'top': posProp.top + 5 + 'px', - 'left': posProp.left + 5 + 'px', - 'padding-top' : posProp['padding-top'] - 5 + 'px', - 'padding-left' : posProp['padding-left'] - 5 + 'px', - 'padding-right' : posProp['padding-right'] - 5 + 'px', - 'padding-bottom': posProp['padding-bottom'] - 5 + 'px', - 'margin-bottom': posProp['margin-bottom'] + 10 + 'px' - }); - }, 0); - }, - - /** - * Gets the background color of an element (or the inherited one). - * - * @param $e - * A DOM element. - */ - _getBgColor: function($e) { - var c; - - if ($e === null || $e[0].nodeName === 'HTML') { - // Fallback to white. - return 'rgb(255, 255, 255)'; - } - c = $e.css('background-color'); - // TRICKY: edge case for Firefox' "transparent" here; this is a - // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724 - if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') { - return this._getBgColor($e.parent()); - } - return c; - }, - - /** - * Gets the top and left properties of an element and convert extraneous - * values and information into numbers ready for subtraction. - * - * @param $e - * A DOM element. - */ - _getPositionProperties: function($e) { - var p, - r = {}, - props = [ - 'top', 'left', 'bottom', 'right', - 'padding-top', 'padding-left', 'padding-right', 'padding-bottom', - 'margin-bottom' - ]; - - var propCount = props.length; - for (var i = 0; i < propCount; i++) { - p = props[i]; - r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10); - } - return r; - }, - - /** - * Replaces blank or 'auto' CSS "position: " values with "0px". - * - * @param pos - * The value for a CSS position declaration. - */ - _replaceBlankPosition: function(pos) { - if (pos === 'auto' || !pos) { - pos = '0px'; - } - return pos; - }, - - /** - * Ignores hovering to/from the given closest element, but as soon as a hover - * occurs to/from *another* element, then call the given callback. - */ - _ignoreHoveringVia: function(event, closest, callback) { - if ($(event.relatedTarget).closest(closest).length > 0) { - event.stopPropagation(); - } - else { - callback(); - } - }, - - /** - * Removes validation errors' markup changes, if any. - * - * Note: this only needs to happen for type=direct, because for type=direct, - * the property DOM element itself is modified; this is not the case for - * type=form. - */ - _removeValidationErrors: function() { - if (this.model.get('editor') !== 'form') { - this.$el - .removeClass('edit-validation-error') - .next('.edit-validation-errors') - .remove(); - } - } - -}); - -})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js deleted file mode 100644 index 7497776..0000000 --- a/core/modules/edit/js/views/toolbar-view.js +++ /dev/null @@ -1,398 +0,0 @@ -/** - * @file - * A Backbone View that provides an interactive toolbar (1 per property editor). - * - * It listens to state changes of the property editor. It also triggers state - * changes in response to user interactions with the toolbar, including saving. - */ -(function ($, _, Backbone, Drupal) { - -"use strict"; - -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' - }, - - /** - * Implements Backbone Views' initialize() function. - */ - initialize: function(options) { - 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); - }, - - /** - * Renders the Toolbar's markup into the DOM. - * - * Note: depending on whether the 'display' property of the $el for which a - * toolbar is being inserted into the DOM, it will be inserted differently. - */ - render: function () { - // Render toolbar. - this.setElement($(Drupal.theme('editToolbarContainer', { - id: this._id - }))); - - // Insert in DOM. - if (this.$field.css('display') === 'inline') { - this.$el.prependTo(this.$field.offsetParent()); - var pos = this.$field.position(); - this.$el.css('left', pos.left).css('top', pos.top); - } - else { - this.$el.insertBefore(this.$field); - } - - return this; - }, - - /** - * Listens to editor state changes. - */ - 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; - } - }, - - /** - * When the user clicks the info label, nothing should happen. - * @note currently redirects the click.edit-event to the editor DOM element. - * - * @param event - */ - onClickInfoLabel: function(event) { - event.stopPropagation(); - event.preventDefault(); - // Redirects the event to the editor DOM element. - this.$field.trigger('click.edit'); - }, - - /** - * A mouseleave to the editor doesn't matter; a mouseleave to something else - * counts as a mouseleave on the editor itself. - * - * @param event - */ - onMouseLeave: function(event) { - if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) { - this.$field.trigger('mouseleave.edit'); - } - event.stopPropagation(); - }, - - /** - * Upon clicking "Save", trigger a custom event to save this property. - * - * @param event - */ - onClickSave: function(event) { - event.stopPropagation(); - event.preventDefault(); - this.model.set('state', 'saving'); - }, - - /** - * Upon clicking "Close", trigger a custom event to stop editing. - * - * @param 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 bool 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. - * - * @see Drupal.edit.util.getEditUISetting(). - */ - getEditUISetting: function(setting) { - return Drupal.edit.util.getEditUISetting(this.editorView, setting); - }, - - /** - * Adjusts the toolbar to accomodate padding on the PropertyEditor widget. - * - * @see PropertyEditorDecorationView._pad(). - */ - _pad: function() { - // The whole toolbar must move to the top when the property's DOM element - // is displayed inline. - if (this.$field.css('display') === 'inline') { - this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px'); - } - - // The toolbar must move to the top and the left. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '6px', left: '-5px' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: this.$field.width() + 10 }); - } - }, - - /** - * Undoes the changes made by _pad(). - * - * @see PropertyEditorDecorationView._unpad(). - */ - _unpad: function() { - // Move the toolbar back to its original position. - var $hf = this.$el.find('.edit-toolbar-heightfaker'); - $hf.css({ bottom: '1px', left: '' }); - - if (this.getEditUISetting('fullWidthToolbar')) { - $hf.css({ width: '' }); - } - }, - - insertWYSIWYGToolGroups: function() { - this.$el - .find('.edit-toolbar') - .append(Drupal.theme('editToolgroup', { - id: this.getFloatedWysiwygToolgroupId(), - classes: 'wysiwyg-floated', - buttons: [] - })) - .append(Drupal.theme('editToolgroup', { - id: this.getMainWysiwygToolgroupId(), - classes: 'wysiwyg-main', - buttons: [] - })); - - // Animate the toolgroups into visibility. - var that = this; - setTimeout(function () { - that.show('wysiwyg-floated'); - that.show('wysiwyg-main'); - }, 0); - }, - - /** - * Retrieves the ID for this toolbar's container. - * - * Only used to make sane hovering behavior possible. - * - * @return string - * A string that can be used as the ID for this toolbar's container. - */ - getId: function () { - return 'edit-toolbar-for-' + this._id; - }, - - /** - * Retrieves the ID for this toolbar's floating WYSIWYG toolgroup. - * - * Used to provide an abstraction for any WYSIWYG editor to plug in. - * - * @return string - * A string that can be used as the ID. - */ - getFloatedWysiwygToolgroupId: function () { - return 'edit-wysiwyg-floated-toolgroup-for-' + this._id; - }, - - /** - * Retrieves the ID for this toolbar's main WYSIWYG toolgroup. - * - * Used to provide an abstraction for any WYSIWYG editor to plug in. - * - * @return string - * A string that can be used as the ID. - */ - 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); - } -}); - -})(jQuery, _, Backbone, Drupal);