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