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