annotate core/misc/ajax.es6.js @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
rev   line source
Chris@0 1 /**
Chris@0 2 * @file
Chris@0 3 * Provides Ajax page updating via jQuery $.ajax.
Chris@0 4 *
Chris@0 5 * Ajax is a method of making a request via JavaScript while viewing an HTML
Chris@0 6 * page. The request returns an array of commands encoded in JSON, which is
Chris@0 7 * then executed to make any changes that are necessary to the page.
Chris@0 8 *
Chris@0 9 * Drupal uses this file to enhance form elements with `#ajax['url']` and
Chris@0 10 * `#ajax['wrapper']` properties. If set, this file will automatically be
Chris@0 11 * included to provide Ajax capabilities.
Chris@0 12 */
Chris@0 13
Chris@0 14 (function ($, window, Drupal, drupalSettings) {
Chris@0 15 /**
Chris@0 16 * Attaches the Ajax behavior to each Ajax form element.
Chris@0 17 *
Chris@0 18 * @type {Drupal~behavior}
Chris@0 19 *
Chris@0 20 * @prop {Drupal~behaviorAttach} attach
Chris@0 21 * Initialize all {@link Drupal.Ajax} objects declared in
Chris@0 22 * `drupalSettings.ajax` or initialize {@link Drupal.Ajax} objects from
Chris@0 23 * DOM elements having the `use-ajax-submit` or `use-ajax` css class.
Chris@0 24 * @prop {Drupal~behaviorDetach} detach
Chris@0 25 * During `unload` remove all {@link Drupal.Ajax} objects related to
Chris@0 26 * the removed content.
Chris@0 27 */
Chris@0 28 Drupal.behaviors.AJAX = {
Chris@0 29 attach(context, settings) {
Chris@0 30 function loadAjaxBehavior(base) {
Chris@0 31 const elementSettings = settings.ajax[base];
Chris@0 32 if (typeof elementSettings.selector === 'undefined') {
Chris@0 33 elementSettings.selector = `#${base}`;
Chris@0 34 }
Chris@0 35 $(elementSettings.selector).once('drupal-ajax').each(function () {
Chris@0 36 elementSettings.element = this;
Chris@0 37 elementSettings.base = base;
Chris@0 38 Drupal.ajax(elementSettings);
Chris@0 39 });
Chris@0 40 }
Chris@0 41
Chris@0 42 // Load all Ajax behaviors specified in the settings.
Chris@0 43 Object.keys(settings.ajax || {}).forEach(base => loadAjaxBehavior(base));
Chris@0 44
Chris@0 45 Drupal.ajax.bindAjaxLinks(document.body);
Chris@0 46
Chris@0 47 // This class means to submit the form to the action using Ajax.
Chris@0 48 $('.use-ajax-submit').once('ajax').each(function () {
Chris@0 49 const elementSettings = {};
Chris@0 50
Chris@0 51 // Ajax submits specified in this manner automatically submit to the
Chris@0 52 // normal form action.
Chris@0 53 elementSettings.url = $(this.form).attr('action');
Chris@0 54 // Form submit button clicks need to tell the form what was clicked so
Chris@0 55 // it gets passed in the POST request.
Chris@0 56 elementSettings.setClick = true;
Chris@0 57 // Form buttons use the 'click' event rather than mousedown.
Chris@0 58 elementSettings.event = 'click';
Chris@0 59 // Clicked form buttons look better with the throbber than the progress
Chris@0 60 // bar.
Chris@0 61 elementSettings.progress = { type: 'throbber' };
Chris@0 62 elementSettings.base = $(this).attr('id');
Chris@0 63 elementSettings.element = this;
Chris@0 64
Chris@0 65 Drupal.ajax(elementSettings);
Chris@0 66 });
Chris@0 67 },
Chris@0 68
Chris@0 69 detach(context, settings, trigger) {
Chris@0 70 if (trigger === 'unload') {
Chris@0 71 Drupal.ajax.expired().forEach((instance) => {
Chris@0 72 // Set this to null and allow garbage collection to reclaim
Chris@0 73 // the memory.
Chris@0 74 Drupal.ajax.instances[instance.instanceIndex] = null;
Chris@0 75 });
Chris@0 76 }
Chris@0 77 },
Chris@0 78 };
Chris@0 79
Chris@0 80 /**
Chris@0 81 * Extends Error to provide handling for Errors in Ajax.
Chris@0 82 *
Chris@0 83 * @constructor
Chris@0 84 *
Chris@0 85 * @augments Error
Chris@0 86 *
Chris@0 87 * @param {XMLHttpRequest} xmlhttp
Chris@0 88 * XMLHttpRequest object used for the failed request.
Chris@0 89 * @param {string} uri
Chris@0 90 * The URI where the error occurred.
Chris@0 91 * @param {string} customMessage
Chris@0 92 * The custom message.
Chris@0 93 */
Chris@0 94 Drupal.AjaxError = function (xmlhttp, uri, customMessage) {
Chris@0 95 let statusCode;
Chris@0 96 let statusText;
Chris@0 97 let responseText;
Chris@0 98 if (xmlhttp.status) {
Chris@0 99 statusCode = `\n${Drupal.t('An AJAX HTTP error occurred.')}\n${Drupal.t('HTTP Result Code: !status', { '!status': xmlhttp.status })}`;
Chris@0 100 }
Chris@0 101 else {
Chris@0 102 statusCode = `\n${Drupal.t('An AJAX HTTP request terminated abnormally.')}`;
Chris@0 103 }
Chris@0 104 statusCode += `\n${Drupal.t('Debugging information follows.')}`;
Chris@0 105 const pathText = `\n${Drupal.t('Path: !uri', { '!uri': uri })}`;
Chris@0 106 statusText = '';
Chris@0 107 // In some cases, when statusCode === 0, xmlhttp.statusText may not be
Chris@0 108 // defined. Unfortunately, testing for it with typeof, etc, doesn't seem to
Chris@0 109 // catch that and the test causes an exception. So we need to catch the
Chris@0 110 // exception here.
Chris@0 111 try {
Chris@0 112 statusText = `\n${Drupal.t('StatusText: !statusText', { '!statusText': $.trim(xmlhttp.statusText) })}`;
Chris@0 113 }
Chris@0 114 catch (e) {
Chris@0 115 // Empty.
Chris@0 116 }
Chris@0 117
Chris@0 118 responseText = '';
Chris@0 119 // Again, we don't have a way to know for sure whether accessing
Chris@0 120 // xmlhttp.responseText is going to throw an exception. So we'll catch it.
Chris@0 121 try {
Chris@0 122 responseText = `\n${Drupal.t('ResponseText: !responseText', { '!responseText': $.trim(xmlhttp.responseText) })}`;
Chris@0 123 }
Chris@0 124 catch (e) {
Chris@0 125 // Empty.
Chris@0 126 }
Chris@0 127
Chris@0 128 // Make the responseText more readable by stripping HTML tags and newlines.
Chris@0 129 responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, '');
Chris@0 130 responseText = responseText.replace(/[\n]+\s+/g, '\n');
Chris@0 131
Chris@0 132 // We don't need readyState except for status == 0.
Chris@0 133 const readyStateText = xmlhttp.status === 0 ? (`\n${Drupal.t('ReadyState: !readyState', { '!readyState': xmlhttp.readyState })}`) : '';
Chris@0 134
Chris@0 135 customMessage = customMessage ? (`\n${Drupal.t('CustomMessage: !customMessage', { '!customMessage': customMessage })}`) : '';
Chris@0 136
Chris@0 137 /**
Chris@0 138 * Formatted and translated error message.
Chris@0 139 *
Chris@0 140 * @type {string}
Chris@0 141 */
Chris@0 142 this.message = statusCode + pathText + statusText + customMessage + responseText + readyStateText;
Chris@0 143
Chris@0 144 /**
Chris@0 145 * Used by some browsers to display a more accurate stack trace.
Chris@0 146 *
Chris@0 147 * @type {string}
Chris@0 148 */
Chris@0 149 this.name = 'AjaxError';
Chris@0 150 };
Chris@0 151
Chris@0 152 Drupal.AjaxError.prototype = new Error();
Chris@0 153 Drupal.AjaxError.prototype.constructor = Drupal.AjaxError;
Chris@0 154
Chris@0 155 /**
Chris@0 156 * Provides Ajax page updating via jQuery $.ajax.
Chris@0 157 *
Chris@0 158 * This function is designed to improve developer experience by wrapping the
Chris@0 159 * initialization of {@link Drupal.Ajax} objects and storing all created
Chris@0 160 * objects in the {@link Drupal.ajax.instances} array.
Chris@0 161 *
Chris@0 162 * @example
Chris@0 163 * Drupal.behaviors.myCustomAJAXStuff = {
Chris@0 164 * attach: function (context, settings) {
Chris@0 165 *
Chris@0 166 * var ajaxSettings = {
Chris@0 167 * url: 'my/url/path',
Chris@0 168 * // If the old version of Drupal.ajax() needs to be used those
Chris@0 169 * // properties can be added
Chris@0 170 * base: 'myBase',
Chris@0 171 * element: $(context).find('.someElement')
Chris@0 172 * };
Chris@0 173 *
Chris@0 174 * var myAjaxObject = Drupal.ajax(ajaxSettings);
Chris@0 175 *
Chris@0 176 * // Declare a new Ajax command specifically for this Ajax object.
Chris@0 177 * myAjaxObject.commands.insert = function (ajax, response, status) {
Chris@0 178 * $('#my-wrapper').append(response.data);
Chris@0 179 * alert('New content was appended to #my-wrapper');
Chris@0 180 * };
Chris@0 181 *
Chris@0 182 * // This command will remove this Ajax object from the page.
Chris@0 183 * myAjaxObject.commands.destroyObject = function (ajax, response, status) {
Chris@0 184 * Drupal.ajax.instances[this.instanceIndex] = null;
Chris@0 185 * };
Chris@0 186 *
Chris@0 187 * // Programmatically trigger the Ajax request.
Chris@0 188 * myAjaxObject.execute();
Chris@0 189 * }
Chris@0 190 * };
Chris@0 191 *
Chris@0 192 * @param {object} settings
Chris@0 193 * The settings object passed to {@link Drupal.Ajax} constructor.
Chris@0 194 * @param {string} [settings.base]
Chris@0 195 * Base is passed to {@link Drupal.Ajax} constructor as the 'base'
Chris@0 196 * parameter.
Chris@0 197 * @param {HTMLElement} [settings.element]
Chris@0 198 * Element parameter of {@link Drupal.Ajax} constructor, element on which
Chris@0 199 * event listeners will be bound.
Chris@0 200 *
Chris@0 201 * @return {Drupal.Ajax}
Chris@0 202 * The created Ajax object.
Chris@0 203 *
Chris@0 204 * @see Drupal.AjaxCommands
Chris@0 205 */
Chris@0 206 Drupal.ajax = function (settings) {
Chris@0 207 if (arguments.length !== 1) {
Chris@0 208 throw new Error('Drupal.ajax() function must be called with one configuration object only');
Chris@0 209 }
Chris@0 210 // Map those config keys to variables for the old Drupal.ajax function.
Chris@0 211 const base = settings.base || false;
Chris@0 212 const element = settings.element || false;
Chris@0 213 delete settings.base;
Chris@0 214 delete settings.element;
Chris@0 215
Chris@0 216 // By default do not display progress for ajax calls without an element.
Chris@0 217 if (!settings.progress && !element) {
Chris@0 218 settings.progress = false;
Chris@0 219 }
Chris@0 220
Chris@0 221 const ajax = new Drupal.Ajax(base, element, settings);
Chris@0 222 ajax.instanceIndex = Drupal.ajax.instances.length;
Chris@0 223 Drupal.ajax.instances.push(ajax);
Chris@0 224
Chris@0 225 return ajax;
Chris@0 226 };
Chris@0 227
Chris@0 228 /**
Chris@0 229 * Contains all created Ajax objects.
Chris@0 230 *
Chris@0 231 * @type {Array.<Drupal.Ajax|null>}
Chris@0 232 */
Chris@0 233 Drupal.ajax.instances = [];
Chris@0 234
Chris@0 235 /**
Chris@0 236 * List all objects where the associated element is not in the DOM
Chris@0 237 *
Chris@0 238 * This method ignores {@link Drupal.Ajax} objects not bound to DOM elements
Chris@0 239 * when created with {@link Drupal.ajax}.
Chris@0 240 *
Chris@0 241 * @return {Array.<Drupal.Ajax>}
Chris@0 242 * The list of expired {@link Drupal.Ajax} objects.
Chris@0 243 */
Chris@0 244 Drupal.ajax.expired = function () {
Chris@0 245 return Drupal.ajax.instances.filter(instance => instance && instance.element !== false && !document.body.contains(instance.element));
Chris@0 246 };
Chris@0 247
Chris@0 248 /**
Chris@0 249 * Bind Ajax functionality to links that use the 'use-ajax' class.
Chris@0 250 *
Chris@0 251 * @param {HTMLElement} element
Chris@0 252 * Element to enable Ajax functionality for.
Chris@0 253 */
Chris@0 254 Drupal.ajax.bindAjaxLinks = (element) => {
Chris@0 255 // Bind Ajax behaviors to all items showing the class.
Chris@0 256 $(element).find('.use-ajax').once('ajax').each((i, ajaxLink) => {
Chris@0 257 const $linkElement = $(ajaxLink);
Chris@0 258
Chris@0 259 const elementSettings = {
Chris@0 260 // Clicked links look better with the throbber than the progress bar.
Chris@0 261 progress: { type: 'throbber' },
Chris@0 262 dialogType: $linkElement.data('dialog-type'),
Chris@0 263 dialog: $linkElement.data('dialog-options'),
Chris@0 264 dialogRenderer: $linkElement.data('dialog-renderer'),
Chris@0 265 base: $linkElement.attr('id'),
Chris@0 266 element: ajaxLink,
Chris@0 267 };
Chris@0 268 const href = $linkElement.attr('href');
Chris@0 269 /**
Chris@0 270 * For anchor tags, these will go to the target of the anchor rather
Chris@0 271 * than the usual location.
Chris@0 272 */
Chris@0 273 if (href) {
Chris@0 274 elementSettings.url = href;
Chris@0 275 elementSettings.event = 'click';
Chris@0 276 }
Chris@0 277 Drupal.ajax(elementSettings);
Chris@0 278 });
Chris@0 279 };
Chris@0 280
Chris@0 281 /**
Chris@0 282 * Settings for an Ajax object.
Chris@0 283 *
Chris@0 284 * @typedef {object} Drupal.Ajax~elementSettings
Chris@0 285 *
Chris@0 286 * @prop {string} url
Chris@0 287 * Target of the Ajax request.
Chris@0 288 * @prop {?string} [event]
Chris@0 289 * Event bound to settings.element which will trigger the Ajax request.
Chris@0 290 * @prop {bool} [keypress=true]
Chris@0 291 * Triggers a request on keypress events.
Chris@0 292 * @prop {?string} selector
Chris@0 293 * jQuery selector targeting the element to bind events to or used with
Chris@0 294 * {@link Drupal.AjaxCommands}.
Chris@0 295 * @prop {string} [effect='none']
Chris@0 296 * Name of the jQuery method to use for displaying new Ajax content.
Chris@0 297 * @prop {string|number} [speed='none']
Chris@0 298 * Speed with which to apply the effect.
Chris@0 299 * @prop {string} [method]
Chris@0 300 * Name of the jQuery method used to insert new content in the targeted
Chris@0 301 * element.
Chris@0 302 * @prop {object} [progress]
Chris@0 303 * Settings for the display of a user-friendly loader.
Chris@0 304 * @prop {string} [progress.type='throbber']
Chris@0 305 * Type of progress element, core provides `'bar'`, `'throbber'` and
Chris@0 306 * `'fullscreen'`.
Chris@0 307 * @prop {string} [progress.message=Drupal.t('Please wait...')]
Chris@0 308 * Custom message to be used with the bar indicator.
Chris@0 309 * @prop {object} [submit]
Chris@0 310 * Extra data to be sent with the Ajax request.
Chris@0 311 * @prop {bool} [submit.js=true]
Chris@0 312 * Allows the PHP side to know this comes from an Ajax request.
Chris@0 313 * @prop {object} [dialog]
Chris@0 314 * Options for {@link Drupal.dialog}.
Chris@0 315 * @prop {string} [dialogType]
Chris@0 316 * One of `'modal'` or `'dialog'`.
Chris@0 317 * @prop {string} [prevent]
Chris@0 318 * List of events on which to stop default action and stop propagation.
Chris@0 319 */
Chris@0 320
Chris@0 321 /**
Chris@0 322 * Ajax constructor.
Chris@0 323 *
Chris@0 324 * The Ajax request returns an array of commands encoded in JSON, which is
Chris@0 325 * then executed to make any changes that are necessary to the page.
Chris@0 326 *
Chris@0 327 * Drupal uses this file to enhance form elements with `#ajax['url']` and
Chris@0 328 * `#ajax['wrapper']` properties. If set, this file will automatically be
Chris@0 329 * included to provide Ajax capabilities.
Chris@0 330 *
Chris@0 331 * @constructor
Chris@0 332 *
Chris@0 333 * @param {string} [base]
Chris@0 334 * Base parameter of {@link Drupal.Ajax} constructor
Chris@0 335 * @param {HTMLElement} [element]
Chris@0 336 * Element parameter of {@link Drupal.Ajax} constructor, element on which
Chris@0 337 * event listeners will be bound.
Chris@0 338 * @param {Drupal.Ajax~elementSettings} elementSettings
Chris@0 339 * Settings for this Ajax object.
Chris@0 340 */
Chris@0 341 Drupal.Ajax = function (base, element, elementSettings) {
Chris@0 342 const defaults = {
Chris@0 343 event: element ? 'mousedown' : null,
Chris@0 344 keypress: true,
Chris@0 345 selector: base ? `#${base}` : null,
Chris@0 346 effect: 'none',
Chris@0 347 speed: 'none',
Chris@0 348 method: 'replaceWith',
Chris@0 349 progress: {
Chris@0 350 type: 'throbber',
Chris@0 351 message: Drupal.t('Please wait...'),
Chris@0 352 },
Chris@0 353 submit: {
Chris@0 354 js: true,
Chris@0 355 },
Chris@0 356 };
Chris@0 357
Chris@0 358 $.extend(this, defaults, elementSettings);
Chris@0 359
Chris@0 360 /**
Chris@0 361 * @type {Drupal.AjaxCommands}
Chris@0 362 */
Chris@0 363 this.commands = new Drupal.AjaxCommands();
Chris@0 364
Chris@0 365 /**
Chris@0 366 * @type {bool|number}
Chris@0 367 */
Chris@0 368 this.instanceIndex = false;
Chris@0 369
Chris@0 370 // @todo Remove this after refactoring the PHP code to:
Chris@0 371 // - Call this 'selector'.
Chris@0 372 // - Include the '#' for ID-based selectors.
Chris@0 373 // - Support non-ID-based selectors.
Chris@0 374 if (this.wrapper) {
Chris@0 375 /**
Chris@0 376 * @type {string}
Chris@0 377 */
Chris@0 378 this.wrapper = `#${this.wrapper}`;
Chris@0 379 }
Chris@0 380
Chris@0 381 /**
Chris@0 382 * @type {HTMLElement}
Chris@0 383 */
Chris@0 384 this.element = element;
Chris@0 385
Chris@0 386 /**
Chris@0 387 * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0.
Chris@0 388 * Use elementSettings.
Chris@0 389 *
Chris@0 390 * @type {Drupal.Ajax~elementSettings}
Chris@0 391 */
Chris@0 392 this.element_settings = elementSettings;
Chris@0 393
Chris@0 394 /**
Chris@0 395 * @type {Drupal.Ajax~elementSettings}
Chris@0 396 */
Chris@0 397 this.elementSettings = elementSettings;
Chris@0 398
Chris@0 399 // If there isn't a form, jQuery.ajax() will be used instead, allowing us to
Chris@0 400 // bind Ajax to links as well.
Chris@0 401 if (this.element && this.element.form) {
Chris@0 402 /**
Chris@0 403 * @type {jQuery}
Chris@0 404 */
Chris@0 405 this.$form = $(this.element.form);
Chris@0 406 }
Chris@0 407
Chris@0 408 // If no Ajax callback URL was given, use the link href or form action.
Chris@0 409 if (!this.url) {
Chris@0 410 const $element = $(this.element);
Chris@0 411 if ($element.is('a')) {
Chris@0 412 this.url = $element.attr('href');
Chris@0 413 }
Chris@0 414 else if (this.element && element.form) {
Chris@0 415 this.url = this.$form.attr('action');
Chris@0 416 }
Chris@0 417 }
Chris@0 418
Chris@0 419 // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let
Chris@0 420 // the server detect when it needs to degrade gracefully.
Chris@0 421 // There are four scenarios to check for:
Chris@0 422 // 1. /nojs/
Chris@0 423 // 2. /nojs$ - The end of a URL string.
Chris@0 424 // 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar).
Chris@0 425 // 4. /nojs# - Followed by a fragment (e.g.: path/nojs#myfragment).
Chris@0 426 const originalUrl = this.url;
Chris@0 427
Chris@0 428 /**
Chris@0 429 * Processed Ajax URL.
Chris@0 430 *
Chris@0 431 * @type {string}
Chris@0 432 */
Chris@0 433 this.url = this.url.replace(/\/nojs(\/|$|\?|#)/g, '/ajax$1');
Chris@0 434 // If the 'nojs' version of the URL is trusted, also trust the 'ajax'
Chris@0 435 // version.
Chris@0 436 if (drupalSettings.ajaxTrustedUrl[originalUrl]) {
Chris@0 437 drupalSettings.ajaxTrustedUrl[this.url] = true;
Chris@0 438 }
Chris@0 439
Chris@0 440 // Set the options for the ajaxSubmit function.
Chris@0 441 // The 'this' variable will not persist inside of the options object.
Chris@0 442 const ajax = this;
Chris@0 443
Chris@0 444 /**
Chris@0 445 * Options for the jQuery.ajax function.
Chris@0 446 *
Chris@0 447 * @name Drupal.Ajax#options
Chris@0 448 *
Chris@0 449 * @type {object}
Chris@0 450 *
Chris@0 451 * @prop {string} url
Chris@0 452 * Ajax URL to be called.
Chris@0 453 * @prop {object} data
Chris@0 454 * Ajax payload.
Chris@0 455 * @prop {function} beforeSerialize
Chris@0 456 * Implement jQuery beforeSerialize function to call
Chris@0 457 * {@link Drupal.Ajax#beforeSerialize}.
Chris@0 458 * @prop {function} beforeSubmit
Chris@0 459 * Implement jQuery beforeSubmit function to call
Chris@0 460 * {@link Drupal.Ajax#beforeSubmit}.
Chris@0 461 * @prop {function} beforeSend
Chris@0 462 * Implement jQuery beforeSend function to call
Chris@0 463 * {@link Drupal.Ajax#beforeSend}.
Chris@0 464 * @prop {function} success
Chris@0 465 * Implement jQuery success function to call
Chris@0 466 * {@link Drupal.Ajax#success}.
Chris@0 467 * @prop {function} complete
Chris@0 468 * Implement jQuery success function to clean up ajax state and trigger an
Chris@0 469 * error if needed.
Chris@0 470 * @prop {string} dataType='json'
Chris@0 471 * Type of the response expected.
Chris@0 472 * @prop {string} type='POST'
Chris@0 473 * HTTP method to use for the Ajax request.
Chris@0 474 */
Chris@0 475 ajax.options = {
Chris@0 476 url: ajax.url,
Chris@0 477 data: ajax.submit,
Chris@0 478 beforeSerialize(elementSettings, options) {
Chris@0 479 return ajax.beforeSerialize(elementSettings, options);
Chris@0 480 },
Chris@0 481 beforeSubmit(formValues, elementSettings, options) {
Chris@0 482 ajax.ajaxing = true;
Chris@0 483 return ajax.beforeSubmit(formValues, elementSettings, options);
Chris@0 484 },
Chris@0 485 beforeSend(xmlhttprequest, options) {
Chris@0 486 ajax.ajaxing = true;
Chris@0 487 return ajax.beforeSend(xmlhttprequest, options);
Chris@0 488 },
Chris@0 489 success(response, status, xmlhttprequest) {
Chris@0 490 // Sanity check for browser support (object expected).
Chris@0 491 // When using iFrame uploads, responses must be returned as a string.
Chris@0 492 if (typeof response === 'string') {
Chris@0 493 response = $.parseJSON(response);
Chris@0 494 }
Chris@0 495
Chris@0 496 // Prior to invoking the response's commands, verify that they can be
Chris@0 497 // trusted by checking for a response header. See
Chris@0 498 // \Drupal\Core\EventSubscriber\AjaxResponseSubscriber for details.
Chris@0 499 // - Empty responses are harmless so can bypass verification. This
Chris@0 500 // avoids an alert message for server-generated no-op responses that
Chris@0 501 // skip Ajax rendering.
Chris@0 502 // - Ajax objects with trusted URLs (e.g., ones defined server-side via
Chris@0 503 // #ajax) can bypass header verification. This is especially useful
Chris@0 504 // for Ajax with multipart forms. Because IFRAME transport is used,
Chris@0 505 // the response headers cannot be accessed for verification.
Chris@0 506 if (response !== null && !drupalSettings.ajaxTrustedUrl[ajax.url]) {
Chris@0 507 if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') {
Chris@0 508 const customMessage = Drupal.t('The response failed verification so will not be processed.');
Chris@0 509 return ajax.error(xmlhttprequest, ajax.url, customMessage);
Chris@0 510 }
Chris@0 511 }
Chris@0 512
Chris@0 513 return ajax.success(response, status);
Chris@0 514 },
Chris@0 515 complete(xmlhttprequest, status) {
Chris@0 516 ajax.ajaxing = false;
Chris@0 517 if (status === 'error' || status === 'parsererror') {
Chris@0 518 return ajax.error(xmlhttprequest, ajax.url);
Chris@0 519 }
Chris@0 520 },
Chris@0 521 dataType: 'json',
Chris@0 522 type: 'POST',
Chris@0 523 };
Chris@0 524
Chris@0 525 if (elementSettings.dialog) {
Chris@0 526 ajax.options.data.dialogOptions = elementSettings.dialog;
Chris@0 527 }
Chris@0 528
Chris@0 529 // Ensure that we have a valid URL by adding ? when no query parameter is
Chris@0 530 // yet available, otherwise append using &.
Chris@0 531 if (ajax.options.url.indexOf('?') === -1) {
Chris@0 532 ajax.options.url += '?';
Chris@0 533 }
Chris@0 534 else {
Chris@0 535 ajax.options.url += '&';
Chris@0 536 }
Chris@0 537 // If this element has a dialog type use if for the wrapper if not use 'ajax'.
Chris@0 538 let wrapper = `drupal_${(elementSettings.dialogType || 'ajax')}`;
Chris@0 539 if (elementSettings.dialogRenderer) {
Chris@0 540 wrapper += `.${elementSettings.dialogRenderer}`;
Chris@0 541 }
Chris@0 542 ajax.options.url += `${Drupal.ajax.WRAPPER_FORMAT}=${wrapper}`;
Chris@0 543
Chris@0 544
Chris@0 545 // Bind the ajaxSubmit function to the element event.
Chris@0 546 $(ajax.element).on(elementSettings.event, function (event) {
Chris@0 547 if (!drupalSettings.ajaxTrustedUrl[ajax.url] && !Drupal.url.isLocal(ajax.url)) {
Chris@0 548 throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', { '!url': ajax.url }));
Chris@0 549 }
Chris@0 550 return ajax.eventResponse(this, event);
Chris@0 551 });
Chris@0 552
Chris@0 553 // If necessary, enable keyboard submission so that Ajax behaviors
Chris@0 554 // can be triggered through keyboard input as well as e.g. a mousedown
Chris@0 555 // action.
Chris@0 556 if (elementSettings.keypress) {
Chris@0 557 $(ajax.element).on('keypress', function (event) {
Chris@0 558 return ajax.keypressResponse(this, event);
Chris@0 559 });
Chris@0 560 }
Chris@0 561
Chris@0 562 // If necessary, prevent the browser default action of an additional event.
Chris@0 563 // For example, prevent the browser default action of a click, even if the
Chris@0 564 // Ajax behavior binds to mousedown.
Chris@0 565 if (elementSettings.prevent) {
Chris@0 566 $(ajax.element).on(elementSettings.prevent, false);
Chris@0 567 }
Chris@0 568 };
Chris@0 569
Chris@0 570 /**
Chris@0 571 * URL query attribute to indicate the wrapper used to render a request.
Chris@0 572 *
Chris@0 573 * The wrapper format determines how the HTML is wrapped, for example in a
Chris@0 574 * modal dialog.
Chris@0 575 *
Chris@0 576 * @const {string}
Chris@0 577 *
Chris@0 578 * @default
Chris@0 579 */
Chris@0 580 Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format';
Chris@0 581
Chris@0 582 /**
Chris@0 583 * Request parameter to indicate that a request is a Drupal Ajax request.
Chris@0 584 *
Chris@0 585 * @const {string}
Chris@0 586 *
Chris@0 587 * @default
Chris@0 588 */
Chris@0 589 Drupal.Ajax.AJAX_REQUEST_PARAMETER = '_drupal_ajax';
Chris@0 590
Chris@0 591 /**
Chris@0 592 * Execute the ajax request.
Chris@0 593 *
Chris@0 594 * Allows developers to execute an Ajax request manually without specifying
Chris@0 595 * an event to respond to.
Chris@0 596 *
Chris@0 597 * @return {object}
Chris@0 598 * Returns the jQuery.Deferred object underlying the Ajax request. If
Chris@0 599 * pre-serialization fails, the Deferred will be returned in the rejected
Chris@0 600 * state.
Chris@0 601 */
Chris@0 602 Drupal.Ajax.prototype.execute = function () {
Chris@0 603 // Do not perform another ajax command if one is already in progress.
Chris@0 604 if (this.ajaxing) {
Chris@0 605 return;
Chris@0 606 }
Chris@0 607
Chris@0 608 try {
Chris@0 609 this.beforeSerialize(this.element, this.options);
Chris@0 610 // Return the jqXHR so that external code can hook into the Deferred API.
Chris@0 611 return $.ajax(this.options);
Chris@0 612 }
Chris@0 613 catch (e) {
Chris@0 614 // Unset the ajax.ajaxing flag here because it won't be unset during
Chris@0 615 // the complete response.
Chris@0 616 this.ajaxing = false;
Chris@0 617 window.alert(`An error occurred while attempting to process ${this.options.url}: ${e.message}`);
Chris@0 618 // For consistency, return a rejected Deferred (i.e., jqXHR's superclass)
Chris@0 619 // so that calling code can take appropriate action.
Chris@0 620 return $.Deferred().reject();
Chris@0 621 }
Chris@0 622 };
Chris@0 623
Chris@0 624 /**
Chris@0 625 * Handle a key press.
Chris@0 626 *
Chris@0 627 * The Ajax object will, if instructed, bind to a key press response. This
Chris@0 628 * will test to see if the key press is valid to trigger this event and
Chris@0 629 * if it is, trigger it for us and prevent other keypresses from triggering.
Chris@0 630 * In this case we're handling RETURN and SPACEBAR keypresses (event codes 13
Chris@0 631 * and 32. RETURN is often used to submit a form when in a textfield, and
Chris@0 632 * SPACE is often used to activate an element without submitting.
Chris@0 633 *
Chris@0 634 * @param {HTMLElement} element
Chris@0 635 * Element the event was triggered on.
Chris@0 636 * @param {jQuery.Event} event
Chris@0 637 * Triggered event.
Chris@0 638 */
Chris@0 639 Drupal.Ajax.prototype.keypressResponse = function (element, event) {
Chris@0 640 // Create a synonym for this to reduce code confusion.
Chris@0 641 const ajax = this;
Chris@0 642
Chris@0 643 // Detect enter key and space bar and allow the standard response for them,
Chris@0 644 // except for form elements of type 'text', 'tel', 'number' and 'textarea',
Chris@0 645 // where the spacebar activation causes inappropriate activation if
Chris@0 646 // #ajax['keypress'] is TRUE. On a text-type widget a space should always
Chris@0 647 // be a space.
Chris@0 648 if (event.which === 13 || (event.which === 32 && element.type !== 'text' &&
Chris@0 649 element.type !== 'textarea' && element.type !== 'tel' && element.type !== 'number')) {
Chris@0 650 event.preventDefault();
Chris@0 651 event.stopPropagation();
Chris@0 652 $(element).trigger(ajax.elementSettings.event);
Chris@0 653 }
Chris@0 654 };
Chris@0 655
Chris@0 656 /**
Chris@0 657 * Handle an event that triggers an Ajax response.
Chris@0 658 *
Chris@0 659 * When an event that triggers an Ajax response happens, this method will
Chris@0 660 * perform the actual Ajax call. It is bound to the event using
Chris@0 661 * bind() in the constructor, and it uses the options specified on the
Chris@0 662 * Ajax object.
Chris@0 663 *
Chris@0 664 * @param {HTMLElement} element
Chris@0 665 * Element the event was triggered on.
Chris@0 666 * @param {jQuery.Event} event
Chris@0 667 * Triggered event.
Chris@0 668 */
Chris@0 669 Drupal.Ajax.prototype.eventResponse = function (element, event) {
Chris@0 670 event.preventDefault();
Chris@0 671 event.stopPropagation();
Chris@0 672
Chris@0 673 // Create a synonym for this to reduce code confusion.
Chris@0 674 const ajax = this;
Chris@0 675
Chris@0 676 // Do not perform another Ajax command if one is already in progress.
Chris@0 677 if (ajax.ajaxing) {
Chris@0 678 return;
Chris@0 679 }
Chris@0 680
Chris@0 681 try {
Chris@0 682 if (ajax.$form) {
Chris@0 683 // If setClick is set, we must set this to ensure that the button's
Chris@0 684 // value is passed.
Chris@0 685 if (ajax.setClick) {
Chris@0 686 // Mark the clicked button. 'form.clk' is a special variable for
Chris@0 687 // ajaxSubmit that tells the system which element got clicked to
Chris@0 688 // trigger the submit. Without it there would be no 'op' or
Chris@0 689 // equivalent.
Chris@0 690 element.form.clk = element;
Chris@0 691 }
Chris@0 692
Chris@0 693 ajax.$form.ajaxSubmit(ajax.options);
Chris@0 694 }
Chris@0 695 else {
Chris@0 696 ajax.beforeSerialize(ajax.element, ajax.options);
Chris@0 697 $.ajax(ajax.options);
Chris@0 698 }
Chris@0 699 }
Chris@0 700 catch (e) {
Chris@0 701 // Unset the ajax.ajaxing flag here because it won't be unset during
Chris@0 702 // the complete response.
Chris@0 703 ajax.ajaxing = false;
Chris@0 704 window.alert(`An error occurred while attempting to process ${ajax.options.url}: ${e.message}`);
Chris@0 705 }
Chris@0 706 };
Chris@0 707
Chris@0 708 /**
Chris@0 709 * Handler for the form serialization.
Chris@0 710 *
Chris@0 711 * Runs before the beforeSend() handler (see below), and unlike that one, runs
Chris@0 712 * before field data is collected.
Chris@0 713 *
Chris@0 714 * @param {object} [element]
Chris@0 715 * Ajax object's `elementSettings`.
Chris@0 716 * @param {object} options
Chris@0 717 * jQuery.ajax options.
Chris@0 718 */
Chris@0 719 Drupal.Ajax.prototype.beforeSerialize = function (element, options) {
Chris@0 720 // Allow detaching behaviors to update field values before collecting them.
Chris@0 721 // This is only needed when field values are added to the POST data, so only
Chris@0 722 // when there is a form such that this.$form.ajaxSubmit() is used instead of
Chris@0 723 // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize()
Chris@0 724 // isn't called, but don't rely on that: explicitly check this.$form.
Chris@0 725 if (this.$form) {
Chris@0 726 const settings = this.settings || drupalSettings;
Chris@0 727 Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize');
Chris@0 728 }
Chris@0 729
Chris@0 730 // Inform Drupal that this is an AJAX request.
Chris@0 731 options.data[Drupal.Ajax.AJAX_REQUEST_PARAMETER] = 1;
Chris@0 732
Chris@0 733 // Allow Drupal to return new JavaScript and CSS files to load without
Chris@0 734 // returning the ones already loaded.
Chris@0 735 // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
Chris@0 736 // @see \Drupal\Core\Asset\LibraryDependencyResolverInterface::getMinimalRepresentativeSubset()
Chris@0 737 // @see system_js_settings_alter()
Chris@0 738 const pageState = drupalSettings.ajaxPageState;
Chris@0 739 options.data['ajax_page_state[theme]'] = pageState.theme;
Chris@0 740 options.data['ajax_page_state[theme_token]'] = pageState.theme_token;
Chris@0 741 options.data['ajax_page_state[libraries]'] = pageState.libraries;
Chris@0 742 };
Chris@0 743
Chris@0 744 /**
Chris@0 745 * Modify form values prior to form submission.
Chris@0 746 *
Chris@0 747 * @param {Array.<object>} formValues
Chris@0 748 * Processed form values.
Chris@0 749 * @param {jQuery} element
Chris@0 750 * The form node as a jQuery object.
Chris@0 751 * @param {object} options
Chris@0 752 * jQuery.ajax options.
Chris@0 753 */
Chris@0 754 Drupal.Ajax.prototype.beforeSubmit = function (formValues, element, options) {
Chris@0 755 // This function is left empty to make it simple to override for modules
Chris@0 756 // that wish to add functionality here.
Chris@0 757 };
Chris@0 758
Chris@0 759 /**
Chris@0 760 * Prepare the Ajax request before it is sent.
Chris@0 761 *
Chris@0 762 * @param {XMLHttpRequest} xmlhttprequest
Chris@0 763 * Native Ajax object.
Chris@0 764 * @param {object} options
Chris@0 765 * jQuery.ajax options.
Chris@0 766 */
Chris@0 767 Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) {
Chris@0 768 // For forms without file inputs, the jQuery Form plugin serializes the
Chris@0 769 // form values, and then calls jQuery's $.ajax() function, which invokes
Chris@0 770 // this handler. In this circumstance, options.extraData is never used. For
Chris@0 771 // forms with file inputs, the jQuery Form plugin uses the browser's normal
Chris@0 772 // form submission mechanism, but captures the response in a hidden IFRAME.
Chris@0 773 // In this circumstance, it calls this handler first, and then appends
Chris@0 774 // hidden fields to the form to submit the values in options.extraData.
Chris@0 775 // There is no simple way to know which submission mechanism will be used,
Chris@0 776 // so we add to extraData regardless, and allow it to be ignored in the
Chris@0 777 // former case.
Chris@0 778 if (this.$form) {
Chris@0 779 options.extraData = options.extraData || {};
Chris@0 780
Chris@0 781 // Let the server know when the IFRAME submission mechanism is used. The
Chris@0 782 // server can use this information to wrap the JSON response in a
Chris@0 783 // TEXTAREA, as per http://jquery.malsup.com/form/#file-upload.
Chris@0 784 options.extraData.ajax_iframe_upload = '1';
Chris@0 785
Chris@0 786 // The triggering element is about to be disabled (see below), but if it
Chris@0 787 // contains a value (e.g., a checkbox, textfield, select, etc.), ensure
Chris@0 788 // that value is included in the submission. As per above, submissions
Chris@0 789 // that use $.ajax() are already serialized prior to the element being
Chris@0 790 // disabled, so this is only needed for IFRAME submissions.
Chris@0 791 const v = $.fieldValue(this.element);
Chris@0 792 if (v !== null) {
Chris@0 793 options.extraData[this.element.name] = v;
Chris@0 794 }
Chris@0 795 }
Chris@0 796
Chris@0 797 // Disable the element that received the change to prevent user interface
Chris@0 798 // interaction while the Ajax request is in progress. ajax.ajaxing prevents
Chris@0 799 // the element from triggering a new request, but does not prevent the user
Chris@0 800 // from changing its value.
Chris@0 801 $(this.element).prop('disabled', true);
Chris@0 802
Chris@0 803 if (!this.progress || !this.progress.type) {
Chris@0 804 return;
Chris@0 805 }
Chris@0 806
Chris@0 807 // Insert progress indicator.
Chris@0 808 const progressIndicatorMethod = `setProgressIndicator${this.progress.type.slice(0, 1).toUpperCase()}${this.progress.type.slice(1).toLowerCase()}`;
Chris@0 809 if (progressIndicatorMethod in this && typeof this[progressIndicatorMethod] === 'function') {
Chris@0 810 this[progressIndicatorMethod].call(this);
Chris@0 811 }
Chris@0 812 };
Chris@0 813
Chris@0 814 /**
Chris@0 815 * Sets the progress bar progress indicator.
Chris@0 816 */
Chris@0 817 Drupal.Ajax.prototype.setProgressIndicatorBar = function () {
Chris@0 818 const progressBar = new Drupal.ProgressBar(`ajax-progress-${this.element.id}`, $.noop, this.progress.method, $.noop);
Chris@0 819 if (this.progress.message) {
Chris@0 820 progressBar.setProgress(-1, this.progress.message);
Chris@0 821 }
Chris@0 822 if (this.progress.url) {
Chris@0 823 progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500);
Chris@0 824 }
Chris@0 825 this.progress.element = $(progressBar.element).addClass('ajax-progress ajax-progress-bar');
Chris@0 826 this.progress.object = progressBar;
Chris@0 827 $(this.element).after(this.progress.element);
Chris@0 828 };
Chris@0 829
Chris@0 830 /**
Chris@0 831 * Sets the throbber progress indicator.
Chris@0 832 */
Chris@0 833 Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
Chris@0 834 this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
Chris@0 835 if (this.progress.message) {
Chris@0 836 this.progress.element.find('.throbber').after(`<div class="message">${this.progress.message}</div>`);
Chris@0 837 }
Chris@0 838 $(this.element).after(this.progress.element);
Chris@0 839 };
Chris@0 840
Chris@0 841 /**
Chris@0 842 * Sets the fullscreen progress indicator.
Chris@0 843 */
Chris@0 844 Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () {
Chris@0 845 this.progress.element = $('<div class="ajax-progress ajax-progress-fullscreen">&nbsp;</div>');
Chris@0 846 $('body').after(this.progress.element);
Chris@0 847 };
Chris@0 848
Chris@0 849 /**
Chris@0 850 * Handler for the form redirection completion.
Chris@0 851 *
Chris@0 852 * @param {Array.<Drupal.AjaxCommands~commandDefinition>} response
Chris@0 853 * Drupal Ajax response.
Chris@0 854 * @param {number} status
Chris@0 855 * XMLHttpRequest status.
Chris@0 856 */
Chris@0 857 Drupal.Ajax.prototype.success = function (response, status) {
Chris@0 858 // Remove the progress element.
Chris@0 859 if (this.progress.element) {
Chris@0 860 $(this.progress.element).remove();
Chris@0 861 }
Chris@0 862 if (this.progress.object) {
Chris@0 863 this.progress.object.stopMonitoring();
Chris@0 864 }
Chris@0 865 $(this.element).prop('disabled', false);
Chris@0 866
Chris@0 867 // Save element's ancestors tree so if the element is removed from the dom
Chris@0 868 // we can try to refocus one of its parents. Using addBack reverse the
Chris@0 869 // result array, meaning that index 0 is the highest parent in the hierarchy
Chris@0 870 // in this situation it is usually a <form> element.
Chris@0 871 const elementParents = $(this.element).parents('[data-drupal-selector]').addBack().toArray();
Chris@0 872
Chris@0 873 // Track if any command is altering the focus so we can avoid changing the
Chris@0 874 // focus set by the Ajax command.
Chris@0 875 let focusChanged = false;
Chris@0 876 Object.keys(response || {}).forEach((i) => {
Chris@0 877 if (response[i].command && this.commands[response[i].command]) {
Chris@0 878 this.commands[response[i].command](this, response[i], status);
Chris@0 879 if (response[i].command === 'invoke' && response[i].method === 'focus') {
Chris@0 880 focusChanged = true;
Chris@0 881 }
Chris@0 882 }
Chris@0 883 });
Chris@0 884
Chris@0 885 // If the focus hasn't be changed by the ajax commands, try to refocus the
Chris@0 886 // triggering element or one of its parents if that element does not exist
Chris@0 887 // anymore.
Chris@0 888 if (!focusChanged && this.element && !$(this.element).data('disable-refocus')) {
Chris@0 889 let target = false;
Chris@0 890
Chris@0 891 for (let n = elementParents.length - 1; !target && n > 0; n--) {
Chris@0 892 target = document.querySelector(`[data-drupal-selector="${elementParents[n].getAttribute('data-drupal-selector')}"]`);
Chris@0 893 }
Chris@0 894
Chris@0 895 if (target) {
Chris@0 896 $(target).trigger('focus');
Chris@0 897 }
Chris@0 898 }
Chris@0 899
Chris@0 900 // Reattach behaviors, if they were detached in beforeSerialize(). The
Chris@0 901 // attachBehaviors() called on the new content from processing the response
Chris@0 902 // commands is not sufficient, because behaviors from the entire form need
Chris@0 903 // to be reattached.
Chris@0 904 if (this.$form) {
Chris@0 905 const settings = this.settings || drupalSettings;
Chris@0 906 Drupal.attachBehaviors(this.$form.get(0), settings);
Chris@0 907 }
Chris@0 908
Chris@0 909 // Remove any response-specific settings so they don't get used on the next
Chris@0 910 // call by mistake.
Chris@0 911 this.settings = null;
Chris@0 912 };
Chris@0 913
Chris@0 914 /**
Chris@0 915 * Build an effect object to apply an effect when adding new HTML.
Chris@0 916 *
Chris@0 917 * @param {object} response
Chris@0 918 * Drupal Ajax response.
Chris@0 919 * @param {string} [response.effect]
Chris@0 920 * Override the default value of {@link Drupal.Ajax#elementSettings}.
Chris@0 921 * @param {string|number} [response.speed]
Chris@0 922 * Override the default value of {@link Drupal.Ajax#elementSettings}.
Chris@0 923 *
Chris@0 924 * @return {object}
Chris@0 925 * Returns an object with `showEffect`, `hideEffect` and `showSpeed`
Chris@0 926 * properties.
Chris@0 927 */
Chris@0 928 Drupal.Ajax.prototype.getEffect = function (response) {
Chris@0 929 const type = response.effect || this.effect;
Chris@0 930 const speed = response.speed || this.speed;
Chris@0 931
Chris@0 932 const effect = {};
Chris@0 933 if (type === 'none') {
Chris@0 934 effect.showEffect = 'show';
Chris@0 935 effect.hideEffect = 'hide';
Chris@0 936 effect.showSpeed = '';
Chris@0 937 }
Chris@0 938 else if (type === 'fade') {
Chris@0 939 effect.showEffect = 'fadeIn';
Chris@0 940 effect.hideEffect = 'fadeOut';
Chris@0 941 effect.showSpeed = speed;
Chris@0 942 }
Chris@0 943 else {
Chris@0 944 effect.showEffect = `${type}Toggle`;
Chris@0 945 effect.hideEffect = `${type}Toggle`;
Chris@0 946 effect.showSpeed = speed;
Chris@0 947 }
Chris@0 948
Chris@0 949 return effect;
Chris@0 950 };
Chris@0 951
Chris@0 952 /**
Chris@0 953 * Handler for the form redirection error.
Chris@0 954 *
Chris@0 955 * @param {object} xmlhttprequest
Chris@0 956 * Native XMLHttpRequest object.
Chris@0 957 * @param {string} uri
Chris@0 958 * Ajax Request URI.
Chris@0 959 * @param {string} [customMessage]
Chris@0 960 * Extra message to print with the Ajax error.
Chris@0 961 */
Chris@0 962 Drupal.Ajax.prototype.error = function (xmlhttprequest, uri, customMessage) {
Chris@0 963 // Remove the progress element.
Chris@0 964 if (this.progress.element) {
Chris@0 965 $(this.progress.element).remove();
Chris@0 966 }
Chris@0 967 if (this.progress.object) {
Chris@0 968 this.progress.object.stopMonitoring();
Chris@0 969 }
Chris@0 970 // Undo hide.
Chris@0 971 $(this.wrapper).show();
Chris@0 972 // Re-enable the element.
Chris@0 973 $(this.element).prop('disabled', false);
Chris@0 974 // Reattach behaviors, if they were detached in beforeSerialize().
Chris@0 975 if (this.$form) {
Chris@0 976 const settings = this.settings || drupalSettings;
Chris@0 977 Drupal.attachBehaviors(this.$form.get(0), settings);
Chris@0 978 }
Chris@0 979 throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage);
Chris@0 980 };
Chris@0 981
Chris@0 982 /**
Chris@0 983 * @typedef {object} Drupal.AjaxCommands~commandDefinition
Chris@0 984 *
Chris@0 985 * @prop {string} command
Chris@0 986 * @prop {string} [method]
Chris@0 987 * @prop {string} [selector]
Chris@0 988 * @prop {string} [data]
Chris@0 989 * @prop {object} [settings]
Chris@0 990 * @prop {bool} [asterisk]
Chris@0 991 * @prop {string} [text]
Chris@0 992 * @prop {string} [title]
Chris@0 993 * @prop {string} [url]
Chris@0 994 * @prop {object} [argument]
Chris@0 995 * @prop {string} [name]
Chris@0 996 * @prop {string} [value]
Chris@0 997 * @prop {string} [old]
Chris@0 998 * @prop {string} [new]
Chris@0 999 * @prop {bool} [merge]
Chris@0 1000 * @prop {Array} [args]
Chris@0 1001 *
Chris@0 1002 * @see Drupal.AjaxCommands
Chris@0 1003 */
Chris@0 1004
Chris@0 1005 /**
Chris@0 1006 * Provide a series of commands that the client will perform.
Chris@0 1007 *
Chris@0 1008 * @constructor
Chris@0 1009 */
Chris@0 1010 Drupal.AjaxCommands = function () {};
Chris@0 1011 Drupal.AjaxCommands.prototype = {
Chris@0 1012
Chris@0 1013 /**
Chris@0 1014 * Command to insert new content into the DOM.
Chris@0 1015 *
Chris@0 1016 * @param {Drupal.Ajax} ajax
Chris@0 1017 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1018 * @param {object} response
Chris@0 1019 * The response from the Ajax request.
Chris@0 1020 * @param {string} response.data
Chris@0 1021 * The data to use with the jQuery method.
Chris@0 1022 * @param {string} [response.method]
Chris@0 1023 * The jQuery DOM manipulation method to be used.
Chris@0 1024 * @param {string} [response.selector]
Chris@0 1025 * A optional jQuery selector string.
Chris@0 1026 * @param {object} [response.settings]
Chris@0 1027 * An optional array of settings that will be used.
Chris@0 1028 * @param {number} [status]
Chris@0 1029 * The XMLHttpRequest status.
Chris@0 1030 */
Chris@0 1031 insert(ajax, response, status) {
Chris@0 1032 // Get information from the response. If it is not there, default to
Chris@0 1033 // our presets.
Chris@0 1034 const $wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
Chris@0 1035 const method = response.method || ajax.method;
Chris@0 1036 const effect = ajax.getEffect(response);
Chris@0 1037 let settings;
Chris@0 1038
Chris@0 1039 // We don't know what response.data contains: it might be a string of text
Chris@0 1040 // without HTML, so don't rely on jQuery correctly interpreting
Chris@0 1041 // $(response.data) as new HTML rather than a CSS selector. Also, if
Chris@0 1042 // response.data contains top-level text nodes, they get lost with either
Chris@0 1043 // $(response.data) or $('<div></div>').replaceWith(response.data).
Chris@0 1044 const $newContentWrapped = $('<div></div>').html(response.data);
Chris@0 1045 let $newContent = $newContentWrapped.contents();
Chris@0 1046
Chris@0 1047 // For legacy reasons, the effects processing code assumes that
Chris@0 1048 // $newContent consists of a single top-level element. Also, it has not
Chris@0 1049 // been sufficiently tested whether attachBehaviors() can be successfully
Chris@0 1050 // called with a context object that includes top-level text nodes.
Chris@0 1051 // However, to give developers full control of the HTML appearing in the
Chris@0 1052 // page, and to enable Ajax content to be inserted in places where <div>
Chris@0 1053 // elements are not allowed (e.g., within <table>, <tr>, and <span>
Chris@0 1054 // parents), we check if the new content satisfies the requirement
Chris@0 1055 // of a single top-level element, and only use the container <div> created
Chris@0 1056 // above when it doesn't. For more information, please see
Chris@0 1057 // https://www.drupal.org/node/736066.
Chris@0 1058 if ($newContent.length !== 1 || $newContent.get(0).nodeType !== 1) {
Chris@0 1059 $newContent = $newContentWrapped;
Chris@0 1060 }
Chris@0 1061
Chris@0 1062 // If removing content from the wrapper, detach behaviors first.
Chris@0 1063 switch (method) {
Chris@0 1064 case 'html':
Chris@0 1065 case 'replaceWith':
Chris@0 1066 case 'replaceAll':
Chris@0 1067 case 'empty':
Chris@0 1068 case 'remove':
Chris@0 1069 settings = response.settings || ajax.settings || drupalSettings;
Chris@0 1070 Drupal.detachBehaviors($wrapper.get(0), settings);
Chris@0 1071 }
Chris@0 1072
Chris@0 1073 // Add the new content to the page.
Chris@0 1074 $wrapper[method]($newContent);
Chris@0 1075
Chris@0 1076 // Immediately hide the new content if we're using any effects.
Chris@0 1077 if (effect.showEffect !== 'show') {
Chris@0 1078 $newContent.hide();
Chris@0 1079 }
Chris@0 1080
Chris@0 1081 // Determine which effect to use and what content will receive the
Chris@0 1082 // effect, then show the new content.
Chris@0 1083 if ($newContent.find('.ajax-new-content').length > 0) {
Chris@0 1084 $newContent.find('.ajax-new-content').hide();
Chris@0 1085 $newContent.show();
Chris@0 1086 $newContent.find('.ajax-new-content')[effect.showEffect](effect.showSpeed);
Chris@0 1087 }
Chris@0 1088 else if (effect.showEffect !== 'show') {
Chris@0 1089 $newContent[effect.showEffect](effect.showSpeed);
Chris@0 1090 }
Chris@0 1091
Chris@0 1092 // Attach all JavaScript behaviors to the new content, if it was
Chris@0 1093 // successfully added to the page, this if statement allows
Chris@0 1094 // `#ajax['wrapper']` to be optional.
Chris@0 1095 if ($newContent.parents('html').length > 0) {
Chris@0 1096 // Apply any settings from the returned JSON if available.
Chris@0 1097 settings = response.settings || ajax.settings || drupalSettings;
Chris@0 1098 Drupal.attachBehaviors($newContent.get(0), settings);
Chris@0 1099 }
Chris@0 1100 },
Chris@0 1101
Chris@0 1102 /**
Chris@0 1103 * Command to remove a chunk from the page.
Chris@0 1104 *
Chris@0 1105 * @param {Drupal.Ajax} [ajax]
Chris@0 1106 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1107 * @param {object} response
Chris@0 1108 * The response from the Ajax request.
Chris@0 1109 * @param {string} response.selector
Chris@0 1110 * A jQuery selector string.
Chris@0 1111 * @param {object} [response.settings]
Chris@0 1112 * An optional array of settings that will be used.
Chris@0 1113 * @param {number} [status]
Chris@0 1114 * The XMLHttpRequest status.
Chris@0 1115 */
Chris@0 1116 remove(ajax, response, status) {
Chris@0 1117 const settings = response.settings || ajax.settings || drupalSettings;
Chris@0 1118 $(response.selector).each(function () {
Chris@0 1119 Drupal.detachBehaviors(this, settings);
Chris@0 1120 })
Chris@0 1121 .remove();
Chris@0 1122 },
Chris@0 1123
Chris@0 1124 /**
Chris@0 1125 * Command to mark a chunk changed.
Chris@0 1126 *
Chris@0 1127 * @param {Drupal.Ajax} [ajax]
Chris@0 1128 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1129 * @param {object} response
Chris@0 1130 * The JSON response object from the Ajax request.
Chris@0 1131 * @param {string} response.selector
Chris@0 1132 * A jQuery selector string.
Chris@0 1133 * @param {bool} [response.asterisk]
Chris@0 1134 * An optional CSS selector. If specified, an asterisk will be
Chris@0 1135 * appended to the HTML inside the provided selector.
Chris@0 1136 * @param {number} [status]
Chris@0 1137 * The request status.
Chris@0 1138 */
Chris@0 1139 changed(ajax, response, status) {
Chris@0 1140 const $element = $(response.selector);
Chris@0 1141 if (!$element.hasClass('ajax-changed')) {
Chris@0 1142 $element.addClass('ajax-changed');
Chris@0 1143 if (response.asterisk) {
Chris@0 1144 $element.find(response.asterisk).append(` <abbr class="ajax-changed" title="${Drupal.t('Changed')}">*</abbr> `);
Chris@0 1145 }
Chris@0 1146 }
Chris@0 1147 },
Chris@0 1148
Chris@0 1149 /**
Chris@0 1150 * Command to provide an alert.
Chris@0 1151 *
Chris@0 1152 * @param {Drupal.Ajax} [ajax]
Chris@0 1153 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1154 * @param {object} response
Chris@0 1155 * The JSON response from the Ajax request.
Chris@0 1156 * @param {string} response.text
Chris@0 1157 * The text that will be displayed in an alert dialog.
Chris@0 1158 * @param {number} [status]
Chris@0 1159 * The XMLHttpRequest status.
Chris@0 1160 */
Chris@0 1161 alert(ajax, response, status) {
Chris@0 1162 window.alert(response.text, response.title);
Chris@0 1163 },
Chris@0 1164
Chris@0 1165 /**
Chris@0 1166 * Command to set the window.location, redirecting the browser.
Chris@0 1167 *
Chris@0 1168 * @param {Drupal.Ajax} [ajax]
Chris@0 1169 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1170 * @param {object} response
Chris@0 1171 * The response from the Ajax request.
Chris@0 1172 * @param {string} response.url
Chris@0 1173 * The URL to redirect to.
Chris@0 1174 * @param {number} [status]
Chris@0 1175 * The XMLHttpRequest status.
Chris@0 1176 */
Chris@0 1177 redirect(ajax, response, status) {
Chris@0 1178 window.location = response.url;
Chris@0 1179 },
Chris@0 1180
Chris@0 1181 /**
Chris@0 1182 * Command to provide the jQuery css() function.
Chris@0 1183 *
Chris@0 1184 * @param {Drupal.Ajax} [ajax]
Chris@0 1185 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1186 * @param {object} response
Chris@0 1187 * The response from the Ajax request.
Chris@0 1188 * @param {string} response.selector
Chris@0 1189 * A jQuery selector string.
Chris@0 1190 * @param {object} response.argument
Chris@0 1191 * An array of key/value pairs to set in the CSS for the selector.
Chris@0 1192 * @param {number} [status]
Chris@0 1193 * The XMLHttpRequest status.
Chris@0 1194 */
Chris@0 1195 css(ajax, response, status) {
Chris@0 1196 $(response.selector).css(response.argument);
Chris@0 1197 },
Chris@0 1198
Chris@0 1199 /**
Chris@0 1200 * Command to set the settings used for other commands in this response.
Chris@0 1201 *
Chris@0 1202 * This method will also remove expired `drupalSettings.ajax` settings.
Chris@0 1203 *
Chris@0 1204 * @param {Drupal.Ajax} [ajax]
Chris@0 1205 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1206 * @param {object} response
Chris@0 1207 * The response from the Ajax request.
Chris@0 1208 * @param {bool} response.merge
Chris@0 1209 * Determines whether the additional settings should be merged to the
Chris@0 1210 * global settings.
Chris@0 1211 * @param {object} response.settings
Chris@0 1212 * Contains additional settings to add to the global settings.
Chris@0 1213 * @param {number} [status]
Chris@0 1214 * The XMLHttpRequest status.
Chris@0 1215 */
Chris@0 1216 settings(ajax, response, status) {
Chris@0 1217 const ajaxSettings = drupalSettings.ajax;
Chris@0 1218
Chris@0 1219 // Clean up drupalSettings.ajax.
Chris@0 1220 if (ajaxSettings) {
Chris@0 1221 Drupal.ajax.expired().forEach((instance) => {
Chris@0 1222 // If the Ajax object has been created through drupalSettings.ajax
Chris@0 1223 // it will have a selector. When there is no selector the object
Chris@0 1224 // has been initialized with a special class name picked up by the
Chris@0 1225 // Ajax behavior.
Chris@0 1226
Chris@0 1227 if (instance.selector) {
Chris@0 1228 const selector = instance.selector.replace('#', '');
Chris@0 1229 if (selector in ajaxSettings) {
Chris@0 1230 delete ajaxSettings[selector];
Chris@0 1231 }
Chris@0 1232 }
Chris@0 1233 });
Chris@0 1234 }
Chris@0 1235
Chris@0 1236 if (response.merge) {
Chris@0 1237 $.extend(true, drupalSettings, response.settings);
Chris@0 1238 }
Chris@0 1239 else {
Chris@0 1240 ajax.settings = response.settings;
Chris@0 1241 }
Chris@0 1242 },
Chris@0 1243
Chris@0 1244 /**
Chris@0 1245 * Command to attach data using jQuery's data API.
Chris@0 1246 *
Chris@0 1247 * @param {Drupal.Ajax} [ajax]
Chris@0 1248 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1249 * @param {object} response
Chris@0 1250 * The response from the Ajax request.
Chris@0 1251 * @param {string} response.name
Chris@0 1252 * The name or key (in the key value pair) of the data attached to this
Chris@0 1253 * selector.
Chris@0 1254 * @param {string} response.selector
Chris@0 1255 * A jQuery selector string.
Chris@0 1256 * @param {string|object} response.value
Chris@0 1257 * The value of to be attached.
Chris@0 1258 * @param {number} [status]
Chris@0 1259 * The XMLHttpRequest status.
Chris@0 1260 */
Chris@0 1261 data(ajax, response, status) {
Chris@0 1262 $(response.selector).data(response.name, response.value);
Chris@0 1263 },
Chris@0 1264
Chris@0 1265 /**
Chris@0 1266 * Command to apply a jQuery method.
Chris@0 1267 *
Chris@0 1268 * @param {Drupal.Ajax} [ajax]
Chris@0 1269 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1270 * @param {object} response
Chris@0 1271 * The response from the Ajax request.
Chris@0 1272 * @param {Array} response.args
Chris@0 1273 * An array of arguments to the jQuery method, if any.
Chris@0 1274 * @param {string} response.method
Chris@0 1275 * The jQuery method to invoke.
Chris@0 1276 * @param {string} response.selector
Chris@0 1277 * A jQuery selector string.
Chris@0 1278 * @param {number} [status]
Chris@0 1279 * The XMLHttpRequest status.
Chris@0 1280 */
Chris@0 1281 invoke(ajax, response, status) {
Chris@0 1282 const $element = $(response.selector);
Chris@0 1283 $element[response.method](...response.args);
Chris@0 1284 },
Chris@0 1285
Chris@0 1286 /**
Chris@0 1287 * Command to restripe a table.
Chris@0 1288 *
Chris@0 1289 * @param {Drupal.Ajax} [ajax]
Chris@0 1290 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1291 * @param {object} response
Chris@0 1292 * The response from the Ajax request.
Chris@0 1293 * @param {string} response.selector
Chris@0 1294 * A jQuery selector string.
Chris@0 1295 * @param {number} [status]
Chris@0 1296 * The XMLHttpRequest status.
Chris@0 1297 */
Chris@0 1298 restripe(ajax, response, status) {
Chris@0 1299 // :even and :odd are reversed because jQuery counts from 0 and
Chris@0 1300 // we count from 1, so we're out of sync.
Chris@0 1301 // Match immediate children of the parent element to allow nesting.
Chris@0 1302 $(response.selector)
Chris@0 1303 .find('> tbody > tr:visible, > tr:visible')
Chris@0 1304 .removeClass('odd even')
Chris@0 1305 .filter(':even')
Chris@0 1306 .addClass('odd')
Chris@0 1307 .end()
Chris@0 1308 .filter(':odd')
Chris@0 1309 .addClass('even');
Chris@0 1310 },
Chris@0 1311
Chris@0 1312 /**
Chris@0 1313 * Command to update a form's build ID.
Chris@0 1314 *
Chris@0 1315 * @param {Drupal.Ajax} [ajax]
Chris@0 1316 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1317 * @param {object} response
Chris@0 1318 * The response from the Ajax request.
Chris@0 1319 * @param {string} response.old
Chris@0 1320 * The old form build ID.
Chris@0 1321 * @param {string} response.new
Chris@0 1322 * The new form build ID.
Chris@0 1323 * @param {number} [status]
Chris@0 1324 * The XMLHttpRequest status.
Chris@0 1325 */
Chris@0 1326 update_build_id(ajax, response, status) {
Chris@0 1327 $(`input[name="form_build_id"][value="${response.old}"]`).val(response.new);
Chris@0 1328 },
Chris@0 1329
Chris@0 1330 /**
Chris@0 1331 * Command to add css.
Chris@0 1332 *
Chris@0 1333 * Uses the proprietary addImport method if available as browsers which
Chris@0 1334 * support that method ignore @import statements in dynamically added
Chris@0 1335 * stylesheets.
Chris@0 1336 *
Chris@0 1337 * @param {Drupal.Ajax} [ajax]
Chris@0 1338 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
Chris@0 1339 * @param {object} response
Chris@0 1340 * The response from the Ajax request.
Chris@0 1341 * @param {string} response.data
Chris@0 1342 * A string that contains the styles to be added.
Chris@0 1343 * @param {number} [status]
Chris@0 1344 * The XMLHttpRequest status.
Chris@0 1345 */
Chris@0 1346 add_css(ajax, response, status) {
Chris@0 1347 // Add the styles in the normal way.
Chris@0 1348 $('head').prepend(response.data);
Chris@0 1349 // Add imports in the styles using the addImport method if available.
Chris@0 1350 let match;
Chris@0 1351 const importMatch = /^@import url\("(.*)"\);$/igm;
Chris@0 1352 if (document.styleSheets[0].addImport && importMatch.test(response.data)) {
Chris@0 1353 importMatch.lastIndex = 0;
Chris@0 1354 do {
Chris@0 1355 match = importMatch.exec(response.data);
Chris@0 1356 document.styleSheets[0].addImport(match[1]);
Chris@0 1357 } while (match);
Chris@0 1358 }
Chris@0 1359 },
Chris@0 1360 };
Chris@0 1361 }(jQuery, window, Drupal, drupalSettings));