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