comparison core/modules/media_library/js/media_library.ui.es6.js @ 5:12f9dff5fda9 tip

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