1/* cspell:ignore xmlhttprequest */ 2 3/** 4 * @file 5 * Provides Ajax page updating via jQuery $.ajax. 6 * 7 * Ajax is a method of making a request via JavaScript while viewing an HTML 8 * page. The request returns an array of commands encoded in JSON, which is 9 * then executed to make any changes that are necessary to the page. 10 * 11 * Drupal uses this file to enhance form elements with `#ajax['url']` and 12 * `#ajax['wrapper']` properties. If set, this file will automatically be 13 * included to provide Ajax capabilities. 14 */ 15 16(function ( 17 $, 18 window, 19 Drupal, 20 drupalSettings, 21 loadjs, 22 { isFocusable, tabbable }, 23) { 24 /** 25 * Attaches the Ajax behavior to each Ajax form element. 26 * 27 * @type {Drupal~behavior} 28 * 29 * @prop {Drupal~behaviorAttach} attach 30 * Initialize all {@link Drupal.Ajax} objects declared in 31 * `drupalSettings.ajax` or initialize {@link Drupal.Ajax} objects from 32 * DOM elements having the `use-ajax-submit` or `use-ajax` css class. 33 * @prop {Drupal~behaviorDetach} detach 34 * During `unload` remove all {@link Drupal.Ajax} objects related to 35 * the removed content. 36 */ 37 Drupal.behaviors.AJAX = { 38 attach(context, settings) { 39 function loadAjaxBehavior(base) { 40 const elementSettings = settings.ajax[base]; 41 if (typeof elementSettings.selector === 'undefined') { 42 elementSettings.selector = `#${base}`; 43 } 44 // Use jQuery selector instead of a native selector for 45 // backwards compatibility. 46 once('drupal-ajax', $(elementSettings.selector)).forEach((el) => { 47 elementSettings.element = el; 48 elementSettings.base = base; 49 Drupal.ajax(elementSettings); 50 }); 51 } 52 53 // Load all Ajax behaviors specified in the settings. 54 Object.keys(settings.ajax || {}).forEach(loadAjaxBehavior); 55 56 Drupal.ajax.bindAjaxLinks(document.body); 57 58 // This class means to submit the form to the action using Ajax. 59 once('ajax', '.use-ajax-submit').forEach((el) => { 60 const elementSettings = { 61 // Ajax submits specified in this manner automatically submit to the 62 // normal form action. 63 url: $(el.form).attr('action'), 64 // Form submit button clicks need to tell the form what was clicked so 65 // it gets passed in the POST request. 66 setClick: true, 67 // Form buttons use the 'click' event rather than mousedown. 68 event: 'click', 69 // Clicked form buttons look better with the throbber than the progress 70 // bar. 71 progress: { type: 'throbber' }, 72 base: el.id, 73 element: el, 74 }; 75 76 Drupal.ajax(elementSettings); 77 }); 78 }, 79 80 detach(context, settings, trigger) { 81 if (trigger === 'unload') { 82 Drupal.ajax.expired().forEach((instance) => { 83 // Set this to null and allow garbage collection to reclaim 84 // the memory. 85 Drupal.ajax.instances[instance.instanceIndex] = null; 86 }); 87 } 88 }, 89 }; 90 91 /** 92 * Extends Error to provide handling for Errors in Ajax. 93 * 94 * @constructor 95 * 96 * @augments Error 97 * 98 * @param {XMLHttpRequest} xmlhttp 99 * XMLHttpRequest object used for the failed request. 100 * @param {string} uri 101 * The URI where the error occurred. 102 * @param {string} customMessage 103 * The custom message. 104 */ 105 Drupal.AjaxError = function (xmlhttp, uri, customMessage) { 106 let statusCode; 107 let statusText; 108 let responseText; 109 if (xmlhttp.status) { 110 statusCode = `\n${Drupal.t('An AJAX HTTP error occurred.')}\n${Drupal.t( 111 'HTTP Result Code: !status', 112 { 113 '!status': xmlhttp.status, 114 }, 115 )}`; 116 } else { 117 statusCode = `\n${Drupal.t( 118 'An AJAX HTTP request terminated abnormally.', 119 )}`; 120 } 121 statusCode += `\n${Drupal.t('Debugging information follows.')}`; 122 const pathText = `\n${Drupal.t('Path: !uri', { '!uri': uri })}`; 123 statusText = ''; 124 // In some cases, when statusCode === 0, xmlhttp.statusText may not be 125 // defined. Unfortunately, testing for it with typeof, etc, doesn't seem to 126 // catch that and the test causes an exception. So we need to catch the 127 // exception here. 128 try { 129 statusText = `\n${Drupal.t('StatusText: !statusText', { 130 '!statusText': xmlhttp.statusText.trim(), 131 })}`; 132 } catch (e) { 133 // Empty. 134 } 135 136 responseText = ''; 137 // Again, we don't have a way to know for sure whether accessing 138 // xmlhttp.responseText is going to throw an exception. So we'll catch it. 139 try { 140 responseText = `\n${Drupal.t('ResponseText: !responseText', { 141 '!responseText': xmlhttp.responseText.trim(), 142 })}`; 143 } catch (e) { 144 // Empty. 145 } 146 147 // Make the responseText more readable by stripping HTML tags and newlines. 148 responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, ''); 149 responseText = responseText.replace(/[\n]+\s+/g, '\n'); 150 151 // We don't need readyState except for status == 0. 152 const readyStateText = 153 xmlhttp.status === 0 154 ? `\n${Drupal.t('ReadyState: !readyState', { 155 '!readyState': xmlhttp.readyState, 156 })}` 157 : ''; 158 159 customMessage = customMessage 160 ? `\n${Drupal.t('CustomMessage: !customMessage', { 161 '!customMessage': customMessage, 162 })}` 163 : ''; 164 165 /** 166 * Formatted and translated error message. 167 * 168 * @type {string} 169 */ 170 this.message = 171 statusCode + 172 pathText + 173 statusText + 174 customMessage + 175 responseText + 176 readyStateText; 177 178 /** 179 * Used by some browsers to display a more accurate stack trace. 180 * 181 * @type {string} 182 */ 183 this.name = 'AjaxError'; 184 185 if (!Drupal.AjaxError.messages) { 186 Drupal.AjaxError.messages = new Drupal.Message(); 187 } 188 Drupal.AjaxError.messages.add( 189 Drupal.t( 190 "Oops, something went wrong. Check your browser's developer console for more details.", 191 ), 192 { 193 type: 'error', 194 }, 195 ); 196 }; 197 198 Drupal.AjaxError.prototype = new Error(); 199 Drupal.AjaxError.prototype.constructor = Drupal.AjaxError; 200 201 /** 202 * Provides Ajax page updating via jQuery $.ajax. 203 * 204 * This function is designed to improve developer experience by wrapping the 205 * initialization of {@link Drupal.Ajax} objects and storing all created 206 * objects in the {@link Drupal.ajax.instances} array. 207 * 208 * @example 209 * Drupal.behaviors.myCustomAJAXStuff = { 210 * attach: function (context, settings) { 211 * 212 * var ajaxSettings = { 213 * url: 'my/url/path', 214 * // If the old version of Drupal.ajax() needs to be used those 215 * // properties can be added 216 * base: 'myBase', 217 * element: $(context).find('.someElement') 218 * }; 219 * 220 * var myAjaxObject = Drupal.ajax(ajaxSettings); 221 * 222 * // Declare a new Ajax command specifically for this Ajax object. 223 * myAjaxObject.commands.insert = function (ajax, response, status) { 224 * $('#my-wrapper').append(response.data); 225 * alert('New content was appended to #my-wrapper'); 226 * }; 227 * 228 * // This command will remove this Ajax object from the page. 229 * myAjaxObject.commands.destroyObject = function (ajax, response, status) { 230 * Drupal.ajax.instances[this.instanceIndex] = null; 231 * }; 232 * 233 * // Programmatically trigger the Ajax request. 234 * myAjaxObject.execute(); 235 * } 236 * }; 237 * 238 * @param {object} settings 239 * The settings object passed to {@link Drupal.Ajax} constructor. 240 * @param {string} [settings.base] 241 * Base is passed to {@link Drupal.Ajax} constructor as the 'base' 242 * parameter. 243 * @param {HTMLElement} [settings.element] 244 * Element parameter of {@link Drupal.Ajax} constructor, element on which 245 * event listeners will be bound. 246 * 247 * @return {Drupal.Ajax} 248 * The created Ajax object. 249 * 250 * @see Drupal.AjaxCommands 251 */ 252 Drupal.ajax = function (settings) { 253 if (arguments.length !== 1) { 254 throw new Error( 255 'Drupal.ajax() function must be called with one configuration object only', 256 ); 257 } 258 // Map those config keys to variables for the old Drupal.ajax function. 259 const base = settings.base || false; 260 const element = settings.element || false; 261 delete settings.base; 262 delete settings.element; 263 264 // By default do not display progress for ajax calls without an element. 265 if (!settings.progress && !element) { 266 settings.progress = false;
267 } 268 269 const ajax = new Drupal.Ajax(base, element, settings); 270 ajax.instanceIndex = Drupal.ajax.instances.length; 271 Drupal.ajax.instances.push(ajax); 272 273 return ajax; 274 }; 275 276 /** 277 * Contains all created Ajax objects. 278 * 279 * @type {Array.<Drupal.Ajax|null>} 280 */ 281 Drupal.ajax.instances = []; 282 283 /** 284 * List all objects where the associated element is not in the DOM 285 * 286 * This method ignores {@link Drupal.Ajax} objects not bound to DOM elements 287 * when created with {@link Drupal.ajax}. 288 * 289 * @return {Array.<Drupal.Ajax>} 290 * The list of expired {@link Drupal.Ajax} objects. 291 */ 292 Drupal.ajax.expired = function () { 293 return Drupal.ajax.instances.filter( 294 (instance) => 295 instance && 296 instance.element !== false && 297 !document.body.contains(instance.element), 298 ); 299 }; 300 301 /** 302 * Bind Ajax functionality to links that use the 'use-ajax' class. 303 * 304 * @param {HTMLElement} element 305 * Element to enable Ajax functionality for. 306 */ 307 Drupal.ajax.bindAjaxLinks = (element) => { 308 // Bind Ajax behaviors to all items showing the class. 309 once('ajax', '.use-ajax', element).forEach((ajaxLink) => { 310 const $linkElement = $(ajaxLink); 311 312 const elementSettings = { 313 // Clicked links look better with the throbber than the progress bar. 314 progress: { type: 'throbber' }, 315 dialogType: $linkElement.data('dialog-type'), 316 dialog: $linkElement.data('dialog-options'), 317 dialogRenderer: $linkElement.data('dialog-renderer'), 318 base: $linkElement.attr('id'), 319 element: ajaxLink, 320 }; 321 const href = $linkElement.attr('href'); 322 /** 323 * For anchor tags, these will go to the target of the anchor rather than 324 * the usual location. 325 */ 326 if (href) { 327 elementSettings.url = href; 328 elementSettings.event = 'click'; 329 } 330 const httpMethod = $linkElement.data('ajax-http-method'); 331 /** 332 * In case of setting custom ajax http method for link we rewrite ajax.httpMethod. 333 */ 334 if (httpMethod) { 335 elementSettings.httpMethod = httpMethod; 336 } 337 Drupal.ajax(elementSettings); 338 }); 339 }; 340 341 /** 342 * Settings for an Ajax object. 343 * 344 * @typedef {object} Drupal.Ajax~elementSettings 345 * 346 * @prop {string} url 347 * Target of the Ajax request. 348 * @prop {?string} [event] 349 * Event bound to settings.element which will trigger the Ajax request. 350 * @prop {boolean} [keypress=true] 351 * Triggers a request on keypress events. 352 * @prop {?string} selector 353 * jQuery selector targeting the element to bind events to or used with 354 * {@link Drupal.AjaxCommands}. 355 * @prop {string} [effect='none'] 356 * Name of the jQuery method to use for displaying new Ajax content. 357 * @prop {string|number} [speed='none'] 358 * Speed with which to apply the effect. 359 * @prop {string} [method] 360 * Name of the jQuery method used to insert new content in the targeted 361 * element. 362 * @prop {object} [progress] 363 * Settings for the display of a user-friendly loader. 364 * @prop {string} [progress.type='throbber'] 365 * Type of progress element, core provides `'bar'`, `'throbber'` and 366 * `'fullscreen'`. 367 * @prop {string} [progress.message=Drupal.t('Processing...')] 368 * Custom message to be used with the bar indicator. 369 * @prop {object} [submit] 370 * Extra data to be sent with the Ajax request. 371 * @prop {boolean} [submit.js=true] 372 * Allows the PHP side to know this comes from an Ajax request. 373 * @prop {object} [dialog] 374 * Options for {@link Drupal.dialog}. 375 * @prop {string} [dialogType] 376 * One of `'modal'` or `'dialog'`. 377 * @prop {string} [prevent] 378 * List of events on which to stop default action and stop propagation. 379 */ 380 381 /** 382 * Ajax constructor. 383 * 384 * The Ajax request returns an array of commands encoded in JSON, which is 385 * then executed to make any changes that are necessary to the page. 386 * 387 * Drupal uses this file to enhance form elements with `#ajax['url']` and 388 * `#ajax['wrapper']` properties. If set, this file will automatically be 389 * included to provide Ajax capabilities. 390 * 391 * @constructor 392 * 393 * @param {string} [base] 394 * Base parameter of {@link Drupal.Ajax} constructor 395 * @param {HTMLElement} [element] 396 * Element parameter of {@link Drupal.Ajax} constructor, element on which 397 * event listeners will be bound. 398 * @param {Drupal.Ajax~elementSettings} elementSettings 399 * Settings for this Ajax object. 400 */ 401 Drupal.Ajax = function (base, element, elementSettings) { 402 const defaults = { 403 httpMethod: 'POST', 404 event: element ? 'mousedown' : null, 405 keypress: true, 406 selector: base ? `#${base}` : null, 407 effect: 'none', 408 speed: 'none', 409 method: 'replaceWith', 410 progress: { 411 type: 'throbber', 412 message: Drupal.t('Processing...'), 413 }, 414 submit: { 415 js: true, 416 }, 417 }; 418 419 $.extend(this, defaults, elementSettings); 420 421 /** 422 * @type {Drupal.AjaxCommands} 423 */ 424 this.commands = new Drupal.AjaxCommands(); 425 426 /** 427 * @type {boolean|number} 428 */ 429 this.instanceIndex = false;
430 431 // @todo Remove this after refactoring the PHP code to: 432 // - Call this 'selector'. 433 // - Include the '#' for ID-based selectors. 434 // - Support non-ID-based selectors. 435 if (this.wrapper) { 436 /** 437 * @type {string} 438 */ 439 this.wrapper = `#${this.wrapper}`; 440 } 441 442 /** 443 * @type {HTMLElement} 444 */ 445 this.element = element; 446 447 /** 448 * The last focused element right before processing ajax response. 449 * 450 * @type {string|null} 451 */ 452 this.preCommandsFocusedElementSelector = null; 453 454 /** 455 * @type {Drupal.Ajax~elementSettings} 456 */ 457 this.elementSettings = elementSettings; 458 459 // If there isn't a form, jQuery.ajax() will be used instead, allowing us to 460 // bind Ajax to links as well. 461 if (this.element?.form) { 462 /** 463 * @type {jQuery} 464 */ 465 this.$form = $(this.element.form); 466 } 467 468 // If no Ajax callback URL was given, use the link href or form action. 469 if (!this.url) { 470 const $element = $(this.element); 471 if (this.element.tagName === 'A') { 472 this.url = $element.attr('href'); 473 } else if (this.element && element.form) { 474 this.url = this.$form.attr('action'); 475 } 476 } 477 478 // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let 479 // the server detect when it needs to degrade gracefully. 480 // There are four scenarios to check for: 481 // 1. /nojs/ 482 // 2. /nojs$ - The end of a URL string. 483 // 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar). 484 // 4. /nojs# - Followed by a fragment (e.g.: path/nojs#my-fragment). 485 const originalUrl = this.url; 486 487 /** 488 * Processed Ajax URL. 489 * 490 * @type {string} 491 */ 492 this.url = this.url.replace(/\/nojs(\/|$|\?|#)/, '/ajax$1'); 493 // If the 'nojs' version of the URL is trusted, also trust the 'ajax' 494 // version. 495 if (drupalSettings.ajaxTrustedUrl[originalUrl]) { 496 drupalSettings.ajaxTrustedUrl[this.url] = true; 497 } 498 499 // Set the options for the ajaxSubmit function. 500 // The 'this' variable will not persist inside of the options object. 501 const ajax = this; 502 503 /** 504 * Options for the jQuery.ajax function. 505 * 506 * @name Drupal.Ajax#options 507 * 508 * @type {object} 509 * 510 * @prop {string} url 511 * Ajax URL to be called. 512 * @prop {object} data 513 * Ajax payload. 514 * @prop {function} beforeSerialize 515 * Implement jQuery beforeSerialize function to call 516 * {@link Drupal.Ajax#beforeSerialize}. 517 * @prop {function} beforeSubmit 518 * Implement jQuery beforeSubmit function to call 519 * {@link Drupal.Ajax#beforeSubmit}. 520 * @prop {function} beforeSend 521 * Implement jQuery beforeSend function to call 522 * {@link Drupal.Ajax#beforeSend}. 523 * @prop {function} success 524 * Implement jQuery success function to call 525 * {@link Drupal.Ajax#success}. 526 * @prop {function} complete 527 * Implement jQuery success function to clean up ajax state and trigger an 528 * error if needed. 529 * @prop {string} dataType='json' 530 * Type of the response expected. 531 * @prop {string} type='POST' 532 * HTTP method to use for the Ajax request. 533 */ 534 ajax.options = { 535 url: ajax.url, 536 data: ajax.submit, 537 isInProgress() { 538 return ajax.ajaxing; 539 }, 540 beforeSerialize(elementSettings, options) { 541 return ajax.beforeSerialize(elementSettings, options); 542 }, 543 beforeSubmit(formValues, elementSettings, options) { 544 ajax.ajaxing = true; 545 ajax.preCommandsFocusedElementSelector = null; 546 return ajax.beforeSubmit(formValues, elementSettings, options); 547 }, 548 beforeSend(xmlhttprequest, options) { 549 ajax.ajaxing = true; 550 return ajax.beforeSend(xmlhttprequest, options); 551 }, 552 success(response, status, xmlhttprequest) { 553 ajax.preCommandsFocusedElementSelector = 554 document.activeElement.getAttribute('data-drupal-selector'); 555 556 // Sanity check for browser support (object expected). 557 // When using iFrame uploads, responses must be returned as a string. 558 if (typeof response === 'string') { 559 response = JSON.parse(response); 560 } 561 562 // Prior to invoking the response's commands, verify that they can be 563 // trusted by checking for a response header. See 564 // \Drupal\Core\EventSubscriber\AjaxResponseSubscriber for details. 565 // - Empty responses are harmless so can bypass verification. This 566 // avoids an alert message for server-generated no-op responses that 567 // skip Ajax rendering. 568 // - Ajax objects with trusted URLs (e.g., ones defined server-side via 569 // #ajax) can bypass header verification. This is especially useful 570 // for Ajax with multipart forms. Because IFRAME transport is used, 571 // the response headers cannot be accessed for verification. 572 if (response !== null && !drupalSettings.ajaxTrustedUrl[ajax.url]) { 573 if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') { 574 const customMessage = Drupal.t( 575 'The response failed verification so will not be processed.', 576 ); 577 return ajax.error(xmlhttprequest, ajax.url, customMessage); 578 } 579 } 580 581 return ( 582 // Ensure that the return of the success callback is a Promise. 583 // When the return is a Promise, using resolve will unwrap it, and 584 // when the return is not a Promise we make sure it can be used as 585 // one. This is useful for code that overrides the success method. 586 Promise.resolve(ajax.success(response, status))
587 // Ajaxing status is back to false when all the AJAX commands have 588 // finished executing. 589 .then(() => { 590 ajax.ajaxing = false; 591 // jQuery normally triggers the ajaxSuccess, ajaxComplete, and 592 // ajaxStop events after the "success" function passed to $.ajax() 593 // returns, but we prevented that via 594 // $.event.special[EVENT_NAME].trigger in order to wait for the 595 // commands to finish executing. Now that they have, re-trigger 596 // those events. 597 $(document).trigger('ajaxSuccess', [xmlhttprequest, this]); 598 $(document).trigger('ajaxComplete', [xmlhttprequest, this]); 599 if (--$.active === 0) { 600 $(document).trigger('ajaxStop'); 601 } 602 }) 603 ); 604 }, 605 error(xmlhttprequest, status, error) { 606 ajax.ajaxing = false; 607 }, 608 complete(xmlhttprequest, status) { 609 if (status === 'error' || status === 'parsererror') { 610 return ajax.error(xmlhttprequest, ajax.url); 611 } 612 }, 613 dataType: 'json', 614 jsonp: false, 615 method: ajax.httpMethod, 616 }; 617 618 if (elementSettings.dialog) { 619 ajax.options.data.dialogOptions = elementSettings.dialog; 620 } 621 622 // Ensure that we have a valid URL by adding ? when no query parameter is 623 // yet available, otherwise append using &. 624 if (!ajax.options.url.includes('?')) { 625 ajax.options.url += '?'; 626 } else { 627 ajax.options.url += '&'; 628 } 629 // If this element has a dialog type use if for the wrapper if not use 'ajax'. 630 let wrapper = `drupal_${elementSettings.dialogType || 'ajax'}`; 631 if (elementSettings.dialogRenderer) { 632 wrapper += `.${elementSettings.dialogRenderer}`; 633 } 634 ajax.options.url += `${Drupal.ajax.WRAPPER_FORMAT}=${wrapper}`; 635 636 // Bind the ajaxSubmit function to the element event. 637 $(ajax.element).on(elementSettings.event, function (event) { 638 if ( 639 !drupalSettings.ajaxTrustedUrl[ajax.url] && 640 !Drupal.url.isLocal(ajax.url) 641 ) { 642 throw new Error( 643 Drupal.t('The callback URL is not local and not trusted: !url', { 644 '!url': ajax.url, 645 }), 646 ); 647 } 648 649 if ( 650 $(ajax.element).attr('type') === 'submit' && 651 !$(ajax.element).attr('formnovalidate') 652 ) { 653 const formValidationEvent = $.Event('drupalAjaxFormValidate'); 654 $(event.target).trigger(formValidationEvent); 655 if (formValidationEvent.result === false) { 656 event.preventDefault(); 657 return false; 658 } 659 } 660 return ajax.eventResponse(this, event); 661 }); 662 663 // If necessary, enable keyboard submission so that Ajax behaviors 664 // can be triggered through keyboard input as well as e.g. a mousedown 665 // action. 666 if (elementSettings.keypress) { 667 $(ajax.element).on('keypress', function (event) { 668 return ajax.keypressResponse(this, event); 669 }); 670 } 671 672 // If necessary, prevent the browser default action of an additional event. 673 // For example, prevent the browser default action of a click, even if the 674 // Ajax behavior binds to mousedown. 675 if (elementSettings.prevent) { 676 $(ajax.element).on(elementSettings.prevent, false); 677 } 678 }; 679 680 /** 681 * URL query attribute to indicate the wrapper used to render a request. 682 * 683 * The wrapper format determines how the HTML is wrapped, for example in a 684 * modal dialog. 685 * 686 * @const {string} 687 * 688 * @default 689 */ 690 Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format'; 691 692 /** 693 * Request parameter to indicate that a request is a Drupal Ajax request. 694 * 695 * @const {string} 696 * 697 * @default 698 */ 699 Drupal.Ajax.AJAX_REQUEST_PARAMETER = '_drupal_ajax'; 700 701 /** 702 * Execute the ajax request. 703 * 704 * Allows developers to execute an Ajax request manually without specifying 705 * an event to respond to. 706 * 707 * @return {object} 708 * Returns the jQuery.Deferred object underlying the Ajax request. If 709 * pre-serialization fails, the Deferred will be returned in the rejected 710 * state. 711 */ 712 Drupal.Ajax.prototype.execute = function () {
713 // Do not perform another ajax command if one is already in progress. 714 if (this.ajaxing) { 715 return; 716 } 717 718 try { 719 this.beforeSerialize(this.element, this.options); 720 // Return the jqXHR so that external code can hook into the Deferred API. 721 return $.ajax(this.options); 722 } catch (e) { 723 // Unset the ajax.ajaxing flag here because it won't be unset during 724 // the complete response. 725 this.ajaxing = false; 726 window.alert( 727 `An error occurred while attempting to process ${this.options.url}: ${e.message}`, 728 ); 729 // For consistency, return a rejected Deferred (i.e., jqXHR's superclass) 730 // so that calling code can take appropriate action. 731 return $.Deferred().reject(); 732 } 733 }; 734 735 /** 736 * Handle a key press. 737 * 738 * The Ajax object will, if instructed, bind to a key press response. This 739 * will test to see if the key press is valid to trigger this event and 740 * if it is, trigger it for us and prevent other keypresses from triggering. 741 * In this case we're handling RETURN and SPACE BAR keypresses (event codes 13 742 * and 32. RETURN is often used to submit a form when in a textfield, and 743 * SPACE is often used to activate an element without submitting. 744 * 745 * @param {HTMLElement} element 746 * Element the event was triggered on. 747 * @param {jQuery.Event} event 748 * Triggered event. 749 */ 750 Drupal.Ajax.prototype.keypressResponse = function (element, event) { 751 // Create a synonym for this to reduce code confusion. 752 const ajax = this; 753 754 // Detect enter key and space bar and allow the standard response for them, 755 // except for form elements of type 'text', 'tel', 'number' and 'textarea', 756 // where the space bar activation causes inappropriate activation if 757 // #ajax['keypress'] is TRUE. On a text-type widget a space should always 758 // be a space. 759 if ( 760 event.which === 13 || 761 (event.which === 32 && 762 element.type !== 'text' && 763 element.type !== 'textarea' && 764 element.type !== 'tel' && 765 element.type !== 'number') 766 ) { 767 event.preventDefault(); 768 event.stopPropagation(); 769 $(element).trigger(ajax.elementSettings.event); 770 } 771 }; 772 773 /** 774 * Handle an event that triggers an Ajax response. 775 * 776 * When an event that triggers an Ajax response happens, this method will 777 * perform the actual Ajax call. It is bound to the event using 778 * bind() in the constructor, and it uses the options specified on the 779 * Ajax object. 780 * 781 * @param {HTMLElement} element 782 * Element the event was triggered on. 783 * @param {jQuery.Event} event 784 * Triggered event. 785 */ 786 Drupal.Ajax.prototype.eventResponse = function (element, event) { 787 event.preventDefault(); 788 event.stopPropagation(); 789 790 // Create a synonym for this to reduce code confusion. 791 const ajax = this; 792 793 // Do not perform another Ajax command if one is already in progress. 794 if (ajax.ajaxing) { 795 return; 796 } 797 798 try { 799 if (ajax.$form) { 800 // If setClick is set, we must set this to ensure that the button's 801 // value is passed. 802 if (ajax.setClick) { 803 // Mark the clicked button. 'form.clk' is a special variable for 804 // ajaxSubmit that tells the system which element got clicked to 805 // trigger the submit. Without it there would be no 'op' or 806 // equivalent. 807 element.form.clk = element; 808 } 809 810 ajax.$form.ajaxSubmit(ajax.options); 811 } else { 812 ajax.beforeSerialize(ajax.element, ajax.options); 813 $.ajax(ajax.options); 814 } 815 } catch (e) { 816 // Unset the ajax.ajaxing flag here because it won't be unset during 817 // the complete response. 818 ajax.ajaxing = false; 819 window.alert( 820 `An error occurred while attempting to process ${ajax.options.url}: ${e.message}`, 821 ); 822 } 823 }; 824 825 /** 826 * Handler for the form serialization. 827 * 828 * Runs before the beforeSend() handler (see below), and unlike that one, runs 829 * before field data is collected. 830 * 831 * @param {object} [element] 832 * Ajax object's `elementSettings`. 833 * @param {object} options 834 * jQuery.ajax options. 835 */ 836 Drupal.Ajax.prototype.beforeSerialize = function (element, options) { 837 // Allow detaching behaviors to update field values before collecting them. 838 // This is only needed when field values are added to the POST data, so only 839 // when there is a form such that this.$form.ajaxSubmit() is used instead of 840 // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize() 841 // isn't called, but don't rely on that: explicitly check this.$form. 842 if (this.$form && document.body.contains(this.$form.get(0))) { 843 const settings = this.settings || drupalSettings; 844 Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize'); 845 } 846 847 // Inform Drupal that this is an AJAX request. 848 options.data[Drupal.Ajax.AJAX_REQUEST_PARAMETER] = 1; 849 850 // Allow Drupal to return new JavaScript and CSS files to load without 851 // returning the ones already loaded. 852 // @see \Drupal\Core\StackMiddleWare\AjaxPageState 853 // @see \Drupal\Core\Theme\AjaxBasePageNegotiator 854 // @see \Drupal\Core\Asset\LibraryDependencyResolverInterface::getMinimalRepresentativeSubset() 855 // @see system_js_settings_alter() 856 const pageState = drupalSettings.ajaxPageState; 857 options.data['ajax_page_state[theme]'] = pageState.theme; 858 options.data['ajax_page_state[theme_token]'] = pageState.theme_token; 859 options.data['ajax_page_state[libraries]'] = pageState.libraries; 860 }; 861 862 /** 863 * Modify form values prior to form submission. 864 * 865 * @param {Array.<object>} formValues 866 * Processed form values. 867 * @param {jQuery} element 868 * The form node as a jQuery object. 869 * @param {object} options 870 * jQuery.ajax options. 871 */ 872 Drupal.Ajax.prototype.beforeSubmit = function (formValues, element, options) { 873 // This function is left empty to make it simple to override for modules 874 // that wish to add functionality here. 875 }; 876 877 /** 878 * Prepare the Ajax request before it is sent. 879 * 880 * @param {XMLHttpRequest} xmlhttprequest 881 * Native Ajax object. 882 * @param {object} options 883 * jQuery.ajax options. 884 */ 885 Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) { 886 // For forms without file inputs, the jQuery Form plugin serializes the 887 // form values, and then calls jQuery's $.ajax() function, which invokes 888 // this handler. In this circumstance, options.extraData is never used. For 889 // forms with file inputs, the jQuery Form plugin uses the browser's normal 890 // form submission mechanism, but captures the response in a hidden IFRAME. 891 // In this circumstance, it calls this handler first, and then appends 892 // hidden fields to the form to submit the values in options.extraData. 893 // There is no simple way to know which submission mechanism will be used, 894 // so we add to extraData regardless, and allow it to be ignored in the 895 // former case. 896 if (this.$form) { 897 options.extraData = options.extraData || {}; 898 899 // Let the server know when the IFRAME submission mechanism is used. The 900 // server can use this information to wrap the JSON response in a 901 // TEXTAREA, as per http://jquery.malsup.com/form/#file-upload. 902 options.extraData.ajax_iframe_upload = '1'; 903 904 // The triggering element is about to be disabled (see below), but if it 905 // contains a value (e.g., a checkbox, textfield, select, etc.), ensure 906 // that value is included in the submission. As per above, submissions 907 // that use $.ajax() are already serialized prior to the element being 908 // disabled, so this is only needed for IFRAME submissions. 909 const v = $.fieldValue(this.element); 910 if (v !== null) { 911 options.extraData[this.element.name] = v; 912 } 913 } 914 915 // Disable the element that received the change to prevent user interface 916 // interaction while the Ajax request is in progress. ajax.ajaxing prevents 917 // the element from triggering a new request, but does not prevent the user 918 // from changing its value. 919 $(this.element).prop('disabled', true); 920 921 if (!this.progress || !this.progress.type) { 922 return; 923 } 924 925 // Insert progress indicator. 926 const progressIndicatorMethod = `setProgressIndicator${this.progress.type 927 .slice(0, 1) 928 .toUpperCase()}${this.progress.type.slice(1).toLowerCase()}`; 929 if ( 930 progressIndicatorMethod in this && 931 typeof this[progressIndicatorMethod] === 'function' 932 ) { 933 this[progressIndicatorMethod].call(this); 934 } 935 }; 936 937 /** 938 * An animated progress throbber and container element for AJAX operations. 939 * 940 * @param {string} [message] 941 * (optional) The message shown on the UI. 942 * @return {string} 943 * The HTML markup for the throbber. 944 */ 945 Drupal.theme.ajaxProgressThrobber = (message) => { 946 // Build markup without adding extra white space since it affects rendering. 947 const messageMarkup = 948 typeof message === 'string' 949 ? Drupal.theme('ajaxProgressMessage', message) 950 : ''; 951 const throbber = '<div class="throbber"> </div>'; 952 953 return `<div class="ajax-progress ajax-progress-throbber">${throbber}${messageMarkup}</div>`; 954 }; 955 956 /** 957 * An animated progress throbber and container element for AJAX operations. 958 * 959 * @return {string} 960 * The HTML markup for the throbber. 961 */ 962 Drupal.theme.ajaxProgressIndicatorFullscreen = () => 963 '<div class="ajax-progress ajax-progress-fullscreen"> </div>'; 964 965 /** 966 * Formats text accompanying the AJAX progress throbber. 967 * 968 * @param {string} message 969 * The message shown on the UI. 970 * @return {string} 971 * The HTML markup for the throbber. 972 */ 973 Drupal.theme.ajaxProgressMessage = (message) => 974 `<div class="message">${message}</div>`; 975 976 /** 977 * Provide a wrapper for the AJAX progress bar element. 978 * 979 * @param {jQuery} $element 980 * Progress bar element. 981 * @return {string} 982 * The HTML markup for the progress bar. 983 */ 984 Drupal.theme.ajaxProgressBar = ($element) => 985 $('<div class="ajax-progress ajax-progress-bar"></div>').append($element); 986 987 /** 988 * Sets the progress bar progress indicator. 989 */ 990 Drupal.Ajax.prototype.setProgressIndicatorBar = function () { 991 const progressBar = new Drupal.ProgressBar(
992 `ajax-progress-${this.element.id}`, 993 $.noop, 994 this.progress.method, 995 $.noop, 996 ); 997 if (this.progress.message) { 998 progressBar.setProgress(-1, this.progress.message); 999 } 1000 if (this.progress.url) { 1001 progressBar.startMonitoring( 1002 this.progress.url, 1003 this.progress.interval || 1500, 1004 ); 1005 } 1006 this.progress.element = $( 1007 Drupal.theme('ajaxProgressBar', progressBar.element), 1008 ); 1009 this.progress.object = progressBar; 1010 $(this.element).after(this.progress.element); 1011 }; 1012 1013 /** 1014 * Sets the throbber progress indicator. 1015 */ 1016 Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () { 1017 this.progress.element = $( 1018 Drupal.theme('ajaxProgressThrobber', this.progress.message), 1019 ); 1020 if ($(this.element).closest('[data-drupal-ajax-container]').length) { 1021 $(this.element) 1022 .closest('[data-drupal-ajax-container]') 1023 .after(this.progress.element); 1024 } else { 1025 $(this.element).after(this.progress.element); 1026 } 1027 }; 1028 1029 /** 1030 * Sets the fullscreen progress indicator. 1031 */ 1032 Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () { 1033 this.progress.element = $(Drupal.theme('ajaxProgressIndicatorFullscreen')); 1034 $('body').append(this.progress.element); 1035 }; 1036 1037 /** 1038 * Helper method to make sure commands are executed in sequence. 1039 * 1040 * @param {Array.<Drupal.AjaxCommands~commandDefinition>} response 1041 * Drupal Ajax response. 1042 * @param {number} status 1043 * XMLHttpRequest status. 1044 * 1045 * @return {Promise} 1046 * The promise that will resolve once all commands have finished executing. 1047 */ 1048 Drupal.Ajax.prototype.commandExecutionQueue = function (response, status) { 1049 const ajaxCommands = this.commands; 1050 return Object.keys(response || {}).reduce( 1051 // Add all commands to a single execution queue. 1052 (executionQueue, key) => 1053 executionQueue.then(() => { 1054 const { command } = response[key]; 1055 if (command && ajaxCommands[command]) { 1056 // When a command returns a promise, the remaining commands will not 1057 // execute until that promise has been fulfilled. This is typically 1058 // used to ensure JavaScript files added via the 'add_js' command 1059 // have loaded before subsequent commands execute. 1060 return ajaxCommands[command](this, response[key], status); 1061 } 1062 }), 1063 Promise.resolve(), 1064 ); 1065 }; 1066 1067 /** 1068 * Handler for the form redirection completion. 1069 * 1070 * @param {Array.<Drupal.AjaxCommands~commandDefinition>} response 1071 * Drupal Ajax response. 1072 * @param {number} status 1073 * XMLHttpRequest status. 1074 * 1075 * @return {Promise} 1076 * The promise that will resolve once all commands have finished executing. 1077 */ 1078 Drupal.Ajax.prototype.success = function (response, status) { 1079 // Remove the progress element. 1080 if (this.progress.element) { 1081 $(this.progress.element).remove(); 1082 } 1083 if (this.progress.object) { 1084 this.progress.object.stopMonitoring(); 1085 } 1086 $(this.element).prop('disabled', false); 1087 1088 // Save element's ancestors tree so if the element is removed from the dom 1089 // we can try to refocus one of its parents. Using addBack reverse the 1090 // result array, meaning that index 0 is the highest parent in the hierarchy 1091 // in this situation it is usually a <form> element. 1092 const elementParents = $(this.element) 1093 .parents('[data-drupal-selector]') 1094 .addBack() 1095 .toArray(); 1096 1097 // Track if any command is altering the focus so we can avoid changing the 1098 // focus set by the Ajax command. 1099 const focusChanged = Object.keys(response || {}).some((key) => { 1100 const { command, method } = response[key]; 1101 return ( 1102 command === 'focusFirst' || 1103 command === 'openDialog' || 1104 (command === 'invoke' && method === 'focus') 1105 ); 1106 }); 1107 1108 return ( 1109 this.commandExecutionQueue(response, status) 1110 // If the focus hasn't been changed by the AJAX commands, try to refocus 1111 // the triggering element or one of its parents if that element does not 1112 // exist anymore. 1113 .then(() => { 1114 if (!focusChanged) { 1115 let target = false;
1116 if (this.element) { 1117 if ( 1118 $(this.element).data('refocus-blur') && 1119 this.preCommandsFocusedElementSelector 1120 ) { 1121 target = document.querySelector( 1122 `[data-drupal-selector="${this.preCommandsFocusedElementSelector}"]`, 1123 ); 1124 } 1125 if (!target && !$(this.element).data('disable-refocus')) { 1126 for ( 1127 let n = elementParents.length - 1; 1128 !target && n >= 0; 1129 n-- 1130 ) { 1131 target = document.querySelector( 1132 `[data-drupal-selector="${elementParents[n].getAttribute( 1133 'data-drupal-selector', 1134 )}"]`, 1135 ); 1136 } 1137 } 1138 } 1139 if (target) { 1140 $(target).trigger('focus'); 1141 } 1142 } 1143 // Reattach behaviors, if they were detached in beforeSerialize(). The 1144 // attachBehaviors() called on the new content from processing the 1145 // response commands is not sufficient, because behaviors from the 1146 // entire form need to be reattached. 1147 if (this.$form && document.body.contains(this.$form.get(0))) { 1148 const settings = this.settings || drupalSettings; 1149 Drupal.attachBehaviors(this.$form.get(0), settings); 1150 } 1151 // Remove any response-specific settings so they don't get used on the 1152 // next call by mistake. 1153 this.settings = null; 1154 }) 1155 .catch((error) => 1156 // eslint-disable-next-line no-console 1157 console.error( 1158 Drupal.t( 1159 'An error occurred during the execution of the Ajax response: !error', 1160 { 1161 '!error': error, 1162 }, 1163 ), 1164 ), 1165 ) 1166 ); 1167 }; 1168 1169 /** 1170 * Build an effect object to apply an effect when adding new HTML. 1171 * 1172 * @param {object} response 1173 * Drupal Ajax response. 1174 * @param {string} [response.effect] 1175 * Override the default value of {@link Drupal.Ajax#elementSettings}. 1176 * @param {string|number} [response.speed] 1177 * Override the default value of {@link Drupal.Ajax#elementSettings}. 1178 * 1179 * @return {object} 1180 * Returns an object with `showEffect`, `hideEffect` and `showSpeed` 1181 * properties. 1182 */ 1183 Drupal.Ajax.prototype.getEffect = function (response) { 1184 const type = response.effect || this.effect; 1185 const speed = response.speed || this.speed; 1186 1187 const effect = {}; 1188 if (type === 'none') { 1189 effect.showEffect = 'show'; 1190 effect.hideEffect = 'hide'; 1191 effect.showSpeed = ''; 1192 } else if (type === 'fade') { 1193 effect.showEffect = 'fadeIn'; 1194 effect.hideEffect = 'fadeOut'; 1195 effect.showSpeed = speed; 1196 } else { 1197 effect.showEffect = `${type}Toggle`; 1198 effect.hideEffect = `${type}Toggle`; 1199 effect.showSpeed = speed; 1200 } 1201 1202 return effect; 1203 }; 1204 1205 /** 1206 * Handler for the form redirection error. 1207 * 1208 * @param {object} xmlhttprequest 1209 * Native XMLHttpRequest object. 1210 * @param {string} uri 1211 * Ajax Request URI. 1212 * @param {string} [customMessage] 1213 * Extra message to print with the Ajax error. 1214 */ 1215 Drupal.Ajax.prototype.error = function (xmlhttprequest, uri, customMessage) { 1216 // Remove the progress element. 1217 if (this.progress.element) { 1218 $(this.progress.element).remove(); 1219 } 1220 if (this.progress.object) { 1221 this.progress.object.stopMonitoring(); 1222 } 1223 // Undo hide. 1224 $(this.wrapper).show(); 1225 // Re-enable the element. 1226 $(this.element).prop('disabled', false); 1227 // Reattach behaviors, if they were detached in beforeSerialize(), and the 1228 // form is still part of the document. 1229 if (this.$form && document.body.contains(this.$form.get(0))) { 1230 const settings = this.settings || drupalSettings; 1231 Drupal.attachBehaviors(this.$form.get(0), settings); 1232 } 1233 throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage); 1234 }; 1235 1236 /** 1237 * Provide a wrapper for new content via Ajax. 1238 * 1239 * Wrap the inserted markup when inserting multiple root elements with an 1240 * ajax effect. 1241 * 1242 * @param {jQuery} $newContent 1243 * Response elements after parsing. 1244 * @param {Drupal.Ajax} ajax 1245 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1246 * @param {object} response 1247 * The response from the Ajax request. 1248 *
1249 * @deprecated in drupal:8.6.0 and is removed from drupal:12.0.0. 1250 * Use data with desired wrapper. 1251 * 1252 * @see https://www.drupal.org/node/2940704 1253 * 1254 * @todo Add deprecation warning after it is possible. For more information 1255 * see: https://www.drupal.org/project/drupal/issues/2973400 1256 */ 1257 Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) => 1258 (response.effect || ajax.effect) !== 'none' && 1259 $newContent.filter( 1260 (i) => 1261 !( 1262 // We can not consider HTML comments or whitespace text as separate 1263 // roots, since they do not cause visual regression with effect. 1264 ( 1265 $newContent[i].nodeName === '#comment' || 1266 ($newContent[i].nodeName === '#text' && 1267 /^(\s|\n|\r)*$/.test($newContent[i].textContent)) 1268 ) 1269 ), 1270 ).length > 1 1271 ? Drupal.theme('ajaxWrapperMultipleRootElements', $newContent) 1272 : $newContent; 1273 1274 /** 1275 * Provide a wrapper for multiple root elements via Ajax. 1276 * 1277 * @param {jQuery} $elements 1278 * Response elements after parsing. 1279 * 1280 * @deprecated in drupal:8.6.0 and is removed from drupal:12.0.0. 1281 * Use data with desired wrapper. 1282 * 1283 * @see https://www.drupal.org/node/2940704 1284 * 1285 * @todo Add deprecation warning after it is possible. For more information 1286 * see: https://www.drupal.org/project/drupal/issues/2973400 1287 */ 1288 Drupal.theme.ajaxWrapperMultipleRootElements = ($elements) => 1289 $('<div></div>').append($elements); 1290 1291 /** 1292 * @typedef {object} Drupal.AjaxCommands~commandDefinition 1293 * 1294 * @prop {string} command 1295 * @prop {string} [method] 1296 * @prop {string} [selector] 1297 * @prop {string} [data] 1298 * @prop {object} [settings] 1299 * @prop {boolean} [asterisk] 1300 * @prop {string} [text] 1301 * @prop {string} [title] 1302 * @prop {string} [url] 1303 * @prop {object} [argument] 1304 * @prop {string} [name] 1305 * @prop {string} [value] 1306 * @prop {string} [old] 1307 * @prop {string} [new] 1308 * @prop {boolean} [merge] 1309 * @prop {Array} [args] 1310 * 1311 * @see Drupal.AjaxCommands 1312 */ 1313 1314 /** 1315 * Provide a series of commands that the client will perform. 1316 * 1317 * @constructor 1318 */ 1319 Drupal.AjaxCommands = function () {}; 1320 Drupal.AjaxCommands.prototype = { 1321 /** 1322 * Command to insert new content into the DOM. 1323 * 1324 * @param {Drupal.Ajax} ajax 1325 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1326 * @param {object} response 1327 * The response from the Ajax request. 1328 * @param {string} response.data 1329 * The data to use with the jQuery method. 1330 * @param {string} [response.method] 1331 * The jQuery DOM manipulation method to be used. 1332 * @param {string} [response.selector] 1333 * An optional jQuery selector string. 1334 * @param {object} [response.settings] 1335 * An optional array of settings that will be used. 1336 */ 1337 insert(ajax, response) { 1338 // Get information from the response. If it is not there, default to 1339 // our presets. 1340 const $wrapper = response.selector 1341 ? $(response.selector) 1342 : $(ajax.wrapper); 1343 const method = response.method || ajax.method; 1344 const effect = ajax.getEffect(response); 1345 1346 // Apply any settings from the returned JSON if available. 1347 const settings = response.settings || ajax.settings || drupalSettings; 1348 1349 // Parse response.data into an element collection. 1350 const parseHTML = (htmlString) => { 1351 const fragment = document.createDocumentFragment(); 1352 // Create a temporary template element. 1353 const template = fragment.appendChild( 1354 document.createElement('template'), 1355 ); 1356 1357 // Set the innerHTML of the template to the provided HTML string. 1358 template.innerHTML = htmlString; 1359 1360 // Return the contents of the temporary template. 1361 return template.content.childNodes; 1362 }; 1363 1364 let $newContent = $(parseHTML(response.data)); 1365 1366 // For backward compatibility, in some cases a wrapper will be added. This 1367 // behavior will be removed before Drupal 9.0.0. If different behavior is 1368 // needed, the theme functions can be overridden. 1369 // @see https://www.drupal.org/node/2940704 1370 $newContent = Drupal.theme( 1371 'ajaxWrapperNewContent', 1372 $newContent, 1373 ajax, 1374 response, 1375 ); 1376 1377 // If removing content from the wrapper, detach behaviors first. 1378 switch (method) { 1379 case 'html': 1380 case 'replaceWith': 1381 case 'replaceAll': 1382 case 'empty': 1383 case 'remove': 1384 Drupal.detachBehaviors($wrapper.get(0), settings); 1385 break; 1386 default: 1387 break; 1388 } 1389 1390 // Add the new content to the page. 1391 $wrapper[method]($newContent); 1392 1393 // Immediately hide the new content if we're using any effects. 1394 if (effect.showEffect !== 'show') { 1395 $newContent.hide(); 1396 } 1397 1398 // Determine which effect to use and what content will receive the 1399 // effect, then show the new content. 1400 const $ajaxNewContent = $newContent.find('.ajax-new-content'); 1401 if ($ajaxNewContent.length) { 1402 $ajaxNewContent.hide(); 1403 $newContent.show(); 1404 $ajaxNewContent[effect.showEffect](effect.showSpeed); 1405 } else if (effect.showEffect !== 'show') { 1406 $newContent[effect.showEffect](effect.showSpeed); 1407 } 1408 1409 // Attach behaviors to all element nodes. 1410 $newContent.each((index, element) => { 1411 if ( 1412 element.nodeType === Node.ELEMENT_NODE && 1413 // Attach all JavaScript behaviors to the new content, if it was 1414 // successfully added to the page, this condition allows 1415 // `#ajax['wrapper']` to be optional. 1416 document.documentElement.contains(element) 1417 ) { 1418 Drupal.attachBehaviors(element, settings); 1419 } 1420 }); 1421 }, 1422 1423 /** 1424 * Command to remove a chunk from the page. 1425 * 1426 * @param {Drupal.Ajax} [ajax] 1427 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1428 * @param {object} response 1429 * The response from the Ajax request. 1430 * @param {string} response.selector 1431 * A jQuery selector string. 1432 * @param {object} [response.settings] 1433 * An optional array of settings that will be used. 1434 * @param {number} [status] 1435 * The XMLHttpRequest status. 1436 */ 1437 remove(ajax, response, status) { 1438 const settings = response.settings || ajax.settings || drupalSettings; 1439 $(response.selector) 1440 .each(function () { 1441 Drupal.detachBehaviors(this, settings); 1442 }) 1443 .remove(); 1444 }, 1445 1446 /** 1447 * Command to mark a chunk changed. 1448 * 1449 * @param {Drupal.Ajax} [ajax] 1450 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1451 * @param {object} response 1452 * The JSON response object from the Ajax request. 1453 * @param {string} response.selector 1454 * A jQuery selector string. 1455 * @param {string} [response.asterisk] 1456 * An optional CSS selector. If specified, an asterisk will be 1457 * appended to the HTML inside the provided selector. 1458 * @param {number} [status] 1459 * The request status. 1460 */ 1461 changed(ajax, response, status) { 1462 const $element = $(response.selector); 1463 if (!$element.hasClass('ajax-changed')) { 1464 $element.addClass('ajax-changed'); 1465 if (response.asterisk) { 1466 $element 1467 .find(response.asterisk) 1468 .append( 1469 ` <abbr class="ajax-changed" title="${Drupal.t( 1470 'Changed', 1471 )}">*</abbr> `, 1472 ); 1473 } 1474 } 1475 }, 1476 1477 /** 1478 * Command to provide an alert. 1479 * 1480 * @param {Drupal.Ajax} [ajax] 1481 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1482 * @param {object} response 1483 * The JSON response from the Ajax request. 1484 * @param {string} response.text 1485 * The text that will be displayed in an alert dialog. 1486 * @param {number} [status] 1487 * The XMLHttpRequest status. 1488 */ 1489 alert(ajax, response, status) { 1490 window.alert(response.text); 1491 }, 1492 1493 /** 1494 * Command to provide triggers audio UAs to read the supplied text. 1495 * 1496 * @param {Drupal.Ajax} [ajax] 1497 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1498 * @param {object} response 1499 * The JSON response from the Ajax request. 1500 * @param {string} [response.text] 1501 * The text that will be read. 1502 * @param {string} [response.priority] 1503 * An optional priority that will be used for the announcement. 1504 */ 1505 announce(ajax, response) { 1506 if (response.priority) { 1507 Drupal.announce(response.text, response.priority); 1508 } else { 1509 Drupal.announce(response.text); 1510 } 1511 }, 1512 1513 /** 1514 * Command to set the window.location, redirecting the browser. 1515 * 1516 * @param {Drupal.Ajax} [ajax] 1517 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1518 * @param {object} response 1519 * The response from the Ajax request. 1520 * @param {string} response.url 1521 * The URL to redirect to. 1522 * @param {number} [status] 1523 * The XMLHttpRequest status. 1524 */ 1525 redirect(ajax, response, status) { 1526 window.location = response.url; 1527 }, 1528 1529 /** 1530 * Command to provide the jQuery css() function. 1531 * 1532 * @param {Drupal.Ajax} [ajax] 1533 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1534 * @param {object} response 1535 * The response from the Ajax request. 1536 * @param {string} response.selector 1537 * A jQuery selector string. 1538 * @param {object} response.argument 1539 * An array of key/value pairs to set in the CSS for the selector. 1540 * @param {number} [status] 1541 * The XMLHttpRequest status. 1542 */ 1543 css(ajax, response, status) { 1544 // eslint-disable-next-line no-jquery/no-css 1545 $(response.selector).css(response.argument); 1546 }, 1547 1548 /** 1549 * Command to set the settings used for other commands in this response. 1550 * 1551 * This method will also remove expired `drupalSettings.ajax` settings. 1552 * 1553 * @param {Drupal.Ajax} [ajax] 1554 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1555 * @param {object} response 1556 * The response from the Ajax request. 1557 * @param {boolean} response.merge 1558 * Determines whether the additional settings should be merged to the 1559 * global settings. 1560 * @param {object} response.settings 1561 * Contains additional settings to add to the global settings. 1562 * @param {number} [status] 1563 * The XMLHttpRequest status. 1564 */ 1565 settings(ajax, response, status) { 1566 const ajaxSettings = drupalSettings.ajax; 1567 1568 // Clean up drupalSettings.ajax. 1569 if (ajaxSettings) {
1570 Drupal.ajax.expired().forEach((instance) => { 1571 // If the Ajax object has been created through drupalSettings.ajax 1572 // it will have a selector. When there is no selector the object 1573 // has been initialized with a special class name picked up by the 1574 // Ajax behavior. 1575 1576 if (instance.selector) { 1577 const selector = instance.selector.replace('#', ''); 1578 if (selector in ajaxSettings) { 1579 delete ajaxSettings[selector]; 1580 } 1581 } 1582 }); 1583 } 1584 1585 if (response.merge) { 1586 $.extend(true, drupalSettings, response.settings); 1587 } else { 1588 ajax.settings = response.settings; 1589 } 1590 }, 1591 1592 /** 1593 * Command to attach data using jQuery's data API. 1594 * 1595 * @param {Drupal.Ajax} [ajax] 1596 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1597 * @param {object} response 1598 * The response from the Ajax request. 1599 * @param {string} response.name 1600 * The name or key (in the key value pair) of the data attached to this 1601 * selector. 1602 * @param {string} response.selector 1603 * A jQuery selector string. 1604 * @param {string|object} response.value 1605 * The value of to be attached. 1606 * @param {number} [status] 1607 * The XMLHttpRequest status. 1608 */ 1609 data(ajax, response, status) { 1610 $(response.selector).data(response.name, response.value); 1611 }, 1612 1613 /** 1614 * Command to focus the first tabbable element within a container. 1615 * 1616 * If no tabbable elements are found and the container is focusable, then 1617 * focus will move to that container. 1618 * 1619 * @param {Drupal.Ajax} [ajax] 1620 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1621 * @param {object} response 1622 * The response from the Ajax request. 1623 * @param {string} response.selector 1624 * A query selector string of the container to focus within. 1625 * @param {number} [status] 1626 * The XMLHttpRequest status. 1627 */ 1628 focusFirst(ajax, response, status) { 1629 let focusChanged = false; 1630 const container = document.querySelector(response.selector); 1631 if (container) { 1632 // Find all tabbable elements within the container. 1633 const tabbableElements = tabbable(container); 1634 1635 // Move focus to the first tabbable item found. 1636 if (tabbableElements.length) { 1637 tabbableElements[0].focus(); 1638 focusChanged = true; 1639 } else if (isFocusable(container)) { 1640 // If no tabbable elements are found, but the container is focusable, 1641 // move focus to the container. 1642 container.focus(); 1643 focusChanged = true; 1644 } 1645 } 1646 1647 // If no items were available to receive focus, return focus to the 1648 // triggering element. 1649 if (ajax.hasOwnProperty('element') && !focusChanged) { 1650 ajax.element.focus(); 1651 } 1652 }, 1653 1654 /** 1655 * Command to apply a jQuery method. 1656 * 1657 * @param {Drupal.Ajax} [ajax] 1658 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1659 * @param {object} response 1660 * The response from the Ajax request. 1661 * @param {Array} response.args 1662 * An array of arguments to the jQuery method, if any. 1663 * @param {string} response.method 1664 * The jQuery method to invoke. 1665 * @param {string} response.selector 1666 * A jQuery selector string. 1667 * @param {number} [status] 1668 * The XMLHttpRequest status. 1669 */ 1670 invoke(ajax, response, status) { 1671 const $element = $(response.selector); 1672 $element[response.method](...response.args); 1673 }, 1674 1675 /** 1676 * Command to restripe a table. 1677 * 1678 * @param {Drupal.Ajax} [ajax] 1679 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1680 * @param {object} response 1681 * The response from the Ajax request. 1682 * @param {string} response.selector 1683 * A jQuery selector string. 1684 * @param {number} [status] 1685 * The XMLHttpRequest status. 1686 */ 1687 restripe(ajax, response, status) { 1688 // :even and :odd are reversed because jQuery counts from 0 and 1689 // we count from 1, so we're out of sync. 1690 // Match immediate children of the parent element to allow nesting. 1691 $(response.selector) 1692 .find('> tbody > tr:visible, > tr:visible') 1693 .removeClass('odd even') 1694 .filter(':even') 1695 .addClass('odd') 1696 .end() 1697 .filter(':odd') 1698 .addClass('even'); 1699 }, 1700 1701 /** 1702 * Command to update a form's build ID. 1703 * 1704 * @param {Drupal.Ajax} [ajax] 1705 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1706 * @param {object} response 1707 * The response from the Ajax request. 1708 * @param {string} response.old 1709 * The old form build ID. 1710 * @param {string} response.new 1711 * The new form build ID. 1712 * @param {number} [status] 1713 * The XMLHttpRequest status. 1714 */ 1715 update_build_id(ajax, response, status) { 1716 document 1717 .querySelectorAll( 1718 `input[name="form_build_id"][value="${response.old}"]`, 1719 ) 1720 .forEach((item) => { 1721 item.value = response.new; 1722 }); 1723 }, 1724 1725 /** 1726 * Command to add css. 1727 * 1728 * @param {Drupal.Ajax} [ajax] 1729 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1730 * @param {object} response 1731 * The response from the Ajax request. 1732 * @param {object[]} response.data 1733 * An array of styles to be added. 1734 * @param {number} [status] 1735 * The XMLHttpRequest status. 1736 */ 1737 add_css(ajax, response, status) { 1738 const allUniqueBundleIds = response.data.map(function (style) { 1739 const uniqueBundleId = style.href; 1740 // Force file to load as a CSS stylesheet using 'css!' flag.
1741 if (!loadjs.isDefined(uniqueBundleId)) { 1742 loadjs(`css!${style.href}`, uniqueBundleId, { 1743 before(path, styleEl) { 1744 // This allows all attributes to be added, like media. 1745 Object.keys(style).forEach((attributeKey) => { 1746 styleEl.setAttribute(attributeKey, style[attributeKey]); 1747 }); 1748 }, 1749 }); 1750 } 1751 return uniqueBundleId; 1752 }); 1753 // Returns the promise so that the next AJAX command waits on the 1754 // completion of this one to execute, ensuring the CSS is loaded before 1755 // executing. 1756 return new Promise((resolve, reject) => { 1757 loadjs.ready(allUniqueBundleIds, { 1758 success() { 1759 // All CSS files were loaded. Resolve the promise and let the 1760 // remaining commands execute. 1761 resolve(); 1762 }, 1763 error(depsNotFound) { 1764 const message = Drupal.t( 1765 `The following files could not be loaded: @dependencies`, 1766 { '@dependencies': depsNotFound.join(', ') }, 1767 ); 1768 reject(message); 1769 }, 1770 }); 1771 }); 1772 }, 1773 1774 /** 1775 * Command to add a message to the message area. 1776 * 1777 * @param {Drupal.Ajax} [ajax] 1778 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1779 * @param {object} response 1780 * The response from the Ajax request. 1781 * @param {string} response.messageWrapperQuerySelector 1782 * The zone where to add the message. If null, the default will be used. 1783 * @param {string} response.message 1784 * The message text. 1785 * @param {string} response.messageOptions 1786 * The options argument for Drupal.Message().add(). 1787 * @param {boolean} response.clearPrevious 1788 * If true, clear previous messages. 1789 */ 1790 message(ajax, response) { 1791 const messages = new Drupal.Message( 1792 document.querySelector(response.messageWrapperQuerySelector), 1793 ); 1794 if (response.clearPrevious) { 1795 messages.clear(); 1796 } 1797 messages.add(response.message, response.messageOptions); 1798 }, 1799 1800 /** 1801 * Command to add JS. 1802 * 1803 * @param {Drupal.Ajax} [ajax] 1804 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. 1805 * @param {object} response 1806 * The response from the Ajax request. 1807 * @param {Array} response.data 1808 * An array of objects of script attributes. 1809 * @param {number} [status] 1810 * The XMLHttpRequest status. 1811 */ 1812 add_js(ajax, response, status) { 1813 const parentEl = document.querySelector(response.selector || 'body'); 1814 const settings = ajax.settings || drupalSettings; 1815 const allUniqueBundleIds = response.data.map((script) => { 1816 const uniqueBundleId = script.src; 1817 if (!loadjs.isDefined(uniqueBundleId)) { 1818 loadjs(script.src, uniqueBundleId, { 1819 // The default loadjs behavior is to load script with async, in Drupal 1820 // we need to explicitly tell scripts to load async, this is set in 1821 // the before callback below if necessary. 1822 async: false, 1823 before(path, scriptEl) { 1824 // This allows all attributes to be added, like defer, async and 1825 // crossorigin. 1826 Object.keys(script).forEach((attributeKey) => { 1827 scriptEl.setAttribute(attributeKey, script[attributeKey]); 1828 }); 1829 1830 // By default, loadjs appends the script to the head. When scripts 1831 // are loaded via AJAX, their location has no impact on 1832 // functionality. But, since non-AJAX loaded scripts can choose 1833 // their parent element, we provide that option here for the sake of 1834 // consistency. 1835 parentEl.appendChild(scriptEl); 1836 // Return false to bypass loadjs' default DOM insertion mechanism. 1837 return false; 1838 }, 1839 }); 1840 } 1841 return uniqueBundleId; 1842 }); 1843 // Returns the promise so that the next AJAX command waits on the 1844 // completion of this one to execute, ensuring the JS is loaded before 1845 // executing. 1846 return new Promise((resolve, reject) => { 1847 loadjs.ready(allUniqueBundleIds, { 1848 success() { 1849 Drupal.attachBehaviors(parentEl, settings); 1850 // All JS files were loaded and new and old behaviors have 1851 // been attached. Resolve the promise and let the remaining 1852 // commands execute. 1853 resolve(); 1854 }, 1855 error(depsNotFound) { 1856 const message = Drupal.t( 1857 `The following files could not be loaded: @dependencies`, 1858 { '@dependencies': depsNotFound.join(', ') }, 1859 ); 1860 reject(message); 1861 }, 1862 }); 1863 }); 1864 }, 1865 1866 /** 1867 * Command to scroll the page to an html element. 1868 * 1869 * @param {Drupal.Ajax} [ajax] 1870 * A {@link Drupal.ajax} object. 1871 * @param {object} response 1872 * Ajax response. 1873 * @param {string} response.selector 1874 * Selector to use. 1875 */ 1876 scrollTop(ajax, response) { 1877 document.querySelector(response.selector)?.scrollIntoView(); 1878 }, 1879 }; 1880 1881 /** 1882 * Delay jQuery's global completion events until after commands have executed. 1883 *
1884 * jQuery triggers the ajaxSuccess, ajaxComplete, and ajaxStop events after 1885 * a successful response is returned and local success and complete events 1886 * are triggered. However, Drupal Ajax responses contain commands that run 1887 * asynchronously in a queue, so the following stops these events from getting 1888 * triggered until after the Promise that executes the command queue is 1889 * resolved. 1890 */ 1891 const stopEvent = (xhr, settings) => { 1892 return ( 1893 // Only interfere with Drupal's Ajax responses. 1894 xhr.getResponseHeader('X-Drupal-Ajax-Token') === '1' && 1895 // The isInProgress() function might not be defined if the Ajax request 1896 // was initiated without Drupal.ajax() or new Drupal.Ajax(). 1897 typeof settings.isInProgress === 'function' && 1898 // Until this is false, the Ajax request isn't completely done (the 1899 // response's commands might still be running). 1900 settings.isInProgress() 1901 ); 1902 }; 1903 $.extend(true, $.event.special, { 1904 ajaxSuccess: { 1905 trigger(event, xhr, settings) { 1906 if (stopEvent(xhr, settings)) { 1907 return false; 1908 } 1909 }, 1910 }, 1911 ajaxComplete: { 1912 trigger(event, xhr, settings) { 1913 if (stopEvent(xhr, settings)) { 1914 // jQuery decrements its internal active ajax counter even when we 1915 // stop the ajaxComplete event, but we don't want that counter 1916 // decremented, because for our purposes this request is still active 1917 // while commands are executing. By incrementing it here, the net 1918 // effect is that it remains unchanged. By remaining above 0, the 1919 // ajaxStop event is also prevented. 1920 $.active++; 1921 return false; 1922 } 1923 }, 1924 }, 1925 }); 1926 1927 Drupal.behaviors.clientSideValidationAjax = { 1928 attach(context) { 1929 $(context).on('drupalAjaxFormValidate', (event) => { 1930 const form = $(event.target).closest('form')[0]; 1931 if (typeof form !== 'undefined' && !form.checkValidity()) { 1932 // This is the magic function that displays the validation errors to the user 1933 form.reportValidity(); 1934 return false; 1935 } 1936 return true; 1937 }); 1938 }, 1939 }; 1940})(jQuery, window, Drupal, drupalSettings, loadjs, 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.