Chris@0: /**
Chris@0: * @file
Chris@0: * An abstract Backbone View that controls an in-place editor.
Chris@0: */
Chris@0:
Chris@17: (function($, Backbone, Drupal) {
Chris@17: Drupal.quickedit.EditorView = Backbone.View.extend(
Chris@17: /** @lends Drupal.quickedit.EditorView# */ {
Chris@17: /**
Chris@17: * A base implementation that outlines the structure for in-place editors.
Chris@17: *
Chris@17: * Specific in-place editor implementations should subclass (extend) this
Chris@17: * View and override whichever method they deem necessary to override.
Chris@17: *
Chris@17: * Typically you would want to override this method to set the
Chris@17: * originalValue attribute in the FieldModel to such a value that your
Chris@17: * in-place editor can revert to the original value when necessary.
Chris@17: *
Chris@17: * @example
Chris@17: *
If you override this method, you should call this
Chris@17: * method (the parent class' initialize()) first.
Chris@17: * Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
Chris@17: *
Chris@17: * @constructs
Chris@17: *
Chris@17: * @augments Backbone.View
Chris@17: *
Chris@17: * @param {object} options
Chris@17: * An object with the following keys:
Chris@17: * @param {Drupal.quickedit.EditorModel} options.model
Chris@17: * The in-place editor state model.
Chris@17: * @param {Drupal.quickedit.FieldModel} options.fieldModel
Chris@17: * The field model.
Chris@17: *
Chris@17: * @see Drupal.quickedit.EditorModel
Chris@17: * @see Drupal.quickedit.editors.plain_text
Chris@17: */
Chris@17: initialize(options) {
Chris@17: this.fieldModel = options.fieldModel;
Chris@17: this.listenTo(this.fieldModel, 'change:state', this.stateChange);
Chris@17: },
Chris@0:
Chris@17: /**
Chris@17: * @inheritdoc
Chris@17: */
Chris@17: remove() {
Chris@17: // The el property is the field, which should not be removed. Remove the
Chris@17: // pointer to it, then call Backbone.View.prototype.remove().
Chris@17: this.setElement();
Chris@17: Backbone.View.prototype.remove.call(this);
Chris@17: },
Chris@0:
Chris@17: /**
Chris@17: * Returns the edited element.
Chris@17: *
Chris@17: * For some single cardinality fields, it may be necessary or useful to
Chris@17: * not in-place edit (and hence decorate) the DOM element with the
Chris@17: * data-quickedit-field-id attribute (which is the field's wrapper), but a
Chris@17: * specific element within the field's wrapper.
Chris@17: * e.g. using a WYSIWYG editor on a body field should happen on the DOM
Chris@17: * element containing the text itself, not on the field wrapper.
Chris@17: *
Chris@17: * @return {jQuery}
Chris@17: * A jQuery-wrapped DOM element.
Chris@17: *
Chris@17: * @see Drupal.quickedit.editors.plain_text
Chris@17: */
Chris@17: getEditedElement() {
Chris@17: return this.$el;
Chris@17: },
Chris@0:
Chris@17: /**
Chris@17: *
Chris@17: * @return {object}
Chris@17: * Returns 3 Quick Edit UI settings that depend on the in-place editor:
Chris@17: * - Boolean padding: indicates whether padding should be applied to the
Chris@17: * edited element, to guarantee legibility of text.
Chris@17: * - Boolean unifiedToolbar: provides the in-place editor with the ability
Chris@17: * to insert its own toolbar UI into Quick Edit's tightly integrated
Chris@17: * toolbar.
Chris@17: * - Boolean fullWidthToolbar: indicates whether Quick Edit's tightly
Chris@17: * integrated toolbar should consume the full width of the element,
Chris@17: * rather than being just long enough to accommodate a label.
Chris@17: */
Chris@17: getQuickEditUISettings() {
Chris@17: return {
Chris@17: padding: false,
Chris@17: unifiedToolbar: false,
Chris@17: fullWidthToolbar: false,
Chris@17: popup: false,
Chris@17: };
Chris@17: },
Chris@0:
Chris@17: /**
Chris@17: * Determines the actions to take given a change of state.
Chris@17: *
Chris@17: * @param {Drupal.quickedit.FieldModel} fieldModel
Chris@17: * The quickedit `FieldModel` that holds the state.
Chris@17: * @param {string} state
Chris@17: * The state of the associated field. One of
Chris@17: * {@link Drupal.quickedit.FieldModel.states}.
Chris@17: */
Chris@17: stateChange(fieldModel, state) {
Chris@17: const from = fieldModel.previous('state');
Chris@17: const to = state;
Chris@17: switch (to) {
Chris@17: case 'inactive':
Chris@17: // An in-place editor view will not yet exist in this state, hence
Chris@17: // this will never be reached. Listed for sake of completeness.
Chris@17: break;
Chris@0:
Chris@17: case 'candidate':
Chris@17: // Nothing to do for the typical in-place editor: it should not be
Chris@17: // visible yet. Except when we come from the 'invalid' state, then we
Chris@17: // clean up.
Chris@17: if (from === 'invalid') {
Chris@17: this.removeValidationErrors();
Chris@17: }
Chris@17: break;
Chris@0:
Chris@17: case 'highlighted':
Chris@17: // Nothing to do for the typical in-place editor: it should not be
Chris@17: // visible yet.
Chris@17: break;
Chris@17:
Chris@17: case 'activating': {
Chris@17: // The user has indicated he wants to do in-place editing: if
Chris@17: // something needs to be loaded (CSS/JavaScript/server data/…), then
Chris@17: // do so at this stage, and once the in-place editor is ready,
Chris@17: // set the 'active' state. A "loading" indicator will be shown in the
Chris@17: // UI for as long as the field remains in this state.
Chris@17: const loadDependencies = function(callback) {
Chris@17: // Do the loading here.
Chris@17: callback();
Chris@17: };
Chris@17: loadDependencies(() => {
Chris@17: fieldModel.set('state', 'active');
Chris@17: });
Chris@17: break;
Chris@0: }
Chris@0:
Chris@17: case 'active':
Chris@17: // The user can now actually use the in-place editor.
Chris@17: break;
Chris@0:
Chris@17: case 'changed':
Chris@17: // Nothing to do for the typical in-place editor. The UI will show an
Chris@17: // indicator that the field has changed.
Chris@17: break;
Chris@17:
Chris@17: case 'saving':
Chris@17: // When the user has indicated he wants to save his changes to this
Chris@17: // field, this state will be entered. If the previous saving attempt
Chris@17: // resulted in validation errors, the previous state will be
Chris@17: // 'invalid'. Clean up those validation errors while the user is
Chris@17: // saving.
Chris@17: if (from === 'invalid') {
Chris@17: this.removeValidationErrors();
Chris@17: }
Chris@17: this.save();
Chris@17: break;
Chris@17:
Chris@17: case 'saved':
Chris@17: // Nothing to do for the typical in-place editor. Immediately after
Chris@17: // being saved, a field will go to the 'candidate' state, where it
Chris@17: // should no longer be visible (after all, the field will then again
Chris@17: // just be a *candidate* to be in-place edited).
Chris@17: break;
Chris@17:
Chris@17: case 'invalid':
Chris@17: // The modified field value was attempted to be saved, but there were
Chris@17: // validation errors.
Chris@17: this.showValidationErrors();
Chris@17: break;
Chris@17: }
Chris@17: },
Chris@17:
Chris@17: /**
Chris@17: * Reverts the modified value to the original, before editing started.
Chris@17: */
Chris@17: revert() {
Chris@17: // A no-op by default; each editor should implement reverting itself.
Chris@17: // Note that if the in-place editor does not cause the FieldModel's
Chris@17: // element to be modified, then nothing needs to happen.
Chris@17: },
Chris@17:
Chris@17: /**
Chris@17: * Saves the modified value in the in-place editor for this field.
Chris@17: */
Chris@17: save() {
Chris@17: const fieldModel = this.fieldModel;
Chris@17: const editorModel = this.model;
Chris@17: const backstageId = `quickedit_backstage-${this.fieldModel.id.replace(
Chris@17: /[/[\]_\s]/g,
Chris@17: '-',
Chris@17: )}`;
Chris@17:
Chris@17: function fillAndSubmitForm(value) {
Chris@17: const $form = $(`#${backstageId}`).find('form');
Chris@17: // Fill in the value in any that isn't hidden or a submit
Chris@17: // button.
Chris@17: $form
Chris@17: .find(':input[type!="hidden"][type!="submit"]:not(select)')
Chris@17: // Don't mess with the node summary.
Chris@17: .not('[name$="\\[summary\\]"]')
Chris@17: .val(value);
Chris@17: // Submit the form.
Chris@17: $form.find('.quickedit-form-submit').trigger('click.quickedit');
Chris@14: }
Chris@0:
Chris@17: const formOptions = {
Chris@17: fieldID: this.fieldModel.get('fieldID'),
Chris@17: $el: this.$el,
Chris@17: nocssjs: true,
Chris@17: other_view_modes: fieldModel.findOtherViewModes(),
Chris@17: // Reset an existing entry for this entity in the PrivateTempStore (if
Chris@17: // any) when saving the field. Logically speaking, this should happen in
Chris@17: // a separate request because this is an entity-level operation, not a
Chris@17: // field-level operation. But that would require an additional request,
Chris@17: // that might not even be necessary: it is only when a user saves a
Chris@17: // first changed field for an entity that this needs to happen:
Chris@17: // precisely now!
Chris@17: reset: !this.fieldModel.get('entity').get('inTempStore'),
Chris@0: };
Chris@0:
Chris@17: const self = this;
Chris@17: Drupal.quickedit.util.form.load(formOptions, (form, ajax) => {
Chris@17: // Create a backstage area for storing forms that are hidden from view
Chris@17: // (hence "backstage" — since the editing doesn't happen in the form, it
Chris@17: // happens "directly" in the content, the form is only used for saving).
Chris@17: const $backstage = $(
Chris@17: Drupal.theme('quickeditBackstage', { id: backstageId }),
Chris@17: ).appendTo('body');
Chris@17: // Hidden forms are stuffed into the backstage container for this field.
Chris@17: const $form = $(form).appendTo($backstage);
Chris@17: // Disable the browser's HTML5 validation; we only care about server-
Chris@17: // side validation. (Not disabling this will actually cause problems
Chris@17: // because browsers don't like to set HTML5 validation errors on hidden
Chris@17: // forms.)
Chris@17: $form.prop('novalidate', true);
Chris@17: const $submit = $form.find('.quickedit-form-submit');
Chris@17: self.formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(
Chris@17: formOptions,
Chris@17: $submit,
Chris@17: );
Chris@0:
Chris@17: function removeHiddenForm() {
Chris@17: Drupal.quickedit.util.form.unajaxifySaving(self.formSaveAjax);
Chris@17: delete self.formSaveAjax;
Chris@17: $backstage.remove();
Chris@17: }
Chris@0:
Chris@17: // Successfully saved.
Chris@17: self.formSaveAjax.commands.quickeditFieldFormSaved = function(
Chris@17: ajax,
Chris@17: response,
Chris@17: status,
Chris@17: ) {
Chris@17: removeHiddenForm();
Chris@17: // First, transition the state to 'saved'.
Chris@17: fieldModel.set('state', 'saved');
Chris@17: // Second, set the 'htmlForOtherViewModes' attribute, so that when
Chris@17: // this field is rerendered, the change can be propagated to other
Chris@17: // instances of this field, which may be displayed in different view
Chris@17: // modes.
Chris@17: fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
Chris@17: // Finally, set the 'html' attribute on the field model. This will
Chris@17: // cause the field to be rerendered.
Chris@17: fieldModel.set('html', response.data);
Chris@17: };
Chris@17:
Chris@17: // Unsuccessfully saved; validation errors.
Chris@17: self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function(
Chris@17: ajax,
Chris@17: response,
Chris@17: status,
Chris@17: ) {
Chris@17: removeHiddenForm();
Chris@17: editorModel.set('validationErrors', response.data);
Chris@17: fieldModel.set('state', 'invalid');
Chris@17: };
Chris@17:
Chris@17: // The quickeditFieldForm AJAX command is only called upon loading the
Chris@17: // form for the first time, and when there are validation errors in the
Chris@17: // form; Form API then marks which form items have errors. This is
Chris@17: // useful for the form-based in-place editor, but pointless for any
Chris@17: // other: the form itself won't be visible at all anyway! So, we just
Chris@17: // ignore it.
Chris@17: self.formSaveAjax.commands.quickeditFieldForm = function() {};
Chris@17:
Chris@17: fillAndSubmitForm(editorModel.get('currentValue'));
Chris@17: });
Chris@17: },
Chris@17:
Chris@17: /**
Chris@17: * Shows validation error messages.
Chris@17: *
Chris@17: * Should be called when the state is changed to 'invalid'.
Chris@17: */
Chris@17: showValidationErrors() {
Chris@17: const $errors = $(
Chris@17: '',
Chris@17: ).append(this.model.get('validationErrors'));
Chris@17: this.getEditedElement()
Chris@17: .addClass('quickedit-validation-error')
Chris@17: .after($errors);
Chris@17: },
Chris@17:
Chris@17: /**
Chris@17: * Cleans up validation error messages.
Chris@17: *
Chris@17: * Should be called when the state is changed to 'candidate' or 'saving'. In
Chris@17: * the case of the latter: the user has modified the value in the in-place
Chris@17: * editor again to attempt to save again. In the case of the latter: the
Chris@17: * invalid value was discarded.
Chris@17: */
Chris@17: removeValidationErrors() {
Chris@17: this.getEditedElement()
Chris@17: .removeClass('quickedit-validation-error')
Chris@17: .next('.quickedit-validation-errors')
Chris@17: .remove();
Chris@17: },
Chris@0: },
Chris@17: );
Chris@17: })(jQuery, Backbone, Drupal);