3 * An abstract Backbone View that controls an in-place editor.
6 (function ($, Backbone, Drupal) {
10 Drupal.quickedit.EditorView = Backbone.View.extend(/** @lends Drupal.quickedit.EditorView# */{
13 * A base implementation that outlines the structure for in-place editors.
15 * Specific in-place editor implementations should subclass (extend) this
16 * View and override whichever method they deem necessary to override.
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.
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);
29 * @augments Backbone.View
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
38 * @see Drupal.quickedit.EditorModel
39 * @see Drupal.quickedit.editors.plain_text
41 initialize: function (options) {
42 this.fieldModel = options.fieldModel;
43 this.listenTo(this.fieldModel, 'change:state', this.stateChange);
50 // The el property is the field, which should not be removed. Remove the
51 // pointer to it, then call Backbone.View.prototype.remove().
53 Backbone.View.prototype.remove.call(this);
57 * Returns the edited element.
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.
67 * A jQuery-wrapped DOM element.
69 * @see Drupal.quickedit.editors.plain_text
71 getEditedElement: function () {
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
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.
88 getQuickEditUISettings: function () {
89 return {padding: false, unifiedToolbar: false, fullWidthToolbar: false, popup: false};
93 * Determines the actions to take given a change of state.
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}.
101 stateChange: function (fieldModel, state) {
102 var from = fieldModel.previous('state');
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.
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
114 if (from === 'invalid') {
115 this.removeValidationErrors();
120 // Nothing to do for the typical in-place editor: it should not be
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.
134 loadDependencies(function () {
135 fieldModel.set('state', 'active');
140 // The user can now actually use the in-place editor.
144 // Nothing to do for the typical in-place editor. The UI will show an
145 // indicator that the field has changed.
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
154 if (from === 'invalid') {
155 this.removeValidationErrors();
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).
168 // The modified field value was attempted to be saved, but there were
169 // validation errors.
170 this.showValidationErrors();
176 * Reverts the modified value to the original, before editing started.
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.
185 * Saves the modified value in the in-place editor for this field.
188 var fieldModel = this.fieldModel;
189 var editorModel = this.model;
190 var backstageId = 'quickedit_backstage-' + this.fieldModel.id.replace(/[\/\[\]\_\s]/g, '-');
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
196 $form.find(':input[type!="hidden"][type!="submit"]:not(select)')
197 // Don't mess with the node summary.
198 .not('[name$="\\[summary\\]"]').val(value);
200 $form.find('.quickedit-form-submit').trigger('click.quickedit');
204 fieldID: this.fieldModel.get('fieldID'),
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:
215 reset: !this.fieldModel.get('entity').get('inTempStore')
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
230 $form.prop('novalidate', true);
231 var $submit = $form.find('.quickedit-form-submit');
232 self.formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(formOptions, $submit);
234 function removeHiddenForm() {
235 Drupal.quickedit.util.form.unajaxifySaving(self.formSaveAjax);
236 delete self.formSaveAjax;
240 // Successfully saved.
241 self.formSaveAjax.commands.quickeditFieldFormSaved = function (ajax, response, status) {
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
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);
255 // Unsuccessfully saved; validation errors.
256 self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function (ajax, response, status) {
258 editorModel.set('validationErrors', response.data);
259 fieldModel.set('state', 'invalid');
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
268 self.formSaveAjax.commands.quickeditFieldForm = function () {};
270 fillAndSubmitForm(editorModel.get('currentValue'));
275 * Shows validation error messages.
277 * Should be called when the state is changed to 'invalid'.
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')
288 * Cleans up validation error messages.
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.
295 removeValidationErrors: function () {
296 this.getEditedElement()
297 .removeClass('quickedit-validation-error')
298 .next('.quickedit-validation-errors')
304 }(jQuery, Backbone, Drupal));