annotate core/modules/media_library/js/media_library.ui.es6.js @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
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);