Files
drupal11-ddev/web/core/misc/dialog/dialog.ajax.js
2025-10-08 11:39:17 -04:00

304 lines
9.9 KiB
JavaScript

/**
* @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) {
// 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 (!document.querySelector('#drupal-modal')) {
// 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/.
document.body.insertAdjacentHTML(
'beforeend',
'<div id="drupal-modal" class="ui-front" style="display:none"></div>',
);
}
// Special behaviors specific when attaching content within a dialog.
// These behaviors usually fire after a validation error inside a dialog.
if (context !== document) {
const dialog = context.closest('.ui-dialog-content');
if (dialog) {
// 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.dispatchEvent(new CustomEvent('dialogButtonsChange'));
}
setTimeout(function () {
// Account for pre-existing focus handling that may have already moved
// the focus inside the dialog.
if (!dialog.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 buttonSelectors =
'.form-actions input[type=submit], .form-actions a.button, .form-actions a.action-link';
const buttonElements = $dialog[0].querySelectorAll(buttonSelectors);
buttonElements.forEach((button) => {
button.style.display = 'none';
buttons.push({
text: button.innerHTML || button.getAttribute('value'),
class: button.getAttribute('class'),
'data-once': button.dataset.once,
click(e) {
if (button.tagName === 'A') {
button.click();
} else {
['mousedown', 'mouseup', 'click'].forEach((event) =>
button.dispatchEvent(new MouseEvent(event)),
);
}
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 = document.querySelector(response.selector);
if (!dialog) {
// Create the element if needed.
dialog = document.createElement('div');
dialog.id = response.selector.replace(/^#/, '');
dialog.classList.add('ui-front');
document.body.appendChild(dialog);
}
// Set up the wrapper, if there isn't one.
if (!ajax.wrapper) {
ajax.wrapper = dialog.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));
}
const dialogButtonsChange = () => {
const buttons = Drupal.behaviors.dialog.prepareDialogButtons($(dialog));
$(dialog).dialog('option', 'buttons', buttons);
};
// Bind dialogButtonsChange.
dialog.addEventListener('dialogButtonsChange', dialogButtonsChange);
dialog.addEventListener('dialog:beforeclose', (event) => {
dialog.removeEventListener('dialogButtonsChange', dialogButtonsChange);
});
// Open the dialog itself.
const createdDialog = Drupal.dialog(dialog, response.dialogOptions);
if (response.dialogOptions.modal) {
createdDialog.showModal();
} else {
createdDialog.show();
}
// Add the standard Drupal class for buttons for style consistency.
dialog.parentElement
?.querySelector('.ui-dialog-buttonset')
?.classList.add('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 = document.querySelector(response.selector);
if (dialog) {
Drupal.dialog(dialog).close();
if (!response.persist) {
dialog.remove();
}
}
};
/**
* 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 = document.querySelector(response.selector);
if (dialog) {
$(dialog).dialog('option', response.optionName, response.optionValue);
}
};
/**
* Binds a listener on dialog creation to handle the cancel link.
*
* @param {DrupalDialogEvent} e
* The event triggered.
* @param {Drupal.dialog~dialogDefinition} dialog
* The dialog instance.
* @param {object} [settings]
* Dialog settings.
*/
window.addEventListener('dialog:aftercreate', (event) => {
const dialog = event.dialog;
const cancelButton = event.target.querySelector('.dialog-cancel');
const cancelClick = (e) => {
dialog.close('cancel');
e.preventDefault();
e.stopPropagation();
};
cancelButton?.removeEventListener('click', cancelClick);
cancelButton?.addEventListener('click', cancelClick);
});
/**
* Ajax command to open URL in a modal dialog.
*
* @param {Drupal.Ajax} [ajax]
* An Ajax object.
* @param {object} response
* The Ajax response.
*/
Drupal.AjaxCommands.prototype.openModalDialogWithUrl = function (
ajax,
response,
) {
const dialogOptions = response.dialogOptions || {};
const elementSettings = {
progress: { type: 'throbber' },
dialogType: 'modal',
dialog: dialogOptions,
url: response.url,
httpMethod: 'GET',
};
Drupal.ajax(elementSettings).execute();
};
})(jQuery, Drupal, window.tabbable);