Source: modules/image/js/editors/image.es6.js

  1. /**
  2. * @file
  3. * Drag+drop based in-place editor for images.
  4. */
  5. (function ($, _, Drupal) {
  6. Drupal.quickedit.editors.image = Drupal.quickedit.EditorView.extend(
  7. /** @lends Drupal.quickedit.editors.image# */ {
  8. /**
  9. * @constructs
  10. *
  11. * @augments Drupal.quickedit.EditorView
  12. *
  13. * @param {object} options
  14. * Options for the image editor.
  15. */
  16. initialize(options) {
  17. Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
  18. // Set our original value to our current HTML (for reverting).
  19. this.model.set('originalValue', this.$el.html().trim());
  20. // $.val() callback function for copying input from our custom form to
  21. // the Quick Edit Field Form.
  22. this.model.set('currentValue', function (index, value) {
  23. const matches = $(this)
  24. .attr('name')
  25. .match(/(alt|title)]$/);
  26. if (matches) {
  27. const name = matches[1];
  28. const $toolgroup = $(
  29. `#${options.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
  30. );
  31. const $input = $toolgroup.find(
  32. `.quickedit-image-field-info input[name="${name}"]`,
  33. );
  34. if ($input.length) {
  35. return $input.val();
  36. }
  37. }
  38. });
  39. },
  40. /**
  41. * {@inheritdoc}
  42. *
  43. * @param {Drupal.quickedit.FieldModel} fieldModel
  44. * The field model that holds the state.
  45. * @param {string} state
  46. * The state to change to.
  47. * @param {object} options
  48. * State options, if needed by the state change.
  49. */
  50. stateChange(fieldModel, state, options) {
  51. const from = fieldModel.previous('state');
  52. switch (state) {
  53. case 'inactive':
  54. break;
  55. case 'candidate':
  56. if (from !== 'inactive') {
  57. this.$el.find('.quickedit-image-dropzone').remove();
  58. this.$el.removeClass('quickedit-image-element');
  59. }
  60. if (from === 'invalid') {
  61. this.removeValidationErrors();
  62. }
  63. break;
  64. case 'highlighted':
  65. break;
  66. case 'activating':
  67. // Defer updating the field model until the current state change has
  68. // propagated, to not trigger a nested state change event.
  69. _.defer(() => {
  70. fieldModel.set('state', 'active');
  71. });
  72. break;
  73. case 'active': {
  74. const self = this;
  75. // Indicate that this element is being edited by Quick Edit Image.
  76. this.$el.addClass('quickedit-image-element');
  77. // Render our initial dropzone element. Once the user reverts changes
  78. // or saves a new image, this element is removed.
  79. const $dropzone = this.renderDropzone(
  80. 'upload',
  81. Drupal.t('Drop file here or click to upload'),
  82. );
  83. $dropzone.on('dragenter', function (e) {
  84. $(this).addClass('hover');
  85. });
  86. $dropzone.on('dragleave', function (e) {
  87. $(this).removeClass('hover');
  88. });
  89. $dropzone.on('drop', function (e) {
  90. // Only respond when a file is dropped (could be another element).
  91. if (
  92. e.originalEvent.dataTransfer &&
  93. e.originalEvent.dataTransfer.files.length
  94. ) {
  95. $(this).removeClass('hover');
  96. self.uploadImage(e.originalEvent.dataTransfer.files[0]);
  97. }
  98. });
  99. $dropzone.on('click', (e) => {
  100. // Create an <input> element without appending it to the DOM, and
  101. // trigger a click event. This is the easiest way to arbitrarily
  102. // open the browser's upload dialog.
  103. $('<input type="file">')
  104. .trigger('click')
  105. .on('change', function () {
  106. if (this.files.length) {
  107. self.uploadImage(this.files[0]);
  108. }
  109. });
  110. });
  111. // Prevent the browser's default behavior when dragging files onto
  112. // the document (usually opens them in the same tab).
  113. $dropzone.on('dragover dragenter dragleave drop click', (e) => {
  114. e.preventDefault();
  115. e.stopPropagation();
  116. });
  117. this.renderToolbar(fieldModel);
  118. break;
  119. }
  120. case 'changed':
  121. break;
  122. case 'saving':
  123. if (from === 'invalid') {
  124. this.removeValidationErrors();
  125. }
  126. this.save(options);
  127. break;
  128. case 'saved':
  129. break;
  130. case 'invalid':
  131. this.showValidationErrors();
  132. break;
  133. }
  134. },
  135. /**
  136. * Validates/uploads a given file.
  137. *
  138. * @param {File} file
  139. * The file to upload.
  140. */
  141. uploadImage(file) {
  142. // Indicate loading by adding a special class to our icon.
  143. this.renderDropzone(
  144. 'upload loading',
  145. Drupal.t('Uploading <i>@file</i>…', { '@file': file.name }),
  146. );
  147. // Build a valid URL for our endpoint.
  148. const fieldID = this.fieldModel.get('fieldID');
  149. const url = Drupal.quickedit.util.buildUrl(
  150. fieldID,
  151. Drupal.url(
  152. 'quickedit/image/upload/!entity_type/!id/!field_name/!langcode/!view_mode',
  153. ),
  154. );
  155. // Construct form data that our endpoint can consume.
  156. const data = new FormData();
  157. data.append('files[image]', file);
  158. // Construct a POST request to our endpoint.
  159. const self = this;
  160. this.ajax({
  161. type: 'POST',
  162. url,
  163. data,
  164. success(response) {
  165. const $el = $(self.fieldModel.get('el'));
  166. // Indicate that the field has changed - this enables the
  167. // "Save" button.
  168. self.fieldModel.set('state', 'changed');
  169. self.fieldModel.get('entity').set('inTempStore', true);
  170. self.removeValidationErrors();
  171. // Replace our html with the new image. If we replaced our entire
  172. // element with data.html, we would have to implement complicated logic
  173. // like what's in Drupal.quickedit.AppView.renderUpdatedField.
  174. const $content = $(response.html)
  175. .closest('[data-quickedit-field-id]')
  176. .children();
  177. $el.empty().append($content);
  178. },
  179. });
  180. },
  181. /**
  182. * Utility function to make an AJAX request to the server.
  183. *
  184. * In addition to formatting the correct request, this also handles error
  185. * codes and messages by displaying them visually inline with the image.
  186. *
  187. * Drupal.ajax is not called here as the Form API is unused by this
  188. * in-place editor, and our JSON requests/responses try to be
  189. * editor-agnostic. Ideally similar logic and routes could be used by
  190. * modules like CKEditor for drag+drop file uploads as well.
  191. *
  192. * @param {object} options
  193. * Ajax options.
  194. * @param {string} options.type
  195. * The type of request (i.e. GET, POST, PUT, DELETE, etc.)
  196. * @param {string} options.url
  197. * The URL for the request.
  198. * @param {*} options.data
  199. * The data to send to the server.
  200. * @param {function} options.success
  201. * A callback function used when a request is successful, without errors.
  202. */
  203. ajax(options) {
  204. const defaultOptions = {
  205. context: this,
  206. dataType: 'json',
  207. cache: false,
  208. contentType: false,
  209. processData: false,
  210. error() {
  211. this.renderDropzone(
  212. 'error',
  213. Drupal.t('A server error has occurred.'),
  214. );
  215. },
  216. };
  217. const ajaxOptions = $.extend(defaultOptions, options);
  218. const successCallback = ajaxOptions.success;
  219. // Handle the success callback.
  220. ajaxOptions.success = function (response) {
  221. if (response.main_error) {
  222. this.renderDropzone('error', response.main_error);
  223. if (response.errors.length) {
  224. this.model.set('validationErrors', response.errors);
  225. }
  226. this.showValidationErrors();
  227. } else {
  228. successCallback(response);
  229. }
  230. };
  231. $.ajax(ajaxOptions);
  232. },
  233. /**
  234. * Renders our toolbar form for editing metadata.
  235. *
  236. * @param {Drupal.quickedit.FieldModel} fieldModel
  237. * The current Field Model.
  238. */
  239. renderToolbar(fieldModel) {
  240. const $toolgroup = $(
  241. `#${fieldModel.toolbarView.getMainWysiwygToolgroupId()}`,
  242. );
  243. let $toolbar = $toolgroup.find('.quickedit-image-field-info');
  244. if ($toolbar.length === 0) {
  245. // Perform an AJAX request for extra image info (alt/title).
  246. const fieldID = fieldModel.get('fieldID');
  247. const url = Drupal.quickedit.util.buildUrl(
  248. fieldID,
  249. Drupal.url(
  250. 'quickedit/image/info/!entity_type/!id/!field_name/!langcode/!view_mode',
  251. ),
  252. );
  253. const self = this;
  254. self.ajax({
  255. type: 'GET',
  256. url,
  257. success(response) {
  258. $toolbar = $(Drupal.theme.quickeditImageToolbar(response));
  259. $toolgroup.append($toolbar);
  260. $toolbar.on('keyup paste', () => {
  261. fieldModel.set('state', 'changed');
  262. });
  263. // Re-position the toolbar, which could have changed size.
  264. fieldModel.get('entity').toolbarView.position();
  265. },
  266. });
  267. }
  268. },
  269. /**
  270. * Renders our dropzone element.
  271. *
  272. * @param {string} state
  273. * The current state of our editor. Only used for visual styling.
  274. * @param {string} text
  275. * The text to display in the dropzone area.
  276. *
  277. * @return {jQuery}
  278. * The rendered dropzone.
  279. */
  280. renderDropzone(state, text) {
  281. let $dropzone = this.$el.find('.quickedit-image-dropzone');
  282. // If the element already exists, modify its contents.
  283. if ($dropzone.length) {
  284. $dropzone
  285. .removeClass('upload error hover loading')
  286. .addClass(`.quickedit-image-dropzone ${state}`)
  287. .children('.quickedit-image-text')
  288. .html(text);
  289. } else {
  290. $dropzone = $(
  291. Drupal.theme('quickeditImageDropzone', {
  292. state,
  293. text,
  294. }),
  295. );
  296. this.$el.append($dropzone);
  297. }
  298. return $dropzone;
  299. },
  300. /**
  301. * {@inheritdoc}
  302. */
  303. revert() {
  304. this.$el.html(this.model.get('originalValue'));
  305. },
  306. /**
  307. * {@inheritdoc}
  308. */
  309. getQuickEditUISettings() {
  310. return {
  311. padding: false,
  312. unifiedToolbar: true,
  313. fullWidthToolbar: true,
  314. popup: false,
  315. };
  316. },
  317. /**
  318. * {@inheritdoc}
  319. */
  320. showValidationErrors() {
  321. const errors = Drupal.theme('quickeditImageErrors', {
  322. errors: this.model.get('validationErrors'),
  323. });
  324. $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`).append(
  325. errors,
  326. );
  327. this.getEditedElement().addClass('quickedit-validation-error');
  328. // Re-position the toolbar, which could have changed size.
  329. this.fieldModel.get('entity').toolbarView.position();
  330. },
  331. /**
  332. * {@inheritdoc}
  333. */
  334. removeValidationErrors() {
  335. $(`#${this.fieldModel.toolbarView.getMainWysiwygToolgroupId()}`)
  336. .find('.quickedit-image-errors')
  337. .remove();
  338. this.getEditedElement().removeClass('quickedit-validation-error');
  339. },
  340. },
  341. );
  342. })(jQuery, _, Drupal);