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