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