Chris@18: /** Chris@18: * @file media_library.widget.js Chris@18: */ Chris@18: (($, Drupal, window) => { Chris@18: /** Chris@18: * Wrapper object for the current state of the media library. Chris@18: */ Chris@18: Drupal.MediaLibrary = { Chris@18: /** Chris@18: * When a user interacts with the media library we want the selection to Chris@18: * persist as long as the media library modal is opened. We temporarily Chris@18: * store the selected items while the user filters the media library view or Chris@18: * navigates to different tabs. Chris@18: */ Chris@18: currentSelection: [], Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Command to update the current media library selection. Chris@18: * Chris@18: * @param {Drupal.Ajax} [ajax] Chris@18: * The Drupal Ajax object. Chris@18: * @param {object} response Chris@18: * Object holding the server response. Chris@18: * @param {number} [status] Chris@18: * The HTTP status code. Chris@18: */ Chris@18: Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function( Chris@18: ajax, Chris@18: response, Chris@18: status, Chris@18: ) { Chris@18: Object.values(response.mediaIds).forEach(value => { Chris@18: Drupal.MediaLibrary.currentSelection.push(value); Chris@18: }); Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Warn users when clicking outgoing links from the library or widget. Chris@18: * Chris@18: * @type {Drupal~behavior} Chris@18: * Chris@18: * @prop {Drupal~behaviorAttach} attach Chris@18: * Attaches behavior to links in the media library. Chris@18: */ Chris@18: Drupal.behaviors.MediaLibraryWidgetWarn = { Chris@18: attach(context) { Chris@18: $('.js-media-library-item a[href]', context) Chris@18: .once('media-library-warn-link') Chris@18: .on('click', e => { Chris@18: const message = Drupal.t( Chris@18: 'Unsaved changes to the form will be lost. Are you sure you want to leave?', Chris@18: ); Chris@18: const confirmation = window.confirm(message); Chris@18: if (!confirmation) { Chris@18: e.preventDefault(); Chris@18: } Chris@18: }); Chris@18: }, Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Load media library content through AJAX. Chris@18: * Chris@18: * Standard AJAX links (using the 'use-ajax' class) replace the entire library Chris@18: * dialog. When navigating to a media type through the vertical tabs, we only Chris@18: * want to load the changed library content. This is not only more efficient, Chris@18: * but also provides a more accessible user experience for screen readers. Chris@18: * Chris@18: * @type {Drupal~behavior} Chris@18: * Chris@18: * @prop {Drupal~behaviorAttach} attach Chris@18: * Attaches behavior to vertical tabs in the media library. Chris@18: * Chris@18: * @todo Remove when the AJAX system adds support for replacing a specific Chris@18: * selector via a link. Chris@18: * https://www.drupal.org/project/drupal/issues/3026636 Chris@18: */ Chris@18: Drupal.behaviors.MediaLibraryTabs = { Chris@18: attach(context) { Chris@18: const $menu = $('.js-media-library-menu'); Chris@18: $menu Chris@18: .find('a', context) Chris@18: .once('media-library-menu-item') Chris@18: .on('click', e => { Chris@18: e.preventDefault(); Chris@18: e.stopPropagation(); Chris@18: Chris@18: // Replace the library content. Chris@18: const ajaxObject = Drupal.ajax({ Chris@18: wrapper: 'media-library-content', Chris@18: url: e.currentTarget.href, Chris@18: dialogType: 'ajax', Chris@18: progress: { Chris@18: type: 'fullscreen', Chris@18: message: Drupal.t('Please wait...'), Chris@18: }, Chris@18: }); Chris@18: Chris@18: // Override the AJAX success callback to shift focus to the media Chris@18: // library content. Chris@18: ajaxObject.success = function(response, status) { Chris@18: // Remove the progress element. Chris@18: if (this.progress.element) { Chris@18: $(this.progress.element).remove(); Chris@18: } Chris@18: if (this.progress.object) { Chris@18: this.progress.object.stopMonitoring(); Chris@18: } Chris@18: $(this.element).prop('disabled', false); Chris@18: Chris@18: // Execute the AJAX commands. Chris@18: Object.keys(response || {}).forEach(i => { Chris@18: if (response[i].command && this.commands[response[i].command]) { Chris@18: this.commands[response[i].command](this, response[i], status); Chris@18: } Chris@18: }); Chris@18: Chris@18: // Set focus to the media library content. Chris@18: document.getElementById('media-library-content').focus(); Chris@18: Chris@18: // Remove any response-specific settings so they don't get used on Chris@18: // the next call by mistake. Chris@18: this.settings = null; Chris@18: }; Chris@18: ajaxObject.execute(); Chris@18: Chris@18: // Set the active tab. Chris@18: $menu.find('.active-tab').remove(); Chris@18: $menu.find('a').removeClass('active'); Chris@18: $(e.currentTarget) Chris@18: .addClass('active') Chris@18: .html( Chris@18: Drupal.t( Chris@18: '@title (active tab)', Chris@18: { '@title': $(e.currentTarget).html() }, Chris@18: ), Chris@18: ); Chris@18: }); Chris@18: }, Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Load media library displays through AJAX. Chris@18: * Chris@18: * Standard AJAX links (using the 'use-ajax' class) replace the entire library Chris@18: * dialog. When navigating to a media library views display, we only want to Chris@18: * load the changed views display content. This is not only more efficient, Chris@18: * but also provides a more accessible user experience for screen readers. Chris@18: * Chris@18: * @type {Drupal~behavior} Chris@18: * Chris@18: * @prop {Drupal~behaviorAttach} attach Chris@18: * Attaches behavior to vertical tabs in the media library. Chris@18: * Chris@18: * @todo Remove when the AJAX system adds support for replacing a specific Chris@18: * selector via a link. Chris@18: * https://www.drupal.org/project/drupal/issues/3026636 Chris@18: */ Chris@18: Drupal.behaviors.MediaLibraryViewsDisplay = { Chris@18: attach(context) { Chris@18: const $view = $(context).hasClass('.js-media-library-view') Chris@18: ? $(context) Chris@18: : $('.js-media-library-view', context); Chris@18: Chris@18: // Add a class to the view to allow it to be replaced via AJAX. Chris@18: // @todo Remove the custom ID when the AJAX system allows replacing Chris@18: // elements by selector. Chris@18: // https://www.drupal.org/project/drupal/issues/2821793 Chris@18: $view Chris@18: .closest('.views-element-container') Chris@18: .attr('id', 'media-library-view'); Chris@18: Chris@18: // We would ideally use a generic JavaScript specific class to detect the Chris@18: // display links. Since we have no good way of altering display links yet, Chris@18: // this is the best we can do for now. Chris@18: // @todo Add media library specific classes and data attributes to the Chris@18: // media library display links when we can alter display links. Chris@18: // https://www.drupal.org/project/drupal/issues/3036694 Chris@18: $('.views-display-link-widget, .views-display-link-widget_table', context) Chris@18: .once('media-library-views-display-link') Chris@18: .on('click', e => { Chris@18: e.preventDefault(); Chris@18: e.stopPropagation(); Chris@18: Chris@18: const $link = $(e.currentTarget); Chris@18: Chris@18: // Add a loading and display announcement for screen reader users. Chris@18: let loadingAnnouncement = ''; Chris@18: let displayAnnouncement = ''; Chris@18: let focusSelector = ''; Chris@18: if ($link.hasClass('views-display-link-widget')) { Chris@18: loadingAnnouncement = Drupal.t('Loading grid view.'); Chris@18: displayAnnouncement = Drupal.t('Changed to grid view.'); Chris@18: focusSelector = '.views-display-link-widget'; Chris@18: } else if ($link.hasClass('views-display-link-widget_table')) { Chris@18: loadingAnnouncement = Drupal.t('Loading table view.'); Chris@18: displayAnnouncement = Drupal.t('Changed to table view.'); Chris@18: focusSelector = '.views-display-link-widget_table'; Chris@18: } Chris@18: Chris@18: // Replace the library view. Chris@18: const ajaxObject = Drupal.ajax({ Chris@18: wrapper: 'media-library-view', Chris@18: url: e.currentTarget.href, Chris@18: dialogType: 'ajax', Chris@18: progress: { Chris@18: type: 'fullscreen', Chris@18: message: loadingAnnouncement || Drupal.t('Please wait...'), Chris@18: }, Chris@18: }); Chris@18: Chris@18: // Override the AJAX success callback to announce the updated content Chris@18: // to screen readers. Chris@18: if (displayAnnouncement || focusSelector) { Chris@18: const success = ajaxObject.success; Chris@18: ajaxObject.success = function(response, status) { Chris@18: success.bind(this)(response, status); Chris@18: // The AJAX link replaces the whole view, including the clicked Chris@18: // link. Move the focus back to the clicked link when the view is Chris@18: // replaced. Chris@18: if (focusSelector) { Chris@18: $(focusSelector).focus(); Chris@18: } Chris@18: // Announce the new view is loaded to screen readers. Chris@18: if (displayAnnouncement) { Chris@18: Drupal.announce(displayAnnouncement); Chris@18: } Chris@18: }; Chris@18: } Chris@18: Chris@18: ajaxObject.execute(); Chris@18: Chris@18: // Announce the new view is being loaded to screen readers. Chris@18: // @todo Replace custom announcement when Chris@18: // https://www.drupal.org/project/drupal/issues/2973140 is in. Chris@18: if (loadingAnnouncement) { Chris@18: Drupal.announce(loadingAnnouncement); Chris@18: } Chris@18: }); Chris@18: }, Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Update the media library selection when loaded or media items are selected. Chris@18: * Chris@18: * @type {Drupal~behavior} Chris@18: * Chris@18: * @prop {Drupal~behaviorAttach} attach Chris@18: * Attaches behavior to select media items. Chris@18: */ Chris@18: Drupal.behaviors.MediaLibraryItemSelection = { Chris@18: attach(context, settings) { Chris@18: const $form = $( Chris@18: '.js-media-library-views-form, .js-media-library-add-form', Chris@18: context, Chris@18: ); Chris@18: const currentSelection = Drupal.MediaLibrary.currentSelection; Chris@18: Chris@18: if (!$form.length) { Chris@18: return; Chris@18: } Chris@18: Chris@18: const $mediaItems = $( Chris@18: '.js-media-library-item input[type="checkbox"]', Chris@18: $form, Chris@18: ); Chris@18: Chris@18: /** Chris@18: * Disable media items. Chris@18: * Chris@18: * @param {jQuery} $items Chris@18: * A jQuery object representing the media items that should be disabled. Chris@18: */ Chris@18: function disableItems($items) { Chris@18: $items Chris@18: .prop('disabled', true) Chris@18: .closest('.js-media-library-item') Chris@18: .addClass('media-library-item--disabled'); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Enable media items. Chris@18: * Chris@18: * @param {jQuery} $items Chris@18: * A jQuery object representing the media items that should be enabled. Chris@18: */ Chris@18: function enableItems($items) { Chris@18: $items Chris@18: .prop('disabled', false) Chris@18: .closest('.js-media-library-item') Chris@18: .removeClass('media-library-item--disabled'); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Update the number of selected items in the button pane. Chris@18: * Chris@18: * @param {number} remaining Chris@18: * The number of remaining slots. Chris@18: */ Chris@18: function updateSelectionCount(remaining) { Chris@18: // When the remaining number of items is a negative number, we allow an Chris@18: // unlimited number of items. In that case we don't want to show the Chris@18: // number of remaining slots. Chris@18: const selectItemsText = Chris@18: remaining < 0 Chris@18: ? Drupal.formatPlural( Chris@18: currentSelection.length, Chris@18: '1 item selected', Chris@18: '@count items selected', Chris@18: ) Chris@18: : Drupal.formatPlural( Chris@18: remaining, Chris@18: '@selected of @count item selected', Chris@18: '@selected of @count items selected', Chris@18: { Chris@18: '@selected': currentSelection.length, Chris@18: }, Chris@18: ); Chris@18: // The selected count div could have been created outside of the Chris@18: // context, so we unfortunately can't use context here. Chris@18: $('.js-media-library-selected-count').html(selectItemsText); Chris@18: } Chris@18: Chris@18: // Update the selection array and the hidden form field when a media item Chris@18: // is selected. Chris@18: $mediaItems.once('media-item-change').on('change', e => { Chris@18: const id = e.currentTarget.value; Chris@18: Chris@18: // Update the selection. Chris@18: const position = currentSelection.indexOf(id); Chris@18: if (e.currentTarget.checked) { Chris@18: // Check if the ID is not already in the selection and add if needed. Chris@18: if (position === -1) { Chris@18: currentSelection.push(id); Chris@18: } Chris@18: } else if (position !== -1) { Chris@18: // Remove the ID when it is in the current selection. Chris@18: currentSelection.splice(position, 1); Chris@18: } Chris@18: Chris@18: // Set the selection in the hidden form element. Chris@18: $form Chris@18: .find('#media-library-modal-selection') Chris@18: .val(currentSelection.join()) Chris@18: .trigger('change'); Chris@18: Chris@18: // Set the selection in the media library add form. Since the form is Chris@18: // not necessarily loaded within the same context, we can't use the Chris@18: // context here. Chris@18: $('.js-media-library-add-form-current-selection').val( Chris@18: currentSelection.join(), Chris@18: ); Chris@18: }); Chris@18: Chris@18: // The hidden selection form field changes when the selection is updated. Chris@18: $('#media-library-modal-selection', $form) Chris@18: .once('media-library-selection-change') Chris@18: .on('change', e => { Chris@18: updateSelectionCount(settings.media_library.selection_remaining); Chris@18: Chris@18: // Prevent users from selecting more items than allowed. Chris@18: if ( Chris@18: currentSelection.length === Chris@18: settings.media_library.selection_remaining Chris@18: ) { Chris@18: disableItems($mediaItems.not(':checked')); Chris@18: enableItems($mediaItems.filter(':checked')); Chris@18: } else { Chris@18: enableItems($mediaItems); Chris@18: } Chris@18: }); Chris@18: Chris@18: // Apply the current selection to the media library view. Changing the Chris@18: // checkbox values triggers the change event for the media items. The Chris@18: // change event handles updating the hidden selection field for the form. Chris@18: currentSelection.forEach(value => { Chris@18: $form Chris@18: .find(`input[type="checkbox"][value="${value}"]`) Chris@18: .prop('checked', true) Chris@18: .trigger('change'); Chris@18: }); Chris@18: Chris@18: // Add the selection count to the button pane when a media library dialog Chris@18: // is created. Chris@18: $(window) Chris@18: .once('media-library-selection-info') Chris@18: .on('dialog:aftercreate', () => { Chris@18: // Since the dialog HTML is not part of the context, we can't use Chris@18: // context here. Chris@18: const $buttonPane = $( Chris@18: '.media-library-widget-modal .ui-dialog-buttonpane', Chris@18: ); Chris@18: if (!$buttonPane.length) { Chris@18: return; Chris@18: } Chris@18: $buttonPane.append(Drupal.theme('mediaLibrarySelectionCount')); Chris@18: updateSelectionCount(settings.media_library.selection_remaining); Chris@18: }); Chris@18: }, Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Clear the current selection. Chris@18: * Chris@18: * @type {Drupal~behavior} Chris@18: * Chris@18: * @prop {Drupal~behaviorAttach} attach Chris@18: * Attaches behavior to clear the selection when the library modal closes. Chris@18: */ Chris@18: Drupal.behaviors.MediaLibraryModalClearSelection = { Chris@18: attach() { Chris@18: $(window) Chris@18: .once('media-library-clear-selection') Chris@18: .on('dialog:afterclose', () => { Chris@18: Drupal.MediaLibrary.currentSelection = []; Chris@18: }); Chris@18: }, Chris@18: }; Chris@18: Chris@18: /** Chris@18: * Theme function for the selection count. Chris@18: * Chris@18: * @return {string} Chris@18: * The corresponding HTML. Chris@18: */ Chris@18: Drupal.theme.mediaLibrarySelectionCount = function() { Chris@18: return `
`; Chris@18: }; Chris@18: })(jQuery, Drupal, window);