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