core/modules/edit/edit.module | 2 - core/modules/edit/js/backbone.drupalform.js | 181 -------------------- core/modules/edit/js/edit.js | 3 +- core/modules/edit/js/editors/directEditor.js | 81 +++++---- core/modules/edit/js/models/FieldModel.js | 1 + core/modules/edit/js/views/AppView.js | 10 +- core/modules/edit/js/views/EditorView.js | 134 ++++++++++----- core/modules/edit/js/views/FieldToolbarView.js | 6 - .../editor/js/editor.formattedTextEditor.js | 14 +- 9 files changed, 152 insertions(+), 280 deletions(-) diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 35531b4..043e825 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -82,8 +82,6 @@ function edit_library_info() { $path . '/js/views/ModalView.js' => $options, $path . '/js/views/FieldToolbarView.js' => $options, $path . '/js/views/EditorView.js' => $options, - // Backbone.sync implementation on top of Drupal forms. - $path . '/js/backbone.drupalform.js' => $options, // Other. $path . '/js/util.js' => $options, $path . '/js/theme.js' => $options, diff --git a/core/modules/edit/js/backbone.drupalform.js b/core/modules/edit/js/backbone.drupalform.js deleted file mode 100644 index a5cb6f6..0000000 --- a/core/modules/edit/js/backbone.drupalform.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * @file - * Backbone.sync implementation for Edit. This is the beating heart. - */ -(function (jQuery, Backbone, Drupal) { - -"use strict"; - -Backbone.defaultSync = Backbone.sync; - -// @todo We should define a per-model sync method and assign it when creating -// the fieldModel instance. -Backbone.sync = function(method, model, options) { - if (this.get('editorName') === 'form') { - return Backbone.syncDrupalFormWidget(method, model, options); - } - else { - return Backbone.syncDirect(method, model, options); - } -}; - -/** - * Performs syncing for "form" Editor widgets. - * - * Implemented on top of Form API and the AJAX commands framework. Sets up - * scoped AJAX command closures specifically for a given Editor - * (which contains a pre-existing form). By submitting the form through - * Drupal.ajax and leveraging Drupal.ajax' ability to re-render the - * form when there are validation errors and ensure no Drupal.ajax memory leaks. - * - * @see Drupal.edit.util.form - */ -Backbone.syncDrupalFormWidget = function(method, model, options) { - if (method === 'update') { - var predicate = options.editor.options.property; - - var $formContainer = options.editor.$formContainer; - var $submit = $formContainer.find('.edit-form-submit'); - var base = $submit.attr('id'); - - // Successfully saved. - Drupal.ajax[base].commands.editFieldFormSaved = function(ajax, response, status) { - Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); - - // Call Backbone.sync's success callback with the rerendered field. - var changedAttributes = {}; - // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216) - changedAttributes[predicate] = undefined; - changedAttributes[predicate + '/rendered'] = response.data; - options.success(changedAttributes); - }; - - // Unsuccessfully saved; validation errors. - Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) { - // Call Backbone.sync's error callback with the validation error messages. - options.error(response.data); - }; - - // The edit_field_form AJAX command is only called upon loading the form for - // the first time, and when there are validation errors in the form; Form - // API then marks which form items have errors. Therefor, we have to replace - // the existing form, unbind the existing Drupal.ajax instance and create a - // new Drupal.ajax instance. - Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) { - Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element)); - - Drupal.ajax.prototype.commands.insert(ajax, { - data: response.data, - selector: '#' + $formContainer.attr('id') + ' form' - }); - - // Create a Drupa.ajax instance for the re-rendered ("new") form. - var $newSubmit = $formContainer.find('.edit-form-submit'); - Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit); - }; - - // Click the form's submit button; the scoped AJAX commands above will - // handle the server's response. - $submit.trigger('click.edit'); - } -}; - -/** -* Performs syncing for "direct" PredicateEditor widgets. - * - * @see Backbone.syncDrupalFormWidget() - * @see Drupal.edit.util.form - */ -Backbone.syncDirect = function(method, model, options) { - if (method === 'update') { - var fillAndSubmitForm = function(value) { - var $form = jQuery('#edit_backstage form'); - // Fill in the value in any that isn't hidden or a submit button. - $form.find(':input[type!="hidden"][type!="submit"]:not(select)') - // Don't mess with the node summary. - .not('[name$="\[summary\]"]').val(value); - // Submit the form. - $form.find('.edit-form-submit').trigger('click.edit'); - }; - - var value = model.get('currentValue'); - - // If form doesn't already exist, load it and then submit. - if (jQuery('#edit_backstage form').length === 0) { - var formOptions = { - propertyID: model.id, - $editorElement: jQuery(model.get('el')), - nocssjs: true - }; - Drupal.edit.util.form.load(formOptions, function(form, ajax) { - // Create a backstage area for storing forms that are hidden from view - // (hence "backstage" — since the editing doesn't happen in the form, it - // happens "directly" in the content, the form is only used for saving). - jQuery(Drupal.theme('editBackstage', { id: 'edit_backstage' })).appendTo('body'); - // Direct forms are stuffed into #edit_backstage, apparently. - jQuery('#edit_backstage').append(form); - // Disable the browser's HTML5 validation; we only care about server- - // side validation. (Not disabling this will actually cause problems - // because browsers don't like to set HTML5 validation errors on hidden - // forms.) - jQuery('#edit_backstage form').prop('novalidate', true); - var $submit = jQuery('#edit_backstage form .edit-form-submit'); - var base = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); - - // Successfully saved. - Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) { - Backbone.syncDirectCleanUp(); - - // Call Backbone.sync's success callback with the rerendered field. - var changedAttributes = {}; - // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216) - changedAttributes[model.id] = jQuery(response.data).find('.field-item').html(); - changedAttributes[model.id + '/rendered'] = response.data; - options.success(changedAttributes); - }; - - // Unsuccessfully saved; validation errors. - Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) { - // Call Backbone.sync's error callback with the validation error messages. - options.error(response.data); - }; - - // The editFieldForm AJAX command is only called upon loading the form - // for the first time, and when there are validation errors in the form; - // Form API then marks which form items have errors. This is useful for - // "form" editors, but pointless for "direct" editors: the form itself - // won't be visible at all anyway! Therefor, we ignore the new form and - // we continue to use the existing form. - Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) { - // no-op - }; - - fillAndSubmitForm(value); - }); - } - else { - fillAndSubmitForm(value); - } - } -}; - -/** - * Cleans up the hidden form that Backbone.syncDirect uses for syncing. - * - * This is called automatically by Backbone.syncDirect when saving is successful - * (i.e. when there are no validation errors). Only when editing is canceled - * while an Editor widget is in the invalid state, this must be called - * "manually" (in practice, FieldToolbarView does this). This is necessary because - * Backbone.syncDirect is not aware of the application state, it only does the - * syncing. - * An alternative could be to also remove the hidden form when validation errors - * occur, but then the form must be retrieved again, thus resulting in another - * roundtrip, which is bad for front-end performance. - */ -Backbone.syncDirectCleanUp = function() { - var $submit = jQuery('#edit_backstage form .edit-form-submit'); - Drupal.edit.util.form.unajaxifySaving($submit); - jQuery('#edit_backstage form').remove(); -}; - -})(jQuery, Backbone, Drupal); diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js index 413692b..c68ebb3 100644 --- a/core/modules/edit/js/edit.js +++ b/core/modules/edit/js/edit.js @@ -198,8 +198,7 @@ function initializeField (fieldElement, editID) { var field = new Drupal.edit.FieldModel({ entity: entity, el: fieldElement, - id: editID || null, - editID: editID || null, + editID: editID, label: Drupal.edit.Metadata.get(editID, 'label'), editor: Drupal.edit.Metadata.get(editID, 'editor'), html: fieldElement.outerHTML, diff --git a/core/modules/edit/js/editors/directEditor.js b/core/modules/edit/js/editors/directEditor.js index 12273ec..61fdf95 100644 --- a/core/modules/edit/js/editors/directEditor.js +++ b/core/modules/edit/js/editors/directEditor.js @@ -1,6 +1,6 @@ /** * @file - * Defined a editor for direct editable fields. + * contentEditable-based in-place editor for plain text content. */ (function ($, Drupal) { @@ -9,43 +9,44 @@ Drupal.edit = Drupal.edit || {}; Drupal.edit.editors = Drupal.edit.editors || {}; -Drupal.edit.editors.direct = Backbone.View.extend({ +Drupal.edit.editors.direct = Drupal.edit.EditorView.extend({ - /** - * {@inheritdoc} - */ - getEditUISettings: function () { - return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; - }, + $textElement: null, /** * {@inheritdoc} */ - initialize: function () { - var that = this; + initialize: function (options) { + Drupal.edit.EditorView.prototype.initialize.call(this, options); - // Sets the state to 'changed' whenever the content has changed. - var before = jQuery.trim(this.element.text()); - this.element.on('keyup paste', function (event) { - var current = jQuery.trim(that.element.text()); - if (before !== current) { - before = current; - that.model.set('state', 'changed'); + var model = this.model; + + // Store the actual value of this field. We'll need this to restore the + // original value when the user discards his modifications. + var $textElement = this.$textElement = this.$el.find('.field-item:first'); + model.set('originalValue', jQuery.trim(this.$textElement.text())); - // @todo we have yet to set this value originally (before the editing - // starts) AND we have to handle the reverting aspect when editing is - // canceled, see editorStateChange(). - that.model.set('value', current); + // Sets the state to 'changed' whenever the content has changed. + var previousText = model.get('originalValue'); + $textElement.on('keyup paste', function (event) { + var currentText = jQuery.trim($textElement.text()); + if (previousText !== currentText) { + previousText = currentText; + model.set('currentValue', currentText); + model.set('state', 'changed'); } }); }, /** - * 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. + * {@inheritdoc} + */ + getEditedElement: function () { + return this.$textElement; + }, + + /** + * {@inheritdoc} */ stateChange: function (model, state) { var from = model.previous('state'); @@ -55,8 +56,10 @@ Drupal.edit.editors.direct = Backbone.View.extend({ break; case 'candidate': if (from !== 'inactive') { - // Removes the "contenteditable" attribute. - this.disable(); + this.$textElement.removeAttr('contentEditable'); + } + if (from === 'invalid') { + this.removeValidationErrors(); } break; case 'highlighted': @@ -70,20 +73,38 @@ Drupal.edit.editors.direct = Backbone.View.extend({ }); break; case 'active': - // Sets the "contenteditable" attribute to "true". - this.enable(); + this.$textElement.attr('contentEditable', 'true'); break; case 'changed': break; case 'saving': + if (from === 'invalid') { + this.removeValidationErrors(); + } this.save(); break; case 'saved': break; case 'invalid': + this.showValidationErrors(); break; } + }, + + /** + * {@inheritdoc} + */ + getEditUISettings: function () { + return { padding: true, unifiedToolbar: false, fullWidthToolbar: false }; + }, + + /** + * {@inheritdoc} + */ + revert: function () { + this.$textElement.html(this.model.get('originalValue')); } + }); })(jQuery, Drupal); diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js index 0ce6790..63f17fb 100644 --- a/core/modules/edit/js/models/FieldModel.js +++ b/core/modules/edit/js/models/FieldModel.js @@ -36,6 +36,7 @@ $.extend(Drupal.edit, { // // The edit ID, format: `::::`. + // @todo rename to "id"? 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 diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js index 10d73f6..4137ac0 100644 --- a/core/modules/edit/js/views/AppView.js +++ b/core/modules/edit/js/views/AppView.js @@ -178,15 +178,11 @@ $.extend(Drupal.edit, { // @todo rename to decorateField decorate: function (fieldModel) { - var editID = fieldModel.get('editID'); - var el = fieldModel.get('el'); - var $el = $(el); - // Create a new Editor. var editorName = fieldModel.get('editor'); fieldModel.set('editorName', editorName); var editorView = new Drupal.edit.editors[editorName]({ - el: $el, + el: $(fieldModel.get('el')), model: fieldModel }); @@ -194,13 +190,13 @@ $.extend(Drupal.edit, { // They are a sibling element before the editor's DOM element. var toolbarView = new Drupal.edit.FieldToolbarView({ model: fieldModel, - $field: $el, + $field: $(editorView.getEditedElement()), editorView: editorView }); // Decorate the editor's DOM element depending on its state. var decorationView = new Drupal.edit.EditorDecorationView({ - el: $el, + el: $(editorView.getEditedElement()), model: fieldModel, editorView: editorView, toolbarId: toolbarView.getId() diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js index fc99aa2..5dd6f4b 100644 --- a/core/modules/edit/js/views/EditorView.js +++ b/core/modules/edit/js/views/EditorView.js @@ -31,6 +31,25 @@ $.extend(Drupal.edit, { }, /** + * Returns the edited element. + * + * For some single cardinality fields, it may be necessary or useful to + * not in-place edit (and hence decorate) the DOM element with the + * data-edit-id attribute (which is the field's wrapper), but a specific + * element within the field's wrapper. + * e.g. using a WYSIWYG editor on a body field should happen on the DOM + * element containing the text itself, not on the field wrapper. + * + * For example, @see Drupal.edit.editors.direct. + * + * @return jQuery + * A jQuery-wrapped DOM element. + */ + getEditedElement: function () { + return this.$el; + }, + + /** * Returns 3 Edit UI settings that depend on the in-place editor: * - padding: @todo * - unifiedToolbar: @todo @@ -121,62 +140,82 @@ $.extend(Drupal.edit, { * started). */ revert: function () { - // @todo Should we implement a default here? + // A no-op by default; each editor should implement reverting itself. + + // Note that if the in-place editor does not cause the FieldModel's + // element to be modified, then nothing needs to happen. }, /** * Saves the modified value in the in-place editor for this field. */ save: function () { - // @todo implement here the "direct" case, i.e. the hidden form case - this.model.save(null, { - // Successfully saved without validation errors. - success: function (model, response, options) { - model.set('state', 'saved'); - - var entity = model.get('entity'); - var id = model.get('id'); - - // Now that the changes to this property have been saved, the saved - // attributes are now the "original" attributes. - entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes); - - // Get data necessary to rerender property before it is unavailable. - var updatedProperty = response[id + '/rendered']; - var $propertyWrapper = $(model.get('el')); - var $context = $propertyWrapper.parent(); - - model.set('state', 'candidate'); - - // @todo Do we need this trigger now that everything is driven from - // models? - // Trigger event to allow for proper clean-up of editor-specific views. - //editor.element.trigger('destroyedPropertyEditor.edit', editor); + var model = this.model; + + function fillAndSubmitForm (value) { + var $form = jQuery('#edit_backstage form'); + // Fill in the value in any that isn't hidden or a submit button. + $form.find(':input[type!="hidden"][type!="submit"]:not(select)') + // Don't mess with the node summary. + .not('[name$="\\[summary\\]"]').val(value); + // Submit the form. + $form.find('.edit-form-submit').trigger('click.edit'); + } - // Replace the old content with the new content. - $propertyWrapper.replaceWith(updatedProperty); - Drupal.attachBehaviors($context); - }, + var value = model.get('currentValue'); + + var formOptions = { + propertyID: model.get('editID'), + $editorElement: $(this.getEditedElement()), + nocssjs: true + }; + Drupal.edit.util.form.load(formOptions, function(form, ajax) { + // Create a backstage area for storing forms that are hidden from view + // (hence "backstage" — since the editing doesn't happen in the form, it + // happens "directly" in the content, the form is only used for saving). + jQuery(Drupal.theme('editBackstage', { id: 'edit_backstage' })).appendTo('body'); + // Direct forms are stuffed into #edit_backstage, apparently. + jQuery('#edit_backstage').append(form); + // Disable the browser's HTML5 validation; we only care about server- + // side validation. (Not disabling this will actually cause problems + // because browsers don't like to set HTML5 validation errors on hidden + // forms.) + jQuery('#edit_backstage form').prop('novalidate', true); + var $submit = jQuery('#edit_backstage form .edit-form-submit'); + var base = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit); + + function removeHiddenForm () { + Drupal.edit.util.form.unajaxifySaving($submit); + jQuery('#edit_backstage').remove(); + } - // Save attempted but failed due to validation errors. - error: function (validationErrorMessages) { - editableEntity.setState('invalid', predicate); + // Successfully saved. + Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) { + removeHiddenForm(); - if (that.editorName === 'form') { - editor.$formContainer - .find('.edit-form') - .addClass('edit-validation-error') - .find('form') - .prepend(validationErrorMessages); - } - else { - var $errors = $('
') - .append(validationErrorMessages); - editor.element - .addClass('edit-validation-error') - .after($errors); - } - } + // First, transition the state to 'saved'. + model.set('state', 'saved'); + // Then, set the 'html' attribute on the field model. This will cause the + // field to be rerendered. + model.set('html', response.data); + }; + + // Unsuccessfully saved; validation errors. + Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) { + removeHiddenForm(); + + model.set('validationErrors', response.data); + model.set('state', 'invalid'); + }; + + // The editFieldForm AJAX command is only called upon loading the form + // for the first time, and when there are validation errors in the form; + // Form API then marks which form items have errors. This is useful for + // the form-based in-place editor, but pointless for any other: the form + // itself won't be visible at all anyway! So, we just ignore it. + Drupal.ajax[base].commands.editFieldForm = function() {}; + + fillAndSubmitForm(value); }); }, @@ -207,6 +246,7 @@ $.extend(Drupal.edit, { .next('.edit-validation-errors') .remove(); } + }) }); diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js index a854312..117debb 100644 --- a/core/modules/edit/js/views/FieldToolbarView.js +++ b/core/modules/edit/js/views/FieldToolbarView.js @@ -79,9 +79,6 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ case 'inactive': if (from) { this.remove(); - if (this.model.get('editor') !== 'form') { - Backbone.syncDirectCleanUp(); - } } break; case 'candidate': @@ -89,9 +86,6 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({ 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') diff --git a/core/modules/editor/js/editor.formattedTextEditor.js b/core/modules/editor/js/editor.formattedTextEditor.js index 4361548..5d3bd01 100644 --- a/core/modules/editor/js/editor.formattedTextEditor.js +++ b/core/modules/editor/js/editor.formattedTextEditor.js @@ -37,7 +37,6 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ // Store the actual value of this field. We'll need this to restore the // original value when the user discards his modifications. - // @todo: figure out a more nicely abstracted way to handle this. this.$textElement = this.$el.find('.field-item:first'); this.model.set('originalValue', this.$textElement.html()); }, @@ -45,6 +44,13 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ /** * {@inheritdoc} */ + getEditedElement: function () { + return this.$textElement; + }, + + /** + * {@inheritdoc} + */ stateChange: function (model, state) { var that = this; var from = model.previous('state'); @@ -60,7 +66,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ this.textEditor.detach(this.$textElement.get(0), this.textFormat); } if (from === 'invalid') { - this.removeValidationErrors(); + this.removeValidationErrors(); } break; @@ -74,9 +80,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({ if (this.textFormatHasTransformations) { var editID = this.model.get('editID'); this._getUntransformedText(editID, this.$el, function (untransformedText) { - // @todo update this - debugger; - that.$textElement.set(untransformedText); + that.$textElement.html(untransformedText); that.model.set('state', 'active'); }); }