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