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 $(dialog).dialog('instance')._focusedElement = null; 48 $(dialog).dialog('instance')._focusTabbable(); 49 } 50 }, 0); 51 } 52 } 53 54 const originalClose = settings.dialog.close; 55 // Overwrite the close method to remove the dialog on closing. 56 settings.dialog.close = function (event, ...args) { 57 originalClose.apply(settings.dialog, [event, ...args]); 58 // Check if the opener element is inside an AJAX container. 59 const $element = $(event.target); 60 const ajaxContainer = $element.data('uiDialog') 61 ? $element 62 .data('uiDialog') 63 .opener.closest('[data-drupal-ajax-container]') 64 : []; 65 66 // If the opener element was in an ajax container, and focus is on the 67 // body element, we can assume focus was lost. To recover, focus is 68 // moved to the first focusable element in the container. 69 if ( 70 ajaxContainer.length && 71 (document.activeElement === document.body || 72 $(document.activeElement).not(':visible')) 73 ) { 74 const focusableChildren = focusable(ajaxContainer[0]); 75 if (focusableChildren.length > 0) { 76 setTimeout(() => { 77 focusableChildren[0].focus(); 78 }, 0); 79 } 80 } 81 $(event.target).remove(); 82 }; 83 }, 84 85 /** 86 * Scan a dialog for any primary buttons and move them to the button area. 87 * 88 * @param {jQuery} $dialog 89 * A jQuery object containing the element that is the dialog target. 90 * 91 * @return {Array} 92 * An array of buttons that need to be added to the button area. 93 */ 94 prepareDialogButtons($dialog) { 95 const buttons = []; 96 const buttonSelectors = 97 '.form-actions input[type=submit], .form-actions a.button, .form-actions a.action-link'; 98 const buttonElements = $dialog[0].querySelectorAll(buttonSelectors); 99 100 buttonElements.forEach((button) => { 101 button.style.display = 'none'; 102 buttons.push({ 103 text: button.innerHTML || button.getAttribute('value'), 104 class: button.getAttribute('class'), 105 'data-once': button.dataset.once, 106 click(e) { 107 if (button.tagName === 'A') { 108 button.click(); 109 } else { 110 ['mousedown', 'mouseup', 'click'].forEach((event) => 111 button.dispatchEvent(new MouseEvent(event)), 112 ); 113 } 114 e.preventDefault(); 115 }, 116 }); 117 }); 118 return buttons; 119 }, 120 }; 121 122 /** 123 * Command to open a dialog. 124 * 125 * @param {Drupal.Ajax} ajax 126 * The Drupal Ajax object. 127 * @param {object} response 128 * Object holding the server response. 129 * @param {number} [status] 130 * The HTTP status code. 131 * 132 * @return {boolean|undefined} 133 * Returns false if there was no selector property in the response object. 134 */ 135 Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) { 136 if (!response.selector) { 137 return false;
138 } 139 let dialog = document.querySelector(response.selector); 140 if (!dialog) { 141 // Create the element if needed. 142 dialog = document.createElement('div'); 143 dialog.id = response.selector.replace(/^#/, ''); 144 dialog.classList.add('ui-front'); 145 document.body.appendChild(dialog); 146 } 147 // Set up the wrapper, if there isn't one. 148 if (!ajax.wrapper) { 149 ajax.wrapper = dialog.id; 150 } 151 152 // Use the ajax.js insert command to populate the dialog contents. 153 response.command = 'insert'; 154 response.method = 'html'; 155 ajax.commands.insert(ajax, response, status); 156 157 // Move the buttons to the jQuery UI dialog buttons area. 158 response.dialogOptions = response.dialogOptions || {}; 159 if (typeof response.dialogOptions.drupalAutoButtons === 'undefined') { 160 response.dialogOptions.drupalAutoButtons = true; 161 } else if (response.dialogOptions.drupalAutoButtons === 'false') { 162 response.dialogOptions.drupalAutoButtons = false; 163 } else { 164 response.dialogOptions.drupalAutoButtons = 165 !!response.dialogOptions.drupalAutoButtons; 166 } 167 if ( 168 !response.dialogOptions.buttons && 169 response.dialogOptions.drupalAutoButtons 170 ) { 171 response.dialogOptions.buttons = 172 Drupal.behaviors.dialog.prepareDialogButtons($(dialog)); 173 } 174 175 const dialogButtonsChange = () => { 176 const buttons = Drupal.behaviors.dialog.prepareDialogButtons($(dialog)); 177 $(dialog).dialog('option', 'buttons', buttons); 178 }; 179 180 // Bind dialogButtonsChange. 181 dialog.addEventListener('dialogButtonsChange', dialogButtonsChange); 182 dialog.addEventListener('dialog:beforeclose', (event) => { 183 dialog.removeEventListener('dialogButtonsChange', dialogButtonsChange); 184 }); 185 186 // Open the dialog itself. 187 const createdDialog = Drupal.dialog(dialog, response.dialogOptions); 188 if (response.dialogOptions.modal) { 189 createdDialog.showModal(); 190 } else { 191 createdDialog.show(); 192 } 193 194 // Add the standard Drupal class for buttons for style consistency. 195 dialog.parentElement 196 ?.querySelector('.ui-dialog-buttonset') 197 ?.classList.add('form-actions'); 198 }; 199 200 /** 201 * Command to close a dialog. 202 * 203 * If no selector is given, it defaults to trying to close the modal. 204 * 205 * @param {Drupal.Ajax} [ajax] 206 * The ajax object. 207 * @param {object} response 208 * Object holding the server response. 209 * @param {string} response.selector 210 * The selector of the dialog. 211 * @param {boolean} response.persist 212 * Whether to persist the dialog element or not. 213 * @param {number} [status] 214 * The HTTP status code. 215 */ 216 Drupal.AjaxCommands.prototype.closeDialog = function ( 217 ajax, 218 response, 219 status, 220 ) { 221 const dialog = document.querySelector(response.selector); 222 if (dialog) { 223 Drupal.dialog(dialog).close(); 224 if (!response.persist) { 225 dialog.remove(); 226 } 227 } 228 }; 229 230 /** 231 * Command to set a dialog property. 232 * 233 * JQuery UI specific way of setting dialog options. 234 * 235 * @param {Drupal.Ajax} [ajax] 236 * The Drupal Ajax object. 237 * @param {object} response 238 * Object holding the server response. 239 * @param {string} response.selector 240 * Selector for the dialog element. 241 * @param {string} response.optionsName 242 * Name of a key to set. 243 * @param {string} response.optionValue 244 * Value to set. 245 * @param {number} [status] 246 * The HTTP status code. 247 */ 248 Drupal.AjaxCommands.prototype.setDialogOption = function ( 249 ajax, 250 response, 251 status, 252 ) { 253 const dialog = document.querySelector(response.selector); 254 if (dialog) { 255 $(dialog).dialog('option', response.optionName, response.optionValue); 256 } 257 }; 258 259 /** 260 * Binds a listener on dialog creation to handle the cancel link. 261 * 262 * @param {DrupalDialogEvent} e 263 * The event triggered. 264 * @param {Drupal.dialog~dialogDefinition} dialog 265 * The dialog instance. 266 * @param {object} [settings] 267 * Dialog settings. 268 */ 269 window.addEventListener('dialog:aftercreate', (event) => { 270 const dialog = event.dialog; 271 const cancelButton = event.target.querySelector('.dialog-cancel'); 272 const cancelClick = (e) => { 273 dialog.close('cancel'); 274 e.preventDefault(); 275 e.stopPropagation(); 276 }; 277 cancelButton?.removeEventListener('click', cancelClick); 278 cancelButton?.addEventListener('click', cancelClick); 279 }); 280 281 /** 282 * Ajax command to open URL in a modal dialog. 283 * 284 * @param {Drupal.Ajax} [ajax] 285 * An Ajax object. 286 * @param {object} response 287 * The Ajax response. 288 */ 289 Drupal.AjaxCommands.prototype.openModalDialogWithUrl = function ( 290 ajax, 291 response, 292 ) { 293 const dialogOptions = response.dialogOptions || {}; 294 const elementSettings = { 295 progress: { type: 'throbber' }, 296 dialogType: 'modal', 297 dialog: dialogOptions, 298 url: response.url, 299 httpMethod: 'GET', 300 }; 301 Drupal.ajax(elementSettings).execute(); 302 }; 303})(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.