media_library.ui.es6.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /**
  2. * @file media_library.ui.es6.js
  3. */
  4. (($, Drupal, window) => {
  5. /**
  6. * Wrapper object for the current state of the media library.
  7. */
  8. Drupal.MediaLibrary = {
  9. /**
  10. * When a user interacts with the media library we want the selection to
  11. * persist as long as the media library modal is opened. We temporarily
  12. * store the selected items while the user filters the media library view or
  13. * navigates to different tabs.
  14. */
  15. currentSelection: [],
  16. };
  17. /**
  18. * Command to update the current media library selection.
  19. *
  20. * @param {Drupal.Ajax} [ajax]
  21. * The Drupal Ajax object.
  22. * @param {object} response
  23. * Object holding the server response.
  24. * @param {number} [status]
  25. * The HTTP status code.
  26. */
  27. Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function(
  28. ajax,
  29. response,
  30. status,
  31. ) {
  32. Object.values(response.mediaIds).forEach(value => {
  33. Drupal.MediaLibrary.currentSelection.push(value);
  34. });
  35. };
  36. /**
  37. * Load media library content through AJAX.
  38. *
  39. * Standard AJAX links (using the 'use-ajax' class) replace the entire library
  40. * dialog. When navigating to a media type through the vertical tabs, we only
  41. * want to load the changed library content. This is not only more efficient,
  42. * but also provides a more accessible user experience for screen readers.
  43. *
  44. * @type {Drupal~behavior}
  45. *
  46. * @prop {Drupal~behaviorAttach} attach
  47. * Attaches behavior to vertical tabs in the media library.
  48. *
  49. * @todo Remove when the AJAX system adds support for replacing a specific
  50. * selector via a link.
  51. * https://www.drupal.org/project/drupal/issues/3026636
  52. */
  53. Drupal.behaviors.MediaLibraryTabs = {
  54. attach(context) {
  55. const $menu = $('.js-media-library-menu');
  56. $menu
  57. .find('a', context)
  58. .once('media-library-menu-item')
  59. .on('keypress', e => {
  60. // The AJAX link has the button role, so we need to make sure the link
  61. // is also triggered when pressing the spacebar.
  62. if (e.which === 32) {
  63. e.preventDefault();
  64. e.stopPropagation();
  65. $(e.currentTarget).trigger('click');
  66. }
  67. })
  68. .on('click', e => {
  69. e.preventDefault();
  70. e.stopPropagation();
  71. // Replace the library content.
  72. const ajaxObject = Drupal.ajax({
  73. wrapper: 'media-library-content',
  74. url: e.currentTarget.href,
  75. dialogType: 'ajax',
  76. progress: {
  77. type: 'fullscreen',
  78. message: Drupal.t('Please wait...'),
  79. },
  80. });
  81. // Override the AJAX success callback to shift focus to the media
  82. // library content.
  83. ajaxObject.success = function(response, status) {
  84. // Remove the progress element.
  85. if (this.progress.element) {
  86. $(this.progress.element).remove();
  87. }
  88. if (this.progress.object) {
  89. this.progress.object.stopMonitoring();
  90. }
  91. $(this.element).prop('disabled', false);
  92. // Execute the AJAX commands.
  93. Object.keys(response || {}).forEach(i => {
  94. if (response[i].command && this.commands[response[i].command]) {
  95. this.commands[response[i].command](this, response[i], status);
  96. }
  97. });
  98. // Set focus to the first tabbable element in the media library
  99. // content.
  100. $('#media-library-content :tabbable:first').focus();
  101. // Remove any response-specific settings so they don't get used on
  102. // the next call by mistake.
  103. this.settings = null;
  104. };
  105. ajaxObject.execute();
  106. // Set the selected tab.
  107. $menu.find('.active-tab').remove();
  108. $menu.find('a').removeClass('active');
  109. $(e.currentTarget)
  110. .addClass('active')
  111. .html(
  112. Drupal.t(
  113. '<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span><span class="active-tab visually-hidden"> (selected)</span>',
  114. { '@title': $(e.currentTarget).data('title') },
  115. ),
  116. );
  117. // Announce the updated content.
  118. Drupal.announce(
  119. Drupal.t('Showing @title media.', {
  120. '@title': $(e.currentTarget).data('title'),
  121. }),
  122. );
  123. });
  124. },
  125. };
  126. /**
  127. * Load media library displays through AJAX.
  128. *
  129. * Standard AJAX links (using the 'use-ajax' class) replace the entire library
  130. * dialog. When navigating to a media library views display, we only want to
  131. * load the changed views display content. This is not only more efficient,
  132. * but also provides a more accessible user experience for screen readers.
  133. *
  134. * @type {Drupal~behavior}
  135. *
  136. * @prop {Drupal~behaviorAttach} attach
  137. * Attaches behavior to vertical tabs in the media library.
  138. *
  139. * @todo Remove when the AJAX system adds support for replacing a specific
  140. * selector via a link.
  141. * https://www.drupal.org/project/drupal/issues/3026636
  142. */
  143. Drupal.behaviors.MediaLibraryViewsDisplay = {
  144. attach(context) {
  145. const $view = $(context).hasClass('.js-media-library-view')
  146. ? $(context)
  147. : $('.js-media-library-view', context);
  148. // Add a class to the view to allow it to be replaced via AJAX.
  149. // @todo Remove the custom ID when the AJAX system allows replacing
  150. // elements by selector.
  151. // https://www.drupal.org/project/drupal/issues/2821793
  152. $view
  153. .closest('.views-element-container')
  154. .attr('id', 'media-library-view');
  155. // We would ideally use a generic JavaScript specific class to detect the
  156. // display links. Since we have no good way of altering display links yet,
  157. // this is the best we can do for now.
  158. // @todo Add media library specific classes and data attributes to the
  159. // media library display links when we can alter display links.
  160. // https://www.drupal.org/project/drupal/issues/3036694
  161. $('.views-display-link-widget, .views-display-link-widget_table', context)
  162. .once('media-library-views-display-link')
  163. .on('click', e => {
  164. e.preventDefault();
  165. e.stopPropagation();
  166. const $link = $(e.currentTarget);
  167. // Add a loading and display announcement for screen reader users.
  168. let loadingAnnouncement = '';
  169. let displayAnnouncement = '';
  170. let focusSelector = '';
  171. if ($link.hasClass('views-display-link-widget')) {
  172. loadingAnnouncement = Drupal.t('Loading grid view.');
  173. displayAnnouncement = Drupal.t('Changed to grid view.');
  174. focusSelector = '.views-display-link-widget';
  175. } else if ($link.hasClass('views-display-link-widget_table')) {
  176. loadingAnnouncement = Drupal.t('Loading table view.');
  177. displayAnnouncement = Drupal.t('Changed to table view.');
  178. focusSelector = '.views-display-link-widget_table';
  179. }
  180. // Replace the library view.
  181. const ajaxObject = Drupal.ajax({
  182. wrapper: 'media-library-view',
  183. url: e.currentTarget.href,
  184. dialogType: 'ajax',
  185. progress: {
  186. type: 'fullscreen',
  187. message: loadingAnnouncement || Drupal.t('Please wait...'),
  188. },
  189. });
  190. // Override the AJAX success callback to announce the updated content
  191. // to screen readers.
  192. if (displayAnnouncement || focusSelector) {
  193. const success = ajaxObject.success;
  194. ajaxObject.success = function(response, status) {
  195. success.bind(this)(response, status);
  196. // The AJAX link replaces the whole view, including the clicked
  197. // link. Move the focus back to the clicked link when the view is
  198. // replaced.
  199. if (focusSelector) {
  200. $(focusSelector).focus();
  201. }
  202. // Announce the new view is loaded to screen readers.
  203. if (displayAnnouncement) {
  204. Drupal.announce(displayAnnouncement);
  205. }
  206. };
  207. }
  208. ajaxObject.execute();
  209. // Announce the new view is being loaded to screen readers.
  210. // @todo Replace custom announcement when
  211. // https://www.drupal.org/project/drupal/issues/2973140 is in.
  212. if (loadingAnnouncement) {
  213. Drupal.announce(loadingAnnouncement);
  214. }
  215. });
  216. },
  217. };
  218. /**
  219. * Update the media library selection when loaded or media items are selected.
  220. *
  221. * @type {Drupal~behavior}
  222. *
  223. * @prop {Drupal~behaviorAttach} attach
  224. * Attaches behavior to select media items.
  225. */
  226. Drupal.behaviors.MediaLibraryItemSelection = {
  227. attach(context, settings) {
  228. const $form = $(
  229. '.js-media-library-views-form, .js-media-library-add-form',
  230. context,
  231. );
  232. const currentSelection = Drupal.MediaLibrary.currentSelection;
  233. if (!$form.length) {
  234. return;
  235. }
  236. const $mediaItems = $(
  237. '.js-media-library-item input[type="checkbox"]',
  238. $form,
  239. );
  240. /**
  241. * Disable media items.
  242. *
  243. * @param {jQuery} $items
  244. * A jQuery object representing the media items that should be disabled.
  245. */
  246. function disableItems($items) {
  247. $items
  248. .prop('disabled', true)
  249. .closest('.js-media-library-item')
  250. .addClass('media-library-item--disabled');
  251. }
  252. /**
  253. * Enable media items.
  254. *
  255. * @param {jQuery} $items
  256. * A jQuery object representing the media items that should be enabled.
  257. */
  258. function enableItems($items) {
  259. $items
  260. .prop('disabled', false)
  261. .closest('.js-media-library-item')
  262. .removeClass('media-library-item--disabled');
  263. }
  264. /**
  265. * Update the number of selected items in the button pane.
  266. *
  267. * @param {number} remaining
  268. * The number of remaining slots.
  269. */
  270. function updateSelectionCount(remaining) {
  271. // When the remaining number of items is a negative number, we allow an
  272. // unlimited number of items. In that case we don't want to show the
  273. // number of remaining slots.
  274. const selectItemsText =
  275. remaining < 0
  276. ? Drupal.formatPlural(
  277. currentSelection.length,
  278. '1 item selected',
  279. '@count items selected',
  280. )
  281. : Drupal.formatPlural(
  282. remaining,
  283. '@selected of @count item selected',
  284. '@selected of @count items selected',
  285. {
  286. '@selected': currentSelection.length,
  287. },
  288. );
  289. // The selected count div could have been created outside of the
  290. // context, so we unfortunately can't use context here.
  291. $('.js-media-library-selected-count').html(selectItemsText);
  292. }
  293. // Update the selection array and the hidden form field when a media item
  294. // is selected.
  295. $mediaItems.once('media-item-change').on('change', e => {
  296. const id = e.currentTarget.value;
  297. // Update the selection.
  298. const position = currentSelection.indexOf(id);
  299. if (e.currentTarget.checked) {
  300. // Check if the ID is not already in the selection and add if needed.
  301. if (position === -1) {
  302. currentSelection.push(id);
  303. }
  304. } else if (position !== -1) {
  305. // Remove the ID when it is in the current selection.
  306. currentSelection.splice(position, 1);
  307. }
  308. // Set the selection in the hidden form element.
  309. $form
  310. .find('#media-library-modal-selection')
  311. .val(currentSelection.join())
  312. .trigger('change');
  313. // Set the selection in the media library add form. Since the form is
  314. // not necessarily loaded within the same context, we can't use the
  315. // context here.
  316. $('.js-media-library-add-form-current-selection').val(
  317. currentSelection.join(),
  318. );
  319. });
  320. // The hidden selection form field changes when the selection is updated.
  321. $('#media-library-modal-selection', $form)
  322. .once('media-library-selection-change')
  323. .on('change', e => {
  324. updateSelectionCount(settings.media_library.selection_remaining);
  325. // Prevent users from selecting more items than allowed.
  326. if (
  327. currentSelection.length ===
  328. settings.media_library.selection_remaining
  329. ) {
  330. disableItems($mediaItems.not(':checked'));
  331. enableItems($mediaItems.filter(':checked'));
  332. } else {
  333. enableItems($mediaItems);
  334. }
  335. });
  336. // Apply the current selection to the media library view. Changing the
  337. // checkbox values triggers the change event for the media items. The
  338. // change event handles updating the hidden selection field for the form.
  339. currentSelection.forEach(value => {
  340. $form
  341. .find(`input[type="checkbox"][value="${value}"]`)
  342. .prop('checked', true)
  343. .trigger('change');
  344. });
  345. // Add the selection count to the button pane when a media library dialog
  346. // is created.
  347. $(window)
  348. .once('media-library-selection-info')
  349. .on('dialog:aftercreate', () => {
  350. // Since the dialog HTML is not part of the context, we can't use
  351. // context here.
  352. const $buttonPane = $(
  353. '.media-library-widget-modal .ui-dialog-buttonpane',
  354. );
  355. if (!$buttonPane.length) {
  356. return;
  357. }
  358. $buttonPane.append(Drupal.theme('mediaLibrarySelectionCount'));
  359. updateSelectionCount(settings.media_library.selection_remaining);
  360. });
  361. },
  362. };
  363. /**
  364. * Clear the current selection.
  365. *
  366. * @type {Drupal~behavior}
  367. *
  368. * @prop {Drupal~behaviorAttach} attach
  369. * Attaches behavior to clear the selection when the library modal closes.
  370. */
  371. Drupal.behaviors.MediaLibraryModalClearSelection = {
  372. attach() {
  373. $(window)
  374. .once('media-library-clear-selection')
  375. .on('dialog:afterclose', () => {
  376. Drupal.MediaLibrary.currentSelection = [];
  377. });
  378. },
  379. };
  380. /**
  381. * Theme function for the selection count.
  382. *
  383. * @return {string}
  384. * The corresponding HTML.
  385. */
  386. Drupal.theme.mediaLibrarySelectionCount = function() {
  387. return `<div class="media-library-selected-count js-media-library-selected-count" role="status" aria-live="polite" aria-atomic="true"></div>`;
  388. };
  389. })(jQuery, Drupal, window);