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