5e041db0d4eb836eaddcf5f8caac0a7299a60e5e
[yaffs-website] / web / core / modules / quickedit / js / views / EditorView.js
1 /**
2  * @file
3  * An abstract Backbone View that controls an in-place editor.
4  */
5
6 (function ($, Backbone, Drupal) {
7
8   'use strict';
9
10   Drupal.quickedit.EditorView = Backbone.View.extend(/** @lends Drupal.quickedit.EditorView# */{
11
12     /**
13      * A base implementation that outlines the structure for in-place editors.
14      *
15      * Specific in-place editor implementations should subclass (extend) this
16      * View and override whichever method they deem necessary to override.
17      *
18      * Typically you would want to override this method to set the
19      * originalValue attribute in the FieldModel to such a value that your
20      * in-place editor can revert to the original value when necessary.
21      *
22      * @example
23      * <caption>If you override this method, you should call this
24      * method (the parent class' initialize()) first.</caption>
25      * Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
26      *
27      * @constructs
28      *
29      * @augments Backbone.View
30      *
31      * @param {object} options
32      *   An object with the following keys:
33      * @param {Drupal.quickedit.EditorModel} options.model
34      *   The in-place editor state model.
35      * @param {Drupal.quickedit.FieldModel} options.fieldModel
36      *   The field model.
37      *
38      * @see Drupal.quickedit.EditorModel
39      * @see Drupal.quickedit.editors.plain_text
40      */
41     initialize: function (options) {
42       this.fieldModel = options.fieldModel;
43       this.listenTo(this.fieldModel, 'change:state', this.stateChange);
44     },
45
46     /**
47      * @inheritdoc
48      */
49     remove: function () {
50       // The el property is the field, which should not be removed. Remove the
51       // pointer to it, then call Backbone.View.prototype.remove().
52       this.setElement();
53       Backbone.View.prototype.remove.call(this);
54     },
55
56     /**
57      * Returns the edited element.
58      *
59      * For some single cardinality fields, it may be necessary or useful to
60      * not in-place edit (and hence decorate) the DOM element with the
61      * data-quickedit-field-id attribute (which is the field's wrapper), but a
62      * specific element within the field's wrapper.
63      * e.g. using a WYSIWYG editor on a body field should happen on the DOM
64      * element containing the text itself, not on the field wrapper.
65      *
66      * @return {jQuery}
67      *   A jQuery-wrapped DOM element.
68      *
69      * @see Drupal.quickedit.editors.plain_text
70      */
71     getEditedElement: function () {
72       return this.$el;
73     },
74
75     /**
76      *
77      * @return {object}
78      * Returns 3 Quick Edit UI settings that depend on the in-place editor:
79      *  - Boolean padding: indicates whether padding should be applied to the
80      *    edited element, to guarantee legibility of text.
81      *  - Boolean unifiedToolbar: provides the in-place editor with the ability
82      *    to insert its own toolbar UI into Quick Edit's tightly integrated
83      *    toolbar.
84      *  - Boolean fullWidthToolbar: indicates whether Quick Edit's tightly
85      *    integrated toolbar should consume the full width of the element,
86      *    rather than being just long enough to accommodate a label.
87      */
88     getQuickEditUISettings: function () {
89       return {padding: false, unifiedToolbar: false, fullWidthToolbar: false, popup: false};
90     },
91
92     /**
93      * Determines the actions to take given a change of state.
94      *
95      * @param {Drupal.quickedit.FieldModel} fieldModel
96      *   The quickedit `FieldModel` that holds the state.
97      * @param {string} state
98      *   The state of the associated field. One of
99      *   {@link Drupal.quickedit.FieldModel.states}.
100      */
101     stateChange: function (fieldModel, state) {
102       var from = fieldModel.previous('state');
103       var to = state;
104       switch (to) {
105         case 'inactive':
106           // An in-place editor view will not yet exist in this state, hence
107           // this will never be reached. Listed for sake of completeness.
108           break;
109
110         case 'candidate':
111           // Nothing to do for the typical in-place editor: it should not be
112           // visible yet. Except when we come from the 'invalid' state, then we
113           // clean up.
114           if (from === 'invalid') {
115             this.removeValidationErrors();
116           }
117           break;
118
119         case 'highlighted':
120           // Nothing to do for the typical in-place editor: it should not be
121           // visible yet.
122           break;
123
124         case 'activating':
125           // The user has indicated he wants to do in-place editing: if
126           // something needs to be loaded (CSS/JavaScript/server data/…), then
127           // do so at this stage, and once the in-place editor is ready,
128           // set the 'active' state. A "loading" indicator will be shown in the
129           // UI for as long as the field remains in this state.
130           var loadDependencies = function (callback) {
131             // Do the loading here.
132             callback();
133           };
134           loadDependencies(function () {
135             fieldModel.set('state', 'active');
136           });
137           break;
138
139         case 'active':
140           // The user can now actually use the in-place editor.
141           break;
142
143         case 'changed':
144           // Nothing to do for the typical in-place editor. The UI will show an
145           // indicator that the field has changed.
146           break;
147
148         case 'saving':
149           // When the user has indicated he wants to save his changes to this
150           // field, this state will be entered. If the previous saving attempt
151           // resulted in validation errors, the previous state will be
152           // 'invalid'. Clean up those validation errors while the user is
153           // saving.
154           if (from === 'invalid') {
155             this.removeValidationErrors();
156           }
157           this.save();
158           break;
159
160         case 'saved':
161           // Nothing to do for the typical in-place editor. Immediately after
162           // being saved, a field will go to the 'candidate' state, where it
163           // should no longer be visible (after all, the field will then again
164           // just be a *candidate* to be in-place edited).
165           break;
166
167         case 'invalid':
168           // The modified field value was attempted to be saved, but there were
169           // validation errors.
170           this.showValidationErrors();
171           break;
172       }
173     },
174
175     /**
176      * Reverts the modified value to the original, before editing started.
177      */
178     revert: function () {
179       // A no-op by default; each editor should implement reverting itself.
180       // Note that if the in-place editor does not cause the FieldModel's
181       // element to be modified, then nothing needs to happen.
182     },
183
184     /**
185      * Saves the modified value in the in-place editor for this field.
186      */
187     save: function () {
188       var fieldModel = this.fieldModel;
189       var editorModel = this.model;
190       var backstageId = 'quickedit_backstage-' + this.fieldModel.id.replace(/[\/\[\]\_\s]/g, '-');
191
192       function fillAndSubmitForm(value) {
193         var $form = $('#' + backstageId).find('form');
194         // Fill in the value in any <input> that isn't hidden or a submit
195         // button.
196         $form.find(':input[type!="hidden"][type!="submit"]:not(select)')
197           // Don't mess with the node summary.
198           .not('[name$="\\[summary\\]"]').val(value);
199         // Submit the form.
200         $form.find('.quickedit-form-submit').trigger('click.quickedit');
201       }
202
203       var formOptions = {
204         fieldID: this.fieldModel.get('fieldID'),
205         $el: this.$el,
206         nocssjs: true,
207         other_view_modes: fieldModel.findOtherViewModes(),
208         // Reset an existing entry for this entity in the PrivateTempStore (if
209         // any) when saving the field. Logically speaking, this should happen in
210         // a separate request because this is an entity-level operation, not a
211         // field-level operation. But that would require an additional request,
212         // that might not even be necessary: it is only when a user saves a
213         // first changed field for an entity that this needs to happen:
214         // precisely now!
215         reset: !this.fieldModel.get('entity').get('inTempStore')
216       };
217
218       var self = this;
219       Drupal.quickedit.util.form.load(formOptions, function (form, ajax) {
220         // Create a backstage area for storing forms that are hidden from view
221         // (hence "backstage" — since the editing doesn't happen in the form, it
222         // happens "directly" in the content, the form is only used for saving).
223         var $backstage = $(Drupal.theme('quickeditBackstage', {id: backstageId})).appendTo('body');
224         // Hidden forms are stuffed into the backstage container for this field.
225         var $form = $(form).appendTo($backstage);
226         // Disable the browser's HTML5 validation; we only care about server-
227         // side validation. (Not disabling this will actually cause problems
228         // because browsers don't like to set HTML5 validation errors on hidden
229         // forms.)
230         $form.prop('novalidate', true);
231         var $submit = $form.find('.quickedit-form-submit');
232         self.formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(formOptions, $submit);
233
234         function removeHiddenForm() {
235           Drupal.quickedit.util.form.unajaxifySaving(self.formSaveAjax);
236           delete self.formSaveAjax;
237           $backstage.remove();
238         }
239
240         // Successfully saved.
241         self.formSaveAjax.commands.quickeditFieldFormSaved = function (ajax, response, status) {
242           removeHiddenForm();
243           // First, transition the state to 'saved'.
244           fieldModel.set('state', 'saved');
245           // Second, set the 'htmlForOtherViewModes' attribute, so that when
246           // this field is rerendered, the change can be propagated to other
247           // instances of this field, which may be displayed in different view
248           // modes.
249           fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
250           // Finally, set the 'html' attribute on the field model. This will
251           // cause the field to be rerendered.
252           fieldModel.set('html', response.data);
253         };
254
255         // Unsuccessfully saved; validation errors.
256         self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function (ajax, response, status) {
257           removeHiddenForm();
258           editorModel.set('validationErrors', response.data);
259           fieldModel.set('state', 'invalid');
260         };
261
262         // The quickeditFieldForm AJAX command is only called upon loading the
263         // form for the first time, and when there are validation errors in the
264         // form; Form API then marks which form items have errors. This is
265         // useful for the form-based in-place editor, but pointless for any
266         // other: the form itself won't be visible at all anyway! So, we just
267         // ignore it.
268         self.formSaveAjax.commands.quickeditFieldForm = function () {};
269
270         fillAndSubmitForm(editorModel.get('currentValue'));
271       });
272     },
273
274     /**
275      * Shows validation error messages.
276      *
277      * Should be called when the state is changed to 'invalid'.
278      */
279     showValidationErrors: function () {
280       var $errors = $('<div class="quickedit-validation-errors"></div>')
281         .append(this.model.get('validationErrors'));
282       this.getEditedElement()
283         .addClass('quickedit-validation-error')
284         .after($errors);
285     },
286
287     /**
288      * Cleans up validation error messages.
289      *
290      * Should be called when the state is changed to 'candidate' or 'saving'. In
291      * the case of the latter: the user has modified the value in the in-place
292      * editor again to attempt to save again. In the case of the latter: the
293      * invalid value was discarded.
294      */
295     removeValidationErrors: function () {
296       this.getEditedElement()
297         .removeClass('quickedit-validation-error')
298         .next('.quickedit-validation-errors')
299         .remove();
300     }
301
302   });
303
304 }(jQuery, Backbone, Drupal));