EditorView.es6.js 12 KB

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