comparison core/modules/quickedit/js/views/EditorView.es6.js @ 4:a9cd425dd02b

Update, including to Drupal core 8.6.10
author Chris Cannam
date Thu, 28 Feb 2019 13:11:55 +0000
parents c75dbcec494b
children
comparison
equal deleted inserted replaced
3:307d7a7fd348 4:a9cd425dd02b
1 /** 1 /**
2 * @file 2 * @file
3 * An abstract Backbone View that controls an in-place editor. 3 * An abstract Backbone View that controls an in-place editor.
4 */ 4 */
5 5
6 (function ($, Backbone, Drupal) { 6 (function($, Backbone, Drupal) {
7 Drupal.quickedit.EditorView = Backbone.View.extend(/** @lends Drupal.quickedit.EditorView# */{ 7 Drupal.quickedit.EditorView = Backbone.View.extend(
8 8 /** @lends Drupal.quickedit.EditorView# */ {
9 /** 9 /**
10 * A base implementation that outlines the structure for in-place editors. 10 * A base implementation that outlines the structure for in-place editors.
11 * 11 *
12 * Specific in-place editor implementations should subclass (extend) this 12 * Specific in-place editor implementations should subclass (extend) this
13 * View and override whichever method they deem necessary to override. 13 * View and override whichever method they deem necessary to override.
14 * 14 *
15 * Typically you would want to override this method to set the 15 * Typically you would want to override this method to set the
16 * originalValue attribute in the FieldModel to such a value that your 16 * originalValue attribute in the FieldModel to such a value that your
17 * in-place editor can revert to the original value when necessary. 17 * in-place editor can revert to the original value when necessary.
18 * 18 *
19 * @example 19 * @example
20 * <caption>If you override this method, you should call this 20 * <caption>If you override this method, you should call this
21 * method (the parent class' initialize()) first.</caption> 21 * method (the parent class' initialize()) first.</caption>
22 * Drupal.quickedit.EditorView.prototype.initialize.call(this, options); 22 * Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
23 * 23 *
24 * @constructs 24 * @constructs
25 * 25 *
26 * @augments Backbone.View 26 * @augments Backbone.View
27 * 27 *
28 * @param {object} options 28 * @param {object} options
29 * An object with the following keys: 29 * An object with the following keys:
30 * @param {Drupal.quickedit.EditorModel} options.model 30 * @param {Drupal.quickedit.EditorModel} options.model
31 * The in-place editor state model. 31 * The in-place editor state model.
32 * @param {Drupal.quickedit.FieldModel} options.fieldModel 32 * @param {Drupal.quickedit.FieldModel} options.fieldModel
33 * The field model. 33 * The field model.
34 * 34 *
35 * @see Drupal.quickedit.EditorModel 35 * @see Drupal.quickedit.EditorModel
36 * @see Drupal.quickedit.editors.plain_text 36 * @see Drupal.quickedit.editors.plain_text
37 */ 37 */
38 initialize(options) { 38 initialize(options) {
39 this.fieldModel = options.fieldModel; 39 this.fieldModel = options.fieldModel;
40 this.listenTo(this.fieldModel, 'change:state', this.stateChange); 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 },
41 }, 323 },
42 324 );
43 /** 325 })(jQuery, Backbone, Drupal);
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));