formEditor.es6.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /**
  2. * @file
  3. * Form-based in-place editor. Works for any field type.
  4. */
  5. (function($, Drupal, _) {
  6. /**
  7. * @constructor
  8. *
  9. * @augments Drupal.quickedit.EditorView
  10. */
  11. Drupal.quickedit.editors.form = Drupal.quickedit.EditorView.extend(
  12. /** @lends Drupal.quickedit.editors.form# */ {
  13. /**
  14. * Tracks form container DOM element that is used while in-place editing.
  15. *
  16. * @type {jQuery}
  17. */
  18. $formContainer: null,
  19. /**
  20. * Holds the {@link Drupal.Ajax} object.
  21. *
  22. * @type {Drupal.Ajax}
  23. */
  24. formSaveAjax: null,
  25. /**
  26. * @inheritdoc
  27. *
  28. * @param {object} fieldModel
  29. * The field model that holds the state.
  30. * @param {string} state
  31. * The state to change to.
  32. */
  33. stateChange(fieldModel, state) {
  34. const from = fieldModel.previous('state');
  35. const to = state;
  36. switch (to) {
  37. case 'inactive':
  38. break;
  39. case 'candidate':
  40. if (from !== 'inactive') {
  41. this.removeForm();
  42. }
  43. break;
  44. case 'highlighted':
  45. break;
  46. case 'activating':
  47. // If coming from an invalid state, then the form is already loaded.
  48. if (from !== 'invalid') {
  49. this.loadForm();
  50. }
  51. break;
  52. case 'active':
  53. break;
  54. case 'changed':
  55. break;
  56. case 'saving':
  57. this.save();
  58. break;
  59. case 'saved':
  60. break;
  61. case 'invalid':
  62. this.showValidationErrors();
  63. break;
  64. }
  65. },
  66. /**
  67. * @inheritdoc
  68. *
  69. * @return {object}
  70. * A settings object for the quick edit UI.
  71. */
  72. getQuickEditUISettings() {
  73. return {
  74. padding: true,
  75. unifiedToolbar: true,
  76. fullWidthToolbar: true,
  77. popup: true,
  78. };
  79. },
  80. /**
  81. * Loads the form for this field, displays it on top of the actual field.
  82. */
  83. loadForm() {
  84. const fieldModel = this.fieldModel;
  85. // Generate a DOM-compatible ID for the form container DOM element.
  86. const id = `quickedit-form-for-${fieldModel.id.replace(
  87. /[/[\]]/g,
  88. '_',
  89. )}`;
  90. // Render form container.
  91. const $formContainer = $(
  92. Drupal.theme('quickeditFormContainer', {
  93. id,
  94. loadingMsg: Drupal.t('Loading…'),
  95. }),
  96. );
  97. this.$formContainer = $formContainer;
  98. $formContainer
  99. .find('.quickedit-form')
  100. .addClass(
  101. 'quickedit-editable quickedit-highlighted quickedit-editing',
  102. )
  103. .attr('role', 'dialog');
  104. // Insert form container in DOM.
  105. if (this.$el.css('display') === 'inline') {
  106. $formContainer.prependTo(this.$el.offsetParent());
  107. // Position the form container to render on top of the field's element.
  108. const pos = this.$el.position();
  109. $formContainer.css('left', pos.left).css('top', pos.top);
  110. } else {
  111. $formContainer.insertBefore(this.$el);
  112. }
  113. // Load form, insert it into the form container and attach event handlers.
  114. const formOptions = {
  115. fieldID: fieldModel.get('fieldID'),
  116. $el: this.$el,
  117. nocssjs: false,
  118. // Reset an existing entry for this entity in the PrivateTempStore (if
  119. // any) when loading the field. Logically speaking, this should happen
  120. // in a separate request because this is an entity-level operation, not
  121. // a field-level operation. But that would require an additional
  122. // request, that might not even be necessary: it is only when a user
  123. // loads a first changed field for an entity that this needs to happen:
  124. // precisely now!
  125. reset: !fieldModel.get('entity').get('inTempStore'),
  126. };
  127. Drupal.quickedit.util.form.load(formOptions, (form, ajax) => {
  128. Drupal.AjaxCommands.prototype.insert(ajax, {
  129. data: form,
  130. selector: `#${id} .placeholder`,
  131. });
  132. $formContainer
  133. .on('formUpdated.quickedit', ':input', event => {
  134. const state = fieldModel.get('state');
  135. // If the form is in an invalid state, it will persist on the page.
  136. // Set the field to activating so that the user can correct the
  137. // invalid value.
  138. if (state === 'invalid') {
  139. fieldModel.set('state', 'activating');
  140. }
  141. // Otherwise assume that the fieldModel is in a candidate state and
  142. // set it to changed on formUpdate.
  143. else {
  144. fieldModel.set('state', 'changed');
  145. }
  146. })
  147. .on('keypress.quickedit', 'input', event => {
  148. if (event.keyCode === 13) {
  149. return false;
  150. }
  151. });
  152. // The in-place editor has loaded; change state to 'active'.
  153. fieldModel.set('state', 'active');
  154. });
  155. },
  156. /**
  157. * Removes the form for this field, detaches behaviors and event handlers.
  158. */
  159. removeForm() {
  160. if (this.$formContainer === null) {
  161. return;
  162. }
  163. delete this.formSaveAjax;
  164. // Allow form widgets to detach properly.
  165. Drupal.detachBehaviors(this.$formContainer.get(0), null, 'unload');
  166. this.$formContainer
  167. .off('change.quickedit', ':input')
  168. .off('keypress.quickedit', 'input')
  169. .remove();
  170. this.$formContainer = null;
  171. },
  172. /**
  173. * @inheritdoc
  174. */
  175. save() {
  176. const $formContainer = this.$formContainer;
  177. const $submit = $formContainer.find('.quickedit-form-submit');
  178. const editorModel = this.model;
  179. const fieldModel = this.fieldModel;
  180. // Create an AJAX object for the form associated with the field.
  181. let formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(
  182. {
  183. nocssjs: false,
  184. other_view_modes: fieldModel.findOtherViewModes(),
  185. },
  186. $submit,
  187. );
  188. function cleanUpAjax() {
  189. Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
  190. formSaveAjax = null;
  191. }
  192. // Successfully saved.
  193. formSaveAjax.commands.quickeditFieldFormSaved = function(
  194. ajax,
  195. response,
  196. status,
  197. ) {
  198. cleanUpAjax();
  199. // First, transition the state to 'saved'.
  200. fieldModel.set('state', 'saved');
  201. // Second, set the 'htmlForOtherViewModes' attribute, so that when this
  202. // field is rerendered, the change can be propagated to other instances
  203. // of this field, which may be displayed in different view modes.
  204. fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
  205. // Finally, set the 'html' attribute on the field model. This will cause
  206. // the field to be rerendered.
  207. _.defer(() => {
  208. fieldModel.set('html', response.data);
  209. });
  210. };
  211. // Unsuccessfully saved; validation errors.
  212. formSaveAjax.commands.quickeditFieldFormValidationErrors = function(
  213. ajax,
  214. response,
  215. status,
  216. ) {
  217. editorModel.set('validationErrors', response.data);
  218. fieldModel.set('state', 'invalid');
  219. };
  220. // The quickeditFieldForm AJAX command is called upon attempting to save
  221. // the form; Form API will mark which form items have errors, if any. This
  222. // command is invoked only if validation errors exist and then it runs
  223. // before editFieldFormValidationErrors().
  224. formSaveAjax.commands.quickeditFieldForm = function(
  225. ajax,
  226. response,
  227. status,
  228. ) {
  229. Drupal.AjaxCommands.prototype.insert(ajax, {
  230. data: response.data,
  231. selector: `#${$formContainer.attr('id')} form`,
  232. });
  233. };
  234. // Click the form's submit button; the scoped AJAX commands above will
  235. // handle the server's response.
  236. $submit.trigger('click.quickedit');
  237. },
  238. /**
  239. * @inheritdoc
  240. */
  241. showValidationErrors() {
  242. this.$formContainer
  243. .find('.quickedit-form')
  244. .addClass('quickedit-validation-error')
  245. .find('form')
  246. .prepend(this.model.get('validationErrors'));
  247. },
  248. },
  249. );
  250. })(jQuery, Drupal, _);