Updated Drupal to 8.6. This goes with the following updates because it's possible...
[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(
8     /** @lends Drupal.quickedit.EditorView# */ {
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 {
87           padding: false,
88           unifiedToolbar: false,
89           fullWidthToolbar: false,
90           popup: false,
91         };
92       },
93
94       /**
95        * Determines the actions to take given a change of state.
96        *
97        * @param {Drupal.quickedit.FieldModel} fieldModel
98        *   The quickedit `FieldModel` that holds the state.
99        * @param {string} state
100        *   The state of the associated field. One of
101        *   {@link Drupal.quickedit.FieldModel.states}.
102        */
103       stateChange(fieldModel, state) {
104         const from = fieldModel.previous('state');
105         const to = state;
106         switch (to) {
107           case 'inactive':
108             // An in-place editor view will not yet exist in this state, hence
109             // this will never be reached. Listed for sake of completeness.
110             break;
111
112           case 'candidate':
113             // Nothing to do for the typical in-place editor: it should not be
114             // visible yet. Except when we come from the 'invalid' state, then we
115             // clean up.
116             if (from === 'invalid') {
117               this.removeValidationErrors();
118             }
119             break;
120
121           case 'highlighted':
122             // Nothing to do for the typical in-place editor: it should not be
123             // visible yet.
124             break;
125
126           case 'activating': {
127             // The user has indicated he wants to do in-place editing: if
128             // something needs to be loaded (CSS/JavaScript/server data/…), then
129             // do so at this stage, and once the in-place editor is ready,
130             // set the 'active' state. A "loading" indicator will be shown in the
131             // UI for as long as the field remains in this state.
132             const loadDependencies = function(callback) {
133               // Do the loading here.
134               callback();
135             };
136             loadDependencies(() => {
137               fieldModel.set('state', 'active');
138             });
139             break;
140           }
141
142           case 'active':
143             // The user can now actually use the in-place editor.
144             break;
145
146           case 'changed':
147             // Nothing to do for the typical in-place editor. The UI will show an
148             // indicator that the field has changed.
149             break;
150
151           case 'saving':
152             // When the user has indicated he wants to save his changes to this
153             // field, this state will be entered. If the previous saving attempt
154             // resulted in validation errors, the previous state will be
155             // 'invalid'. Clean up those validation errors while the user is
156             // saving.
157             if (from === 'invalid') {
158               this.removeValidationErrors();
159             }
160             this.save();
161             break;
162
163           case 'saved':
164             // Nothing to do for the typical in-place editor. Immediately after
165             // being saved, a field will go to the 'candidate' state, where it
166             // should no longer be visible (after all, the field will then again
167             // just be a *candidate* to be in-place edited).
168             break;
169
170           case 'invalid':
171             // The modified field value was attempted to be saved, but there were
172             // validation errors.
173             this.showValidationErrors();
174             break;
175         }
176       },
177
178       /**
179        * Reverts the modified value to the original, before editing started.
180        */
181       revert() {
182         // A no-op by default; each editor should implement reverting itself.
183         // Note that if the in-place editor does not cause the FieldModel's
184         // element to be modified, then nothing needs to happen.
185       },
186
187       /**
188        * Saves the modified value in the in-place editor for this field.
189        */
190       save() {
191         const fieldModel = this.fieldModel;
192         const editorModel = this.model;
193         const backstageId = `quickedit_backstage-${this.fieldModel.id.replace(
194           /[/[\]_\s]/g,
195           '-',
196         )}`;
197
198         function fillAndSubmitForm(value) {
199           const $form = $(`#${backstageId}`).find('form');
200           // Fill in the value in any <input> that isn't hidden or a submit
201           // button.
202           $form
203             .find(':input[type!="hidden"][type!="submit"]:not(select)')
204             // Don't mess with the node summary.
205             .not('[name$="\\[summary\\]"]')
206             .val(value);
207           // Submit the form.
208           $form.find('.quickedit-form-submit').trigger('click.quickedit');
209         }
210
211         const formOptions = {
212           fieldID: this.fieldModel.get('fieldID'),
213           $el: this.$el,
214           nocssjs: true,
215           other_view_modes: fieldModel.findOtherViewModes(),
216           // Reset an existing entry for this entity in the PrivateTempStore (if
217           // any) when saving the field. Logically speaking, this should happen in
218           // a separate request because this is an entity-level operation, not a
219           // field-level operation. But that would require an additional request,
220           // that might not even be necessary: it is only when a user saves a
221           // first changed field for an entity that this needs to happen:
222           // precisely now!
223           reset: !this.fieldModel.get('entity').get('inTempStore'),
224         };
225
226         const self = this;
227         Drupal.quickedit.util.form.load(formOptions, (form, ajax) => {
228           // Create a backstage area for storing forms that are hidden from view
229           // (hence "backstage" — since the editing doesn't happen in the form, it
230           // happens "directly" in the content, the form is only used for saving).
231           const $backstage = $(
232             Drupal.theme('quickeditBackstage', { id: backstageId }),
233           ).appendTo('body');
234           // Hidden forms are stuffed into the backstage container for this field.
235           const $form = $(form).appendTo($backstage);
236           // Disable the browser's HTML5 validation; we only care about server-
237           // side validation. (Not disabling this will actually cause problems
238           // because browsers don't like to set HTML5 validation errors on hidden
239           // forms.)
240           $form.prop('novalidate', true);
241           const $submit = $form.find('.quickedit-form-submit');
242           self.formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(
243             formOptions,
244             $submit,
245           );
246
247           function removeHiddenForm() {
248             Drupal.quickedit.util.form.unajaxifySaving(self.formSaveAjax);
249             delete self.formSaveAjax;
250             $backstage.remove();
251           }
252
253           // Successfully saved.
254           self.formSaveAjax.commands.quickeditFieldFormSaved = function(
255             ajax,
256             response,
257             status,
258           ) {
259             removeHiddenForm();
260             // First, transition the state to 'saved'.
261             fieldModel.set('state', 'saved');
262             // Second, set the 'htmlForOtherViewModes' attribute, so that when
263             // this field is rerendered, the change can be propagated to other
264             // instances of this field, which may be displayed in different view
265             // modes.
266             fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
267             // Finally, set the 'html' attribute on the field model. This will
268             // cause the field to be rerendered.
269             fieldModel.set('html', response.data);
270           };
271
272           // Unsuccessfully saved; validation errors.
273           self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function(
274             ajax,
275             response,
276             status,
277           ) {
278             removeHiddenForm();
279             editorModel.set('validationErrors', response.data);
280             fieldModel.set('state', 'invalid');
281           };
282
283           // The quickeditFieldForm AJAX command is only called upon loading the
284           // form for the first time, and when there are validation errors in the
285           // form; Form API then marks which form items have errors. This is
286           // useful for the form-based in-place editor, but pointless for any
287           // other: the form itself won't be visible at all anyway! So, we just
288           // ignore it.
289           self.formSaveAjax.commands.quickeditFieldForm = function() {};
290
291           fillAndSubmitForm(editorModel.get('currentValue'));
292         });
293       },
294
295       /**
296        * Shows validation error messages.
297        *
298        * Should be called when the state is changed to 'invalid'.
299        */
300       showValidationErrors() {
301         const $errors = $(
302           '<div class="quickedit-validation-errors"></div>',
303         ).append(this.model.get('validationErrors'));
304         this.getEditedElement()
305           .addClass('quickedit-validation-error')
306           .after($errors);
307       },
308
309       /**
310        * Cleans up validation error messages.
311        *
312        * Should be called when the state is changed to 'candidate' or 'saving'. In
313        * the case of the latter: the user has modified the value in the in-place
314        * editor again to attempt to save again. In the case of the latter: the
315        * invalid value was discarded.
316        */
317       removeValidationErrors() {
318         this.getEditedElement()
319           .removeClass('quickedit-validation-error')
320           .next('.quickedit-validation-errors')
321           .remove();
322       },
323     },
324   );
325 })(jQuery, Backbone, Drupal);