/** * @file * Extends the Drupal AJAX functionality to integrate the dialog API. */ (function ($, Drupal, { focusable }) { /** * Initialize dialogs for Ajax purposes. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches the behaviors for dialog ajax functionality. */ Drupal.behaviors.dialog = { attach(context, settings) { const $context = $(context); // Provide a known 'drupal-modal' DOM element for Drupal-based modal // dialogs. Non-modal dialogs are responsible for creating their own // elements, since there can be multiple non-modal dialogs at a time. if (!$('#drupal-modal').length) { // Add 'ui-front' jQuery UI class so jQuery UI widgets like autocomplete // sit on top of dialogs. For more information see // http://api.jqueryui.com/theming/stacking-elements/. $('
') .hide() .appendTo('body'); } // Special behaviors specific when attaching content within a dialog. // These behaviors usually fire after a validation error inside a dialog. const $dialog = $context.closest('.ui-dialog-content'); if ($dialog.length) { // Remove and replace the dialog buttons with those from the new form. if ($dialog.dialog('option', 'drupalAutoButtons')) { // Trigger an event to detect/sync changes to buttons. $dialog.trigger('dialogButtonsChange'); } setTimeout(function () { // Account for pre-existing focus handling that may have already moved // the focus inside the dialog. if (!$dialog[0].contains(document.activeElement)) { // Move focus to the first focusable element in the next event loop // to allow dialog buttons to be changed first. $dialog.dialog('instance')._focusedElement = null; $dialog.dialog('instance')._focusTabbable(); } }, 0); } const originalClose = settings.dialog.close; // Overwrite the close method to remove the dialog on closing. settings.dialog.close = function (event, ...args) { originalClose.apply(settings.dialog, [event, ...args]); // Check if the opener element is inside an AJAX container. const $element = $(event.target); const ajaxContainer = $element.data('uiDialog') ? $element .data('uiDialog') .opener.closest('[data-drupal-ajax-container]') : []; // If the opener element was in an ajax container, and focus is on the // body element, we can assume focus was lost. To recover, focus is // moved to the first focusable element in the container. if ( ajaxContainer.length && (document.activeElement === document.body || $(document.activeElement).not(':visible')) ) { const focusableChildren = focusable(ajaxContainer[0]); if (focusableChildren.length > 0) { setTimeout(() => { focusableChildren[0].focus(); }, 0); } } $(event.target).remove(); }; }, /** * Scan a dialog for any primary buttons and move them to the button area. * * @param {jQuery} $dialog * A jQuery object containing the element that is the dialog target. * * @return {Array} * An array of buttons that need to be added to the button area. */ prepareDialogButtons($dialog) { const buttons = []; const $buttons = $dialog.find( '.form-actions input[type=submit], .form-actions a.button, .form-actions a.action-link', ); $buttons.each(function () { const $originalButton = $(this); this.style.display = 'none'; buttons.push({ text: $originalButton.html() || $originalButton.attr('value'), class: $originalButton.attr('class'), 'data-once': $originalButton.data('once'), click(e) { // If the original button is an anchor tag, triggering the "click" // event will not simulate a click. Use the click method instead. if ($originalButton[0].tagName === 'A') { $originalButton[0].click(); } else { $originalButton .trigger('mousedown') .trigger('mouseup') .trigger('click'); } e.preventDefault(); }, }); }); return buttons; }, }; /** * Command to open a dialog. * * @param {Drupal.Ajax} ajax * The Drupal Ajax object. * @param {object} response * Object holding the server response. * @param {number} [status] * The HTTP status code. * * @return {boolean|undefined} * Returns false if there was no selector property in the response object. */ Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) { if (!response.selector) { return false; } let $dialog = $(response.selector); if (!$dialog.length) { // Create the element if needed. $dialog = $( ``, ).appendTo('body'); } // Set up the wrapper, if there isn't one. if (!ajax.wrapper) { ajax.wrapper = $dialog.attr('id'); } // Use the ajax.js insert command to populate the dialog contents. response.command = 'insert'; response.method = 'html'; ajax.commands.insert(ajax, response, status); // Move the buttons to the jQuery UI dialog buttons area. response.dialogOptions = response.dialogOptions || {}; if (typeof response.dialogOptions.drupalAutoButtons === 'undefined') { response.dialogOptions.drupalAutoButtons = true; } else if (response.dialogOptions.drupalAutoButtons === 'false') { response.dialogOptions.drupalAutoButtons = false; } else { response.dialogOptions.drupalAutoButtons = !!response.dialogOptions.drupalAutoButtons; } if ( !response.dialogOptions.buttons && response.dialogOptions.drupalAutoButtons ) { response.dialogOptions.buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog); } // Bind dialogButtonsChange. $dialog.on('dialogButtonsChange', () => { const buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog); $dialog.dialog('option', 'buttons', buttons); }); // Open the dialog itself. response.dialogOptions = response.dialogOptions || {}; const dialog = Drupal.dialog($dialog.get(0), response.dialogOptions); if (response.dialogOptions.modal) { dialog.showModal(); } else { dialog.show(); } // Add the standard Drupal class for buttons for style consistency. $dialog.parent().find('.ui-dialog-buttonset').addClass('form-actions'); }; /** * Command to close a dialog. * * If no selector is given, it defaults to trying to close the modal. * * @param {Drupal.Ajax} [ajax] * The ajax object. * @param {object} response * Object holding the server response. * @param {string} response.selector * The selector of the dialog. * @param {boolean} response.persist * Whether to persist the dialog element or not. * @param {number} [status] * The HTTP status code. */ Drupal.AjaxCommands.prototype.closeDialog = function ( ajax, response, status, ) { const $dialog = $(response.selector); if ($dialog.length) { Drupal.dialog($dialog.get(0)).close(); if (!response.persist) { $dialog.remove(); } } // Unbind dialogButtonsChange. $dialog.off('dialogButtonsChange'); }; /** * Command to set a dialog property. * * JQuery UI specific way of setting dialog options. * * @param {Drupal.Ajax} [ajax] * The Drupal Ajax object. * @param {object} response * Object holding the server response. * @param {string} response.selector * Selector for the dialog element. * @param {string} response.optionsName * Name of a key to set. * @param {string} response.optionValue * Value to set. * @param {number} [status] * The HTTP status code. */ Drupal.AjaxCommands.prototype.setDialogOption = function ( ajax, response, status, ) { const $dialog = $(response.selector); if ($dialog.length) { $dialog.dialog('option', response.optionName, response.optionValue); } }; /** * Binds a listener on dialog creation to handle the cancel link. * * @param {jQuery.Event} e * The event triggered. * @param {Drupal.dialog~dialogDefinition} dialog * The dialog instance. * @param {jQuery} $element * The jQuery collection of the dialog element. * @param {object} [settings] * Dialog settings. */ $(window).on('dialog:aftercreate', (e, dialog, $element, settings) => { $element.on('click.dialog', '.dialog-cancel', (e) => { dialog.close('cancel'); e.preventDefault(); e.stopPropagation(); }); }); /** * Removes all 'dialog' listeners. * * @param {jQuery.Event} e * The event triggered. * @param {Drupal.dialog~dialogDefinition} dialog * The dialog instance. * @param {jQuery} $element * jQuery collection of the dialog element. */ $(window).on('dialog:beforeclose', (e, dialog, $element) => { $element.off('.dialog'); }); })(jQuery, Drupal, window.tabbable);