1/** 2 * @file 3 * Extends the Drupal AJAX functionality to integrate the dialog API. 4 */ 5 6(function ($, Drupal, { focusable }) { 7 /** 8 * Initialize dialogs for Ajax purposes. 9 * 10 * @type {Drupal~behavior} 11 * 12 * @prop {Drupal~behaviorAttach} attach 13 * Attaches the behaviors for dialog ajax functionality. 14 */ 15 Drupal.behaviors.dialog = { 16 attach(context, settings) { 17 // Provide a known 'drupal-modal' DOM element for Drupal-based modal 18 // dialogs. Non-modal dialogs are responsible for creating their own 19 // elements, since there can be multiple non-modal dialogs at a time. 20 if (!document.querySelector('#drupal-modal')) { 21 // Add 'ui-front' jQuery UI class so jQuery UI widgets like autocomplete 22 // sit on top of dialogs. For more information see 23 // http://api.jqueryui.com/theming/stacking-elements/. 24 document.body.insertAdjacentHTML( 25 'beforeend', 26 '<div id="drupal-modal" class="ui-front" style="display:none"></div>', 27 ); 28 } 29 30 // Special behaviors specific when attaching content within a dialog. 31 // These behaviors usually fire after a validation error inside a dialog. 32 if (context !== document) { 33 const dialog = context.closest('.ui-dialog-content'); 34 if (dialog) { 35 // Remove and replace the dialog buttons with those from the new form. 36 if ($(dialog).dialog('option', 'drupalAutoButtons')) { 37 // Trigger an event to detect/sync changes to buttons. 38 dialog.dispatchEvent(new CustomEvent('dialogButtonsChange')); 39 } 40 41 setTimeout(function () { 42 // Account for pre-existing focus handling that may have already moved 43 // the focus inside the dialog. 44 if (!dialog.contains(document.activeElement)) { 45 // Move focus to the first focusable element in the next event loop 46 // to allow dialog buttons to be changed first. 47 48 // Replicate the autofocus behavior of jQuery UI Dialog. 49 50 const autoFocusElement = dialog?.querySelector('[autofocus]'); 51 52 if (autoFocusElement) { 53 autoFocusElement.focus(); 54 return; 55 } 56 57 const focusableElements = focusable(dialog); 58 if (focusableElements.length > 0) { 59 focusableElements[0].focus(); 60 return; 61 } 62 63 const dialogWrapper = dialog.closest('.ui-dialog'); 64 65 const buttonPane = dialogWrapper?.querySelector( 66 '.ui-dialog-buttonpane', 67 ); 68 const focusableButtons = focusable(buttonPane); 69 if (focusableButtons.length > 0) { 70 focusableButtons[0].focus(); 71 return; 72 } 73 74 const closeButton = dialogWrapper?.querySelector( 75 '.ui-dialog-titlebar-close', 76 ); 77 if (closeButton) { 78 closeButton.focus(); 79 return; 80 } 81 82 dialog.focus(); 83 } 84 }, 0); 85 } 86 } 87 88 const originalClose = settings.dialog.close; 89 // Overwrite the close method to remove the dialog on closing. 90 settings.dialog.close = function (event, ...args) { 91 originalClose.apply(settings.dialog, [event, ...args]); 92 // Check if the opener element is inside an AJAX container. 93 const $element = $(event.target); 94 const ajaxContainer = $element.data('uiDialog') 95 ? $element 96 .data('uiDialog') 97 .opener.closest('[data-drupal-ajax-container]') 98 : []; 99 100 // If the opener element was in an ajax container, and focus is on the 101 // body element, we can assume focus was lost. To recover, focus is 102 // moved to the first focusable element in the container. 103 if ( 104 ajaxContainer.length && 105 (document.activeElement === document.body || 106 $(document.activeElement).not(':visible')) 107 ) {
108 const focusableChildren = focusable(ajaxContainer[0]); 109 if (focusableChildren.length > 0) { 110 setTimeout(() => { 111 focusableChildren[0].focus(); 112 }, 0); 113 } 114 } 115 $(event.target).remove(); 116 }; 117 }, 118 119 /** 120 * Scan a dialog for any primary buttons and move them to the button area. 121 * 122 * @param {jQuery} $dialog 123 * A jQuery object containing the element that is the dialog target. 124 * 125 * @return {Array} 126 * An array of buttons that need to be added to the button area. 127 */ 128 prepareDialogButtons($dialog) { 129 const buttons = []; 130 const buttonSelectors = 131 '.form-actions input[type=submit], .form-actions a.button, .form-actions a.action-link'; 132 const buttonElements = $dialog[0].querySelectorAll(buttonSelectors); 133 134 buttonElements.forEach((button) => { 135 button.style.display = 'none'; 136 buttons.push({ 137 text: button.innerHTML || button.getAttribute('value'), 138 class: button.getAttribute('class'), 139 'data-once': button.dataset.once, 140 click(e) { 141 if (button.tagName === 'A') { 142 button.click(); 143 } else { 144 ['mousedown', 'mouseup', 'click'].forEach((event) => 145 button.dispatchEvent(new MouseEvent(event)), 146 ); 147 } 148 e.preventDefault(); 149 }, 150 }); 151 }); 152 return buttons; 153 }, 154 }; 155 156 /** 157 * Command to open a dialog. 158 * 159 * @param {Drupal.Ajax} ajax 160 * The Drupal Ajax object. 161 * @param {object} response 162 * Object holding the server response. 163 * @param {number} [status] 164 * The HTTP status code. 165 * 166 * @return {boolean|undefined} 167 * Returns false if there was no selector property in the response object. 168 */ 169 Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) { 170 if (!response.selector) { 171 return false; 172 } 173 let dialog = document.querySelector(response.selector); 174 if (!dialog) { 175 // Create the element if needed. 176 dialog = document.createElement('div'); 177 dialog.id = response.selector.replace(/^#/, ''); 178 dialog.classList.add('ui-front'); 179 document.body.appendChild(dialog); 180 } 181 // Set up the wrapper, if there isn't one. 182 if (!ajax.wrapper) { 183 ajax.wrapper = dialog.id; 184 } 185 186 // Use the ajax.js insert command to populate the dialog contents. 187 response.command = 'insert'; 188 response.method = 'html'; 189 ajax.commands.insert(ajax, response, status); 190 191 // Move the buttons to the jQuery UI dialog buttons area. 192 response.dialogOptions = response.dialogOptions || {}; 193 if (typeof response.dialogOptions.drupalAutoButtons === 'undefined') { 194 response.dialogOptions.drupalAutoButtons = true; 195 } else if (response.dialogOptions.drupalAutoButtons === 'false') { 196 response.dialogOptions.drupalAutoButtons = false; 197 } else { 198 response.dialogOptions.drupalAutoButtons = 199 !!response.dialogOptions.drupalAutoButtons; 200 } 201 if ( 202 !response.dialogOptions.buttons && 203 response.dialogOptions.drupalAutoButtons 204 ) { 205 response.dialogOptions.buttons = 206 Drupal.behaviors.dialog.prepareDialogButtons($(dialog)); 207 } 208 209 const dialogButtonsChange = () => { 210 const buttons = Drupal.behaviors.dialog.prepareDialogButtons($(dialog)); 211 $(dialog).dialog('option', 'buttons', buttons); 212 }; 213 214 // Bind dialogButtonsChange. 215 dialog.addEventListener('dialogButtonsChange', dialogButtonsChange); 216 dialog.addEventListener('dialog:beforeclose', (event) => { 217 dialog.removeEventListener('dialogButtonsChange', dialogButtonsChange); 218 }); 219 220 // Open the dialog itself. 221 const createdDialog = Drupal.dialog(dialog, response.dialogOptions); 222 if (response.dialogOptions.modal) { 223 createdDialog.showModal(); 224 } else { 225 createdDialog.show(); 226 } 227 228 // Add the standard Drupal class for buttons for style consistency. 229 dialog.parentElement 230 ?.querySelector('.ui-dialog-buttonset') 231 ?.classList.add('form-actions'); 232 }; 233 234 /** 235 * Command to close a dialog. 236 * 237 * If no selector is given, it defaults to trying to close the modal. 238 * 239 * @param {Drupal.Ajax} [ajax] 240 * The ajax object. 241 * @param {object} response 242 * Object holding the server response. 243 * @param {string} response.selector 244 * The selector of the dialog. 245 * @param {boolean} response.persist 246 * Whether to persist the dialog element or not. 247 * @param {number} [status] 248 * The HTTP status code. 249 */ 250 Drupal.AjaxCommands.prototype.closeDialog = function ( 251 ajax, 252 response, 253 status, 254 ) { 255 const dialog = document.querySelector(response.selector); 256 if (dialog) { 257 Drupal.dialog(dialog).close(); 258 if (!response.persist) { 259 dialog.remove(); 260 } 261 } 262 }; 263 264 /** 265 * Command to set a dialog property. 266 * 267 * JQuery UI specific way of setting dialog options. 268 * 269 * @param {Drupal.Ajax} [ajax] 270 * The Drupal Ajax object. 271 * @param {object} response 272 * Object holding the server response. 273 * @param {string} response.selector 274 * Selector for the dialog element. 275 * @param {string} response.optionsName
276 * Name of a key to set. 277 * @param {string} response.optionValue 278 * Value to set. 279 * @param {number} [status] 280 * The HTTP status code. 281 */ 282 Drupal.AjaxCommands.prototype.setDialogOption = function ( 283 ajax, 284 response, 285 status, 286 ) { 287 const dialog = document.querySelector(response.selector); 288 if (dialog) { 289 $(dialog).dialog('option', response.optionName, response.optionValue); 290 } 291 }; 292 293 /** 294 * Binds a listener on dialog creation to handle the cancel link. 295 * 296 * @param {DrupalDialogEvent} e 297 * The event triggered. 298 * @param {Drupal.dialog~dialogDefinition} dialog 299 * The dialog instance. 300 * @param {object} [settings] 301 * Dialog settings. 302 */ 303 window.addEventListener('dialog:aftercreate', (event) => { 304 const dialog = event.dialog; 305 const cancelButton = event.target.querySelector('.dialog-cancel'); 306 const cancelClick = (e) => { 307 dialog.close('cancel'); 308 e.preventDefault(); 309 e.stopPropagation(); 310 }; 311 cancelButton?.removeEventListener('click', cancelClick); 312 cancelButton?.addEventListener('click', cancelClick); 313 }); 314 315 /** 316 * Ajax command to open URL in a modal dialog. 317 * 318 * @param {Drupal.Ajax} [ajax] 319 * An Ajax object. 320 * @param {object} response 321 * The Ajax response. 322 */ 323 Drupal.AjaxCommands.prototype.openModalDialogWithUrl = function ( 324 ajax, 325 response, 326 ) { 327 const dialogOptions = response.dialogOptions || {}; 328 const elementSettings = { 329 progress: { type: 'throbber' }, 330 dialogType: 'modal', 331 dialog: dialogOptions, 332 url: response.url, 333 httpMethod: 'GET', 334 }; 335 Drupal.ajax(elementSettings).execute(); 336 }; 337})(jQuery, Drupal, window.tabbable);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.