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