Chris@0: /** Chris@0: * @file Chris@0: * Provides Ajax page updating via jQuery $.ajax. Chris@0: * Chris@0: * Ajax is a method of making a request via JavaScript while viewing an HTML Chris@0: * page. The request returns an array of commands encoded in JSON, which is Chris@0: * then executed to make any changes that are necessary to the page. Chris@0: * Chris@0: * Drupal uses this file to enhance form elements with `#ajax['url']` and Chris@0: * `#ajax['wrapper']` properties. If set, this file will automatically be Chris@0: * included to provide Ajax capabilities. Chris@0: */ Chris@0: Chris@17: (function($, window, Drupal, drupalSettings) { Chris@0: /** Chris@0: * Attaches the Ajax behavior to each Ajax form element. Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Initialize all {@link Drupal.Ajax} objects declared in Chris@0: * `drupalSettings.ajax` or initialize {@link Drupal.Ajax} objects from Chris@0: * DOM elements having the `use-ajax-submit` or `use-ajax` css class. Chris@0: * @prop {Drupal~behaviorDetach} detach Chris@0: * During `unload` remove all {@link Drupal.Ajax} objects related to Chris@0: * the removed content. Chris@0: */ Chris@0: Drupal.behaviors.AJAX = { Chris@0: attach(context, settings) { Chris@0: function loadAjaxBehavior(base) { Chris@14: const elementSettings = settings.ajax[base]; Chris@14: if (typeof elementSettings.selector === 'undefined') { Chris@14: elementSettings.selector = `#${base}`; Chris@0: } Chris@17: $(elementSettings.selector) Chris@17: .once('drupal-ajax') Chris@17: .each(function() { Chris@17: elementSettings.element = this; Chris@17: elementSettings.base = base; Chris@17: Drupal.ajax(elementSettings); Chris@17: }); Chris@0: } Chris@0: Chris@0: // Load all Ajax behaviors specified in the settings. Chris@14: Object.keys(settings.ajax || {}).forEach(base => loadAjaxBehavior(base)); Chris@0: Chris@14: Drupal.ajax.bindAjaxLinks(document.body); Chris@0: Chris@0: // This class means to submit the form to the action using Ajax. Chris@17: $('.use-ajax-submit') Chris@17: .once('ajax') Chris@17: .each(function() { Chris@17: const elementSettings = {}; Chris@0: Chris@17: // Ajax submits specified in this manner automatically submit to the Chris@17: // normal form action. Chris@17: elementSettings.url = $(this.form).attr('action'); Chris@17: // Form submit button clicks need to tell the form what was clicked so Chris@17: // it gets passed in the POST request. Chris@17: elementSettings.setClick = true; Chris@17: // Form buttons use the 'click' event rather than mousedown. Chris@17: elementSettings.event = 'click'; Chris@17: // Clicked form buttons look better with the throbber than the progress Chris@17: // bar. Chris@17: elementSettings.progress = { type: 'throbber' }; Chris@17: elementSettings.base = $(this).attr('id'); Chris@17: elementSettings.element = this; Chris@0: Chris@17: Drupal.ajax(elementSettings); Chris@17: }); Chris@0: }, Chris@0: Chris@0: detach(context, settings, trigger) { Chris@0: if (trigger === 'unload') { Chris@17: Drupal.ajax.expired().forEach(instance => { Chris@0: // Set this to null and allow garbage collection to reclaim Chris@0: // the memory. Chris@0: Drupal.ajax.instances[instance.instanceIndex] = null; Chris@0: }); Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Extends Error to provide handling for Errors in Ajax. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @augments Error Chris@0: * Chris@0: * @param {XMLHttpRequest} xmlhttp Chris@0: * XMLHttpRequest object used for the failed request. Chris@0: * @param {string} uri Chris@0: * The URI where the error occurred. Chris@0: * @param {string} customMessage Chris@0: * The custom message. Chris@0: */ Chris@17: Drupal.AjaxError = function(xmlhttp, uri, customMessage) { Chris@0: let statusCode; Chris@0: let statusText; Chris@0: let responseText; Chris@0: if (xmlhttp.status) { Chris@17: statusCode = `\n${Drupal.t('An AJAX HTTP error occurred.')}\n${Drupal.t( Chris@17: 'HTTP Result Code: !status', Chris@17: { '!status': xmlhttp.status }, Chris@17: )}`; Chris@17: } else { Chris@17: statusCode = `\n${Drupal.t( Chris@17: 'An AJAX HTTP request terminated abnormally.', Chris@17: )}`; Chris@0: } Chris@0: statusCode += `\n${Drupal.t('Debugging information follows.')}`; Chris@14: const pathText = `\n${Drupal.t('Path: !uri', { '!uri': uri })}`; Chris@0: statusText = ''; Chris@0: // In some cases, when statusCode === 0, xmlhttp.statusText may not be Chris@0: // defined. Unfortunately, testing for it with typeof, etc, doesn't seem to Chris@0: // catch that and the test causes an exception. So we need to catch the Chris@0: // exception here. Chris@0: try { Chris@17: statusText = `\n${Drupal.t('StatusText: !statusText', { Chris@17: '!statusText': $.trim(xmlhttp.statusText), Chris@17: })}`; Chris@17: } catch (e) { Chris@0: // Empty. Chris@0: } Chris@0: Chris@0: responseText = ''; Chris@0: // Again, we don't have a way to know for sure whether accessing Chris@0: // xmlhttp.responseText is going to throw an exception. So we'll catch it. Chris@0: try { Chris@17: responseText = `\n${Drupal.t('ResponseText: !responseText', { Chris@17: '!responseText': $.trim(xmlhttp.responseText), Chris@17: })}`; Chris@17: } catch (e) { Chris@0: // Empty. Chris@0: } Chris@0: Chris@0: // Make the responseText more readable by stripping HTML tags and newlines. Chris@0: responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi, ''); Chris@0: responseText = responseText.replace(/[\n]+\s+/g, '\n'); Chris@0: Chris@0: // We don't need readyState except for status == 0. Chris@17: const readyStateText = Chris@17: xmlhttp.status === 0 Chris@17: ? `\n${Drupal.t('ReadyState: !readyState', { Chris@17: '!readyState': xmlhttp.readyState, Chris@17: })}` Chris@17: : ''; Chris@0: Chris@17: customMessage = customMessage Chris@17: ? `\n${Drupal.t('CustomMessage: !customMessage', { Chris@17: '!customMessage': customMessage, Chris@17: })}` Chris@17: : ''; Chris@0: Chris@0: /** Chris@0: * Formatted and translated error message. Chris@0: * Chris@0: * @type {string} Chris@0: */ Chris@17: this.message = Chris@17: statusCode + Chris@17: pathText + Chris@17: statusText + Chris@17: customMessage + Chris@17: responseText + Chris@17: readyStateText; Chris@0: Chris@0: /** Chris@0: * Used by some browsers to display a more accurate stack trace. Chris@0: * Chris@0: * @type {string} Chris@0: */ Chris@0: this.name = 'AjaxError'; Chris@0: }; Chris@0: Chris@0: Drupal.AjaxError.prototype = new Error(); Chris@0: Drupal.AjaxError.prototype.constructor = Drupal.AjaxError; Chris@0: Chris@0: /** Chris@0: * Provides Ajax page updating via jQuery $.ajax. Chris@0: * Chris@0: * This function is designed to improve developer experience by wrapping the Chris@0: * initialization of {@link Drupal.Ajax} objects and storing all created Chris@0: * objects in the {@link Drupal.ajax.instances} array. Chris@0: * Chris@0: * @example Chris@0: * Drupal.behaviors.myCustomAJAXStuff = { Chris@0: * attach: function (context, settings) { Chris@0: * Chris@0: * var ajaxSettings = { Chris@0: * url: 'my/url/path', Chris@0: * // If the old version of Drupal.ajax() needs to be used those Chris@0: * // properties can be added Chris@0: * base: 'myBase', Chris@0: * element: $(context).find('.someElement') Chris@0: * }; Chris@0: * Chris@0: * var myAjaxObject = Drupal.ajax(ajaxSettings); Chris@0: * Chris@0: * // Declare a new Ajax command specifically for this Ajax object. Chris@0: * myAjaxObject.commands.insert = function (ajax, response, status) { Chris@0: * $('#my-wrapper').append(response.data); Chris@0: * alert('New content was appended to #my-wrapper'); Chris@0: * }; Chris@0: * Chris@0: * // This command will remove this Ajax object from the page. Chris@0: * myAjaxObject.commands.destroyObject = function (ajax, response, status) { Chris@0: * Drupal.ajax.instances[this.instanceIndex] = null; Chris@0: * }; Chris@0: * Chris@0: * // Programmatically trigger the Ajax request. Chris@0: * myAjaxObject.execute(); Chris@0: * } Chris@0: * }; Chris@0: * Chris@0: * @param {object} settings Chris@0: * The settings object passed to {@link Drupal.Ajax} constructor. Chris@0: * @param {string} [settings.base] Chris@0: * Base is passed to {@link Drupal.Ajax} constructor as the 'base' Chris@0: * parameter. Chris@0: * @param {HTMLElement} [settings.element] Chris@0: * Element parameter of {@link Drupal.Ajax} constructor, element on which Chris@0: * event listeners will be bound. Chris@0: * Chris@0: * @return {Drupal.Ajax} Chris@0: * The created Ajax object. Chris@0: * Chris@0: * @see Drupal.AjaxCommands Chris@0: */ Chris@17: Drupal.ajax = function(settings) { Chris@0: if (arguments.length !== 1) { Chris@17: throw new Error( Chris@17: 'Drupal.ajax() function must be called with one configuration object only', Chris@17: ); Chris@0: } Chris@0: // Map those config keys to variables for the old Drupal.ajax function. Chris@0: const base = settings.base || false; Chris@0: const element = settings.element || false; Chris@0: delete settings.base; Chris@0: delete settings.element; Chris@0: Chris@0: // By default do not display progress for ajax calls without an element. Chris@0: if (!settings.progress && !element) { Chris@0: settings.progress = false; Chris@0: } Chris@0: Chris@0: const ajax = new Drupal.Ajax(base, element, settings); Chris@0: ajax.instanceIndex = Drupal.ajax.instances.length; Chris@0: Drupal.ajax.instances.push(ajax); Chris@0: Chris@0: return ajax; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Contains all created Ajax objects. Chris@0: * Chris@0: * @type {Array.} Chris@0: */ Chris@0: Drupal.ajax.instances = []; Chris@0: Chris@0: /** Chris@0: * List all objects where the associated element is not in the DOM Chris@0: * Chris@0: * This method ignores {@link Drupal.Ajax} objects not bound to DOM elements Chris@0: * when created with {@link Drupal.ajax}. Chris@0: * Chris@0: * @return {Array.} Chris@0: * The list of expired {@link Drupal.Ajax} objects. Chris@0: */ Chris@17: Drupal.ajax.expired = function() { Chris@17: return Drupal.ajax.instances.filter( Chris@17: instance => Chris@17: instance && Chris@17: instance.element !== false && Chris@17: !document.body.contains(instance.element), Chris@17: ); Chris@0: }; Chris@0: Chris@0: /** Chris@14: * Bind Ajax functionality to links that use the 'use-ajax' class. Chris@14: * Chris@14: * @param {HTMLElement} element Chris@14: * Element to enable Ajax functionality for. Chris@14: */ Chris@17: Drupal.ajax.bindAjaxLinks = element => { Chris@14: // Bind Ajax behaviors to all items showing the class. Chris@17: $(element) Chris@17: .find('.use-ajax') Chris@17: .once('ajax') Chris@17: .each((i, ajaxLink) => { Chris@17: const $linkElement = $(ajaxLink); Chris@14: Chris@17: const elementSettings = { Chris@17: // Clicked links look better with the throbber than the progress bar. Chris@17: progress: { type: 'throbber' }, Chris@17: dialogType: $linkElement.data('dialog-type'), Chris@17: dialog: $linkElement.data('dialog-options'), Chris@17: dialogRenderer: $linkElement.data('dialog-renderer'), Chris@17: base: $linkElement.attr('id'), Chris@17: element: ajaxLink, Chris@17: }; Chris@17: const href = $linkElement.attr('href'); Chris@17: /** Chris@17: * For anchor tags, these will go to the target of the anchor rather Chris@17: * than the usual location. Chris@17: */ Chris@17: if (href) { Chris@17: elementSettings.url = href; Chris@17: elementSettings.event = 'click'; Chris@17: } Chris@17: Drupal.ajax(elementSettings); Chris@17: }); Chris@14: }; Chris@14: Chris@14: /** Chris@0: * Settings for an Ajax object. Chris@0: * Chris@14: * @typedef {object} Drupal.Ajax~elementSettings Chris@0: * Chris@0: * @prop {string} url Chris@0: * Target of the Ajax request. Chris@0: * @prop {?string} [event] Chris@0: * Event bound to settings.element which will trigger the Ajax request. Chris@0: * @prop {bool} [keypress=true] Chris@0: * Triggers a request on keypress events. Chris@0: * @prop {?string} selector Chris@0: * jQuery selector targeting the element to bind events to or used with Chris@0: * {@link Drupal.AjaxCommands}. Chris@0: * @prop {string} [effect='none'] Chris@0: * Name of the jQuery method to use for displaying new Ajax content. Chris@0: * @prop {string|number} [speed='none'] Chris@0: * Speed with which to apply the effect. Chris@0: * @prop {string} [method] Chris@0: * Name of the jQuery method used to insert new content in the targeted Chris@0: * element. Chris@0: * @prop {object} [progress] Chris@0: * Settings for the display of a user-friendly loader. Chris@0: * @prop {string} [progress.type='throbber'] Chris@0: * Type of progress element, core provides `'bar'`, `'throbber'` and Chris@0: * `'fullscreen'`. Chris@0: * @prop {string} [progress.message=Drupal.t('Please wait...')] Chris@0: * Custom message to be used with the bar indicator. Chris@0: * @prop {object} [submit] Chris@0: * Extra data to be sent with the Ajax request. Chris@0: * @prop {bool} [submit.js=true] Chris@0: * Allows the PHP side to know this comes from an Ajax request. Chris@0: * @prop {object} [dialog] Chris@0: * Options for {@link Drupal.dialog}. Chris@0: * @prop {string} [dialogType] Chris@0: * One of `'modal'` or `'dialog'`. Chris@0: * @prop {string} [prevent] Chris@0: * List of events on which to stop default action and stop propagation. Chris@0: */ Chris@0: Chris@0: /** Chris@0: * Ajax constructor. Chris@0: * Chris@0: * The Ajax request returns an array of commands encoded in JSON, which is Chris@0: * then executed to make any changes that are necessary to the page. Chris@0: * Chris@0: * Drupal uses this file to enhance form elements with `#ajax['url']` and Chris@0: * `#ajax['wrapper']` properties. If set, this file will automatically be Chris@0: * included to provide Ajax capabilities. Chris@0: * Chris@0: * @constructor Chris@0: * Chris@0: * @param {string} [base] Chris@0: * Base parameter of {@link Drupal.Ajax} constructor Chris@0: * @param {HTMLElement} [element] Chris@0: * Element parameter of {@link Drupal.Ajax} constructor, element on which Chris@0: * event listeners will be bound. Chris@14: * @param {Drupal.Ajax~elementSettings} elementSettings Chris@0: * Settings for this Ajax object. Chris@0: */ Chris@17: Drupal.Ajax = function(base, element, elementSettings) { Chris@0: const defaults = { Chris@0: event: element ? 'mousedown' : null, Chris@0: keypress: true, Chris@0: selector: base ? `#${base}` : null, Chris@0: effect: 'none', Chris@0: speed: 'none', Chris@0: method: 'replaceWith', Chris@0: progress: { Chris@0: type: 'throbber', Chris@0: message: Drupal.t('Please wait...'), Chris@0: }, Chris@0: submit: { Chris@0: js: true, Chris@0: }, Chris@0: }; Chris@0: Chris@14: $.extend(this, defaults, elementSettings); Chris@0: Chris@0: /** Chris@0: * @type {Drupal.AjaxCommands} Chris@0: */ Chris@0: this.commands = new Drupal.AjaxCommands(); Chris@0: Chris@0: /** Chris@0: * @type {bool|number} Chris@0: */ Chris@0: this.instanceIndex = false; Chris@0: Chris@0: // @todo Remove this after refactoring the PHP code to: Chris@0: // - Call this 'selector'. Chris@0: // - Include the '#' for ID-based selectors. Chris@0: // - Support non-ID-based selectors. Chris@0: if (this.wrapper) { Chris@0: /** Chris@0: * @type {string} Chris@0: */ Chris@0: this.wrapper = `#${this.wrapper}`; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @type {HTMLElement} Chris@0: */ Chris@0: this.element = element; Chris@0: Chris@0: /** Chris@14: * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0. Chris@14: * Use elementSettings. Chris@14: * Chris@14: * @type {Drupal.Ajax~elementSettings} Chris@0: */ Chris@14: this.element_settings = elementSettings; Chris@14: Chris@14: /** Chris@14: * @type {Drupal.Ajax~elementSettings} Chris@14: */ Chris@14: this.elementSettings = elementSettings; Chris@0: Chris@0: // If there isn't a form, jQuery.ajax() will be used instead, allowing us to Chris@0: // bind Ajax to links as well. Chris@0: if (this.element && this.element.form) { Chris@0: /** Chris@0: * @type {jQuery} Chris@0: */ Chris@0: this.$form = $(this.element.form); Chris@0: } Chris@0: Chris@0: // If no Ajax callback URL was given, use the link href or form action. Chris@0: if (!this.url) { Chris@0: const $element = $(this.element); Chris@0: if ($element.is('a')) { Chris@0: this.url = $element.attr('href'); Chris@17: } else if (this.element && element.form) { Chris@0: this.url = this.$form.attr('action'); Chris@0: } Chris@0: } Chris@0: Chris@0: // Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let Chris@0: // the server detect when it needs to degrade gracefully. Chris@0: // There are four scenarios to check for: Chris@0: // 1. /nojs/ Chris@0: // 2. /nojs$ - The end of a URL string. Chris@0: // 3. /nojs? - Followed by a query (e.g. path/nojs?destination=foobar). Chris@0: // 4. /nojs# - Followed by a fragment (e.g.: path/nojs#myfragment). Chris@0: const originalUrl = this.url; Chris@0: Chris@0: /** Chris@0: * Processed Ajax URL. Chris@0: * Chris@0: * @type {string} Chris@0: */ Chris@17: this.url = this.url.replace(/\/nojs(\/|$|\?|#)/, '/ajax$1'); Chris@0: // If the 'nojs' version of the URL is trusted, also trust the 'ajax' Chris@0: // version. Chris@0: if (drupalSettings.ajaxTrustedUrl[originalUrl]) { Chris@0: drupalSettings.ajaxTrustedUrl[this.url] = true; Chris@0: } Chris@0: Chris@0: // Set the options for the ajaxSubmit function. Chris@0: // The 'this' variable will not persist inside of the options object. Chris@0: const ajax = this; Chris@0: Chris@0: /** Chris@0: * Options for the jQuery.ajax function. Chris@0: * Chris@0: * @name Drupal.Ajax#options Chris@0: * Chris@0: * @type {object} Chris@0: * Chris@0: * @prop {string} url Chris@0: * Ajax URL to be called. Chris@0: * @prop {object} data Chris@0: * Ajax payload. Chris@0: * @prop {function} beforeSerialize Chris@0: * Implement jQuery beforeSerialize function to call Chris@0: * {@link Drupal.Ajax#beforeSerialize}. Chris@0: * @prop {function} beforeSubmit Chris@0: * Implement jQuery beforeSubmit function to call Chris@0: * {@link Drupal.Ajax#beforeSubmit}. Chris@0: * @prop {function} beforeSend Chris@0: * Implement jQuery beforeSend function to call Chris@0: * {@link Drupal.Ajax#beforeSend}. Chris@0: * @prop {function} success Chris@0: * Implement jQuery success function to call Chris@0: * {@link Drupal.Ajax#success}. Chris@0: * @prop {function} complete Chris@0: * Implement jQuery success function to clean up ajax state and trigger an Chris@0: * error if needed. Chris@0: * @prop {string} dataType='json' Chris@0: * Type of the response expected. Chris@0: * @prop {string} type='POST' Chris@0: * HTTP method to use for the Ajax request. Chris@0: */ Chris@0: ajax.options = { Chris@0: url: ajax.url, Chris@0: data: ajax.submit, Chris@14: beforeSerialize(elementSettings, options) { Chris@14: return ajax.beforeSerialize(elementSettings, options); Chris@0: }, Chris@14: beforeSubmit(formValues, elementSettings, options) { Chris@0: ajax.ajaxing = true; Chris@14: return ajax.beforeSubmit(formValues, elementSettings, options); Chris@0: }, Chris@0: beforeSend(xmlhttprequest, options) { Chris@0: ajax.ajaxing = true; Chris@0: return ajax.beforeSend(xmlhttprequest, options); Chris@0: }, Chris@0: success(response, status, xmlhttprequest) { Chris@0: // Sanity check for browser support (object expected). Chris@0: // When using iFrame uploads, responses must be returned as a string. Chris@0: if (typeof response === 'string') { Chris@0: response = $.parseJSON(response); Chris@0: } Chris@0: Chris@0: // Prior to invoking the response's commands, verify that they can be Chris@0: // trusted by checking for a response header. See Chris@0: // \Drupal\Core\EventSubscriber\AjaxResponseSubscriber for details. Chris@0: // - Empty responses are harmless so can bypass verification. This Chris@0: // avoids an alert message for server-generated no-op responses that Chris@0: // skip Ajax rendering. Chris@0: // - Ajax objects with trusted URLs (e.g., ones defined server-side via Chris@0: // #ajax) can bypass header verification. This is especially useful Chris@0: // for Ajax with multipart forms. Because IFRAME transport is used, Chris@0: // the response headers cannot be accessed for verification. Chris@0: if (response !== null && !drupalSettings.ajaxTrustedUrl[ajax.url]) { Chris@0: if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') { Chris@17: const customMessage = Drupal.t( Chris@17: 'The response failed verification so will not be processed.', Chris@17: ); Chris@0: return ajax.error(xmlhttprequest, ajax.url, customMessage); Chris@0: } Chris@0: } Chris@0: Chris@0: return ajax.success(response, status); Chris@0: }, Chris@0: complete(xmlhttprequest, status) { Chris@0: ajax.ajaxing = false; Chris@0: if (status === 'error' || status === 'parsererror') { Chris@0: return ajax.error(xmlhttprequest, ajax.url); Chris@0: } Chris@0: }, Chris@0: dataType: 'json', Chris@0: type: 'POST', Chris@0: }; Chris@0: Chris@14: if (elementSettings.dialog) { Chris@14: ajax.options.data.dialogOptions = elementSettings.dialog; Chris@0: } Chris@0: Chris@0: // Ensure that we have a valid URL by adding ? when no query parameter is Chris@0: // yet available, otherwise append using &. Chris@0: if (ajax.options.url.indexOf('?') === -1) { Chris@0: ajax.options.url += '?'; Chris@17: } else { Chris@0: ajax.options.url += '&'; Chris@0: } Chris@0: // If this element has a dialog type use if for the wrapper if not use 'ajax'. Chris@17: let wrapper = `drupal_${elementSettings.dialogType || 'ajax'}`; Chris@14: if (elementSettings.dialogRenderer) { Chris@14: wrapper += `.${elementSettings.dialogRenderer}`; Chris@0: } Chris@0: ajax.options.url += `${Drupal.ajax.WRAPPER_FORMAT}=${wrapper}`; Chris@0: Chris@0: // Bind the ajaxSubmit function to the element event. Chris@17: $(ajax.element).on(elementSettings.event, function(event) { Chris@17: if ( Chris@17: !drupalSettings.ajaxTrustedUrl[ajax.url] && Chris@17: !Drupal.url.isLocal(ajax.url) Chris@17: ) { Chris@17: throw new Error( Chris@17: Drupal.t('The callback URL is not local and not trusted: !url', { Chris@17: '!url': ajax.url, Chris@17: }), Chris@17: ); Chris@0: } Chris@0: return ajax.eventResponse(this, event); Chris@0: }); Chris@0: Chris@0: // If necessary, enable keyboard submission so that Ajax behaviors Chris@0: // can be triggered through keyboard input as well as e.g. a mousedown Chris@0: // action. Chris@14: if (elementSettings.keypress) { Chris@17: $(ajax.element).on('keypress', function(event) { Chris@0: return ajax.keypressResponse(this, event); Chris@0: }); Chris@0: } Chris@0: Chris@0: // If necessary, prevent the browser default action of an additional event. Chris@0: // For example, prevent the browser default action of a click, even if the Chris@0: // Ajax behavior binds to mousedown. Chris@14: if (elementSettings.prevent) { Chris@14: $(ajax.element).on(elementSettings.prevent, false); Chris@0: } Chris@0: }; Chris@0: Chris@0: /** Chris@0: * URL query attribute to indicate the wrapper used to render a request. Chris@0: * Chris@0: * The wrapper format determines how the HTML is wrapped, for example in a Chris@0: * modal dialog. Chris@0: * Chris@0: * @const {string} Chris@0: * Chris@0: * @default Chris@0: */ Chris@0: Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format'; Chris@0: Chris@0: /** Chris@0: * Request parameter to indicate that a request is a Drupal Ajax request. Chris@0: * Chris@0: * @const {string} Chris@0: * Chris@0: * @default Chris@0: */ Chris@0: Drupal.Ajax.AJAX_REQUEST_PARAMETER = '_drupal_ajax'; Chris@0: Chris@0: /** Chris@0: * Execute the ajax request. Chris@0: * Chris@0: * Allows developers to execute an Ajax request manually without specifying Chris@0: * an event to respond to. Chris@0: * Chris@0: * @return {object} Chris@0: * Returns the jQuery.Deferred object underlying the Ajax request. If Chris@0: * pre-serialization fails, the Deferred will be returned in the rejected Chris@0: * state. Chris@0: */ Chris@17: Drupal.Ajax.prototype.execute = function() { Chris@0: // Do not perform another ajax command if one is already in progress. Chris@0: if (this.ajaxing) { Chris@0: return; Chris@0: } Chris@0: Chris@0: try { Chris@0: this.beforeSerialize(this.element, this.options); Chris@0: // Return the jqXHR so that external code can hook into the Deferred API. Chris@0: return $.ajax(this.options); Chris@17: } catch (e) { Chris@0: // Unset the ajax.ajaxing flag here because it won't be unset during Chris@0: // the complete response. Chris@0: this.ajaxing = false; Chris@17: window.alert( Chris@17: `An error occurred while attempting to process ${this.options.url}: ${ Chris@17: e.message Chris@17: }`, Chris@17: ); Chris@0: // For consistency, return a rejected Deferred (i.e., jqXHR's superclass) Chris@0: // so that calling code can take appropriate action. Chris@0: return $.Deferred().reject(); Chris@0: } Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Handle a key press. Chris@0: * Chris@0: * The Ajax object will, if instructed, bind to a key press response. This Chris@0: * will test to see if the key press is valid to trigger this event and Chris@0: * if it is, trigger it for us and prevent other keypresses from triggering. Chris@0: * In this case we're handling RETURN and SPACEBAR keypresses (event codes 13 Chris@0: * and 32. RETURN is often used to submit a form when in a textfield, and Chris@0: * SPACE is often used to activate an element without submitting. Chris@0: * Chris@0: * @param {HTMLElement} element Chris@0: * Element the event was triggered on. Chris@0: * @param {jQuery.Event} event Chris@0: * Triggered event. Chris@0: */ Chris@17: Drupal.Ajax.prototype.keypressResponse = function(element, event) { Chris@0: // Create a synonym for this to reduce code confusion. Chris@0: const ajax = this; Chris@0: Chris@0: // Detect enter key and space bar and allow the standard response for them, Chris@0: // except for form elements of type 'text', 'tel', 'number' and 'textarea', Chris@0: // where the spacebar activation causes inappropriate activation if Chris@0: // #ajax['keypress'] is TRUE. On a text-type widget a space should always Chris@0: // be a space. Chris@17: if ( Chris@17: event.which === 13 || Chris@17: (event.which === 32 && Chris@17: element.type !== 'text' && Chris@17: element.type !== 'textarea' && Chris@17: element.type !== 'tel' && Chris@17: element.type !== 'number') Chris@17: ) { Chris@0: event.preventDefault(); Chris@0: event.stopPropagation(); Chris@14: $(element).trigger(ajax.elementSettings.event); Chris@0: } Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Handle an event that triggers an Ajax response. Chris@0: * Chris@0: * When an event that triggers an Ajax response happens, this method will Chris@0: * perform the actual Ajax call. It is bound to the event using Chris@0: * bind() in the constructor, and it uses the options specified on the Chris@0: * Ajax object. Chris@0: * Chris@0: * @param {HTMLElement} element Chris@0: * Element the event was triggered on. Chris@0: * @param {jQuery.Event} event Chris@0: * Triggered event. Chris@0: */ Chris@17: Drupal.Ajax.prototype.eventResponse = function(element, event) { Chris@0: event.preventDefault(); Chris@0: event.stopPropagation(); Chris@0: Chris@0: // Create a synonym for this to reduce code confusion. Chris@0: const ajax = this; Chris@0: Chris@0: // Do not perform another Ajax command if one is already in progress. Chris@0: if (ajax.ajaxing) { Chris@0: return; Chris@0: } Chris@0: Chris@0: try { Chris@0: if (ajax.$form) { Chris@0: // If setClick is set, we must set this to ensure that the button's Chris@0: // value is passed. Chris@0: if (ajax.setClick) { Chris@0: // Mark the clicked button. 'form.clk' is a special variable for Chris@0: // ajaxSubmit that tells the system which element got clicked to Chris@0: // trigger the submit. Without it there would be no 'op' or Chris@0: // equivalent. Chris@0: element.form.clk = element; Chris@0: } Chris@0: Chris@0: ajax.$form.ajaxSubmit(ajax.options); Chris@17: } else { Chris@0: ajax.beforeSerialize(ajax.element, ajax.options); Chris@0: $.ajax(ajax.options); Chris@0: } Chris@17: } catch (e) { Chris@0: // Unset the ajax.ajaxing flag here because it won't be unset during Chris@0: // the complete response. Chris@0: ajax.ajaxing = false; Chris@17: window.alert( Chris@17: `An error occurred while attempting to process ${ajax.options.url}: ${ Chris@17: e.message Chris@17: }`, Chris@17: ); Chris@0: } Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Handler for the form serialization. Chris@0: * Chris@0: * Runs before the beforeSend() handler (see below), and unlike that one, runs Chris@0: * before field data is collected. Chris@0: * Chris@0: * @param {object} [element] Chris@14: * Ajax object's `elementSettings`. Chris@0: * @param {object} options Chris@0: * jQuery.ajax options. Chris@0: */ Chris@17: Drupal.Ajax.prototype.beforeSerialize = function(element, options) { Chris@0: // Allow detaching behaviors to update field values before collecting them. Chris@0: // This is only needed when field values are added to the POST data, so only Chris@0: // when there is a form such that this.$form.ajaxSubmit() is used instead of Chris@0: // $.ajax(). When there is no form and $.ajax() is used, beforeSerialize() Chris@0: // isn't called, but don't rely on that: explicitly check this.$form. Chris@17: if (this.$form && document.body.contains(this.$form.get(0))) { Chris@0: const settings = this.settings || drupalSettings; Chris@0: Drupal.detachBehaviors(this.$form.get(0), settings, 'serialize'); Chris@0: } Chris@0: Chris@0: // Inform Drupal that this is an AJAX request. Chris@0: options.data[Drupal.Ajax.AJAX_REQUEST_PARAMETER] = 1; Chris@0: Chris@0: // Allow Drupal to return new JavaScript and CSS files to load without Chris@0: // returning the ones already loaded. Chris@0: // @see \Drupal\Core\Theme\AjaxBasePageNegotiator Chris@0: // @see \Drupal\Core\Asset\LibraryDependencyResolverInterface::getMinimalRepresentativeSubset() Chris@0: // @see system_js_settings_alter() Chris@0: const pageState = drupalSettings.ajaxPageState; Chris@0: options.data['ajax_page_state[theme]'] = pageState.theme; Chris@0: options.data['ajax_page_state[theme_token]'] = pageState.theme_token; Chris@0: options.data['ajax_page_state[libraries]'] = pageState.libraries; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Modify form values prior to form submission. Chris@0: * Chris@14: * @param {Array.} formValues Chris@0: * Processed form values. Chris@0: * @param {jQuery} element Chris@0: * The form node as a jQuery object. Chris@0: * @param {object} options Chris@0: * jQuery.ajax options. Chris@0: */ Chris@17: Drupal.Ajax.prototype.beforeSubmit = function(formValues, element, options) { Chris@0: // This function is left empty to make it simple to override for modules Chris@0: // that wish to add functionality here. Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Prepare the Ajax request before it is sent. Chris@0: * Chris@0: * @param {XMLHttpRequest} xmlhttprequest Chris@0: * Native Ajax object. Chris@0: * @param {object} options Chris@0: * jQuery.ajax options. Chris@0: */ Chris@17: Drupal.Ajax.prototype.beforeSend = function(xmlhttprequest, options) { Chris@0: // For forms without file inputs, the jQuery Form plugin serializes the Chris@0: // form values, and then calls jQuery's $.ajax() function, which invokes Chris@0: // this handler. In this circumstance, options.extraData is never used. For Chris@0: // forms with file inputs, the jQuery Form plugin uses the browser's normal Chris@0: // form submission mechanism, but captures the response in a hidden IFRAME. Chris@0: // In this circumstance, it calls this handler first, and then appends Chris@0: // hidden fields to the form to submit the values in options.extraData. Chris@0: // There is no simple way to know which submission mechanism will be used, Chris@0: // so we add to extraData regardless, and allow it to be ignored in the Chris@0: // former case. Chris@0: if (this.$form) { Chris@0: options.extraData = options.extraData || {}; Chris@0: Chris@0: // Let the server know when the IFRAME submission mechanism is used. The Chris@0: // server can use this information to wrap the JSON response in a Chris@0: // TEXTAREA, as per http://jquery.malsup.com/form/#file-upload. Chris@0: options.extraData.ajax_iframe_upload = '1'; Chris@0: Chris@0: // The triggering element is about to be disabled (see below), but if it Chris@0: // contains a value (e.g., a checkbox, textfield, select, etc.), ensure Chris@0: // that value is included in the submission. As per above, submissions Chris@0: // that use $.ajax() are already serialized prior to the element being Chris@0: // disabled, so this is only needed for IFRAME submissions. Chris@0: const v = $.fieldValue(this.element); Chris@0: if (v !== null) { Chris@0: options.extraData[this.element.name] = v; Chris@0: } Chris@0: } Chris@0: Chris@0: // Disable the element that received the change to prevent user interface Chris@0: // interaction while the Ajax request is in progress. ajax.ajaxing prevents Chris@0: // the element from triggering a new request, but does not prevent the user Chris@0: // from changing its value. Chris@0: $(this.element).prop('disabled', true); Chris@0: Chris@0: if (!this.progress || !this.progress.type) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Insert progress indicator. Chris@17: const progressIndicatorMethod = `setProgressIndicator${this.progress.type Chris@17: .slice(0, 1) Chris@17: .toUpperCase()}${this.progress.type.slice(1).toLowerCase()}`; Chris@17: if ( Chris@17: progressIndicatorMethod in this && Chris@17: typeof this[progressIndicatorMethod] === 'function' Chris@17: ) { Chris@0: this[progressIndicatorMethod].call(this); Chris@0: } Chris@0: }; Chris@0: Chris@0: /** Chris@17: * An animated progress throbber and container element for AJAX operations. Chris@17: * Chris@17: * @param {string} [message] Chris@17: * (optional) The message shown on the UI. Chris@17: * @return {string} Chris@17: * The HTML markup for the throbber. Chris@17: */ Chris@17: Drupal.theme.ajaxProgressThrobber = message => { Chris@17: // Build markup without adding extra white space since it affects rendering. Chris@17: const messageMarkup = Chris@17: typeof message === 'string' Chris@17: ? Drupal.theme('ajaxProgressMessage', message) Chris@17: : ''; Chris@17: const throbber = '
 
'; Chris@17: Chris@17: return `
${throbber}${messageMarkup}
`; Chris@17: }; Chris@17: Chris@17: /** Chris@17: * An animated progress throbber and container element for AJAX operations. Chris@17: * Chris@17: * @return {string} Chris@17: * The HTML markup for the throbber. Chris@17: */ Chris@17: Drupal.theme.ajaxProgressIndicatorFullscreen = () => Chris@17: '
 
'; Chris@17: Chris@17: /** Chris@17: * Formats text accompanying the AJAX progress throbber. Chris@17: * Chris@17: * @param {string} message Chris@17: * The message shown on the UI. Chris@17: * @return {string} Chris@17: * The HTML markup for the throbber. Chris@17: */ Chris@17: Drupal.theme.ajaxProgressMessage = message => Chris@17: `
${message}
`; Chris@17: Chris@17: /** Chris@0: * Sets the progress bar progress indicator. Chris@0: */ Chris@17: Drupal.Ajax.prototype.setProgressIndicatorBar = function() { Chris@17: const progressBar = new Drupal.ProgressBar( Chris@17: `ajax-progress-${this.element.id}`, Chris@17: $.noop, Chris@17: this.progress.method, Chris@17: $.noop, Chris@17: ); Chris@0: if (this.progress.message) { Chris@0: progressBar.setProgress(-1, this.progress.message); Chris@0: } Chris@0: if (this.progress.url) { Chris@17: progressBar.startMonitoring( Chris@17: this.progress.url, Chris@17: this.progress.interval || 1500, Chris@17: ); Chris@0: } Chris@17: this.progress.element = $(progressBar.element).addClass( Chris@17: 'ajax-progress ajax-progress-bar', Chris@17: ); Chris@0: this.progress.object = progressBar; Chris@0: $(this.element).after(this.progress.element); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Sets the throbber progress indicator. Chris@0: */ Chris@17: Drupal.Ajax.prototype.setProgressIndicatorThrobber = function() { Chris@17: this.progress.element = $( Chris@17: Drupal.theme('ajaxProgressThrobber', this.progress.message), Chris@17: ); Chris@0: $(this.element).after(this.progress.element); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Sets the fullscreen progress indicator. Chris@0: */ Chris@17: Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function() { Chris@17: this.progress.element = $(Drupal.theme('ajaxProgressIndicatorFullscreen')); Chris@0: $('body').after(this.progress.element); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Handler for the form redirection completion. Chris@0: * Chris@0: * @param {Array.} response Chris@0: * Drupal Ajax response. Chris@0: * @param {number} status Chris@0: * XMLHttpRequest status. Chris@0: */ Chris@17: Drupal.Ajax.prototype.success = function(response, status) { Chris@0: // Remove the progress element. Chris@0: if (this.progress.element) { Chris@0: $(this.progress.element).remove(); Chris@0: } Chris@0: if (this.progress.object) { Chris@0: this.progress.object.stopMonitoring(); Chris@0: } Chris@0: $(this.element).prop('disabled', false); Chris@0: Chris@0: // Save element's ancestors tree so if the element is removed from the dom Chris@0: // we can try to refocus one of its parents. Using addBack reverse the Chris@0: // result array, meaning that index 0 is the highest parent in the hierarchy Chris@0: // in this situation it is usually a
element. Chris@17: const elementParents = $(this.element) Chris@17: .parents('[data-drupal-selector]') Chris@17: .addBack() Chris@17: .toArray(); Chris@0: Chris@0: // Track if any command is altering the focus so we can avoid changing the Chris@0: // focus set by the Ajax command. Chris@0: let focusChanged = false; Chris@17: Object.keys(response || {}).forEach(i => { Chris@14: if (response[i].command && this.commands[response[i].command]) { Chris@0: this.commands[response[i].command](this, response[i], status); Chris@17: if ( Chris@17: response[i].command === 'invoke' && Chris@17: response[i].method === 'focus' Chris@17: ) { Chris@0: focusChanged = true; Chris@0: } Chris@0: } Chris@14: }); Chris@0: Chris@0: // If the focus hasn't be changed by the ajax commands, try to refocus the Chris@0: // triggering element or one of its parents if that element does not exist Chris@0: // anymore. Chris@17: if ( Chris@17: !focusChanged && Chris@17: this.element && Chris@17: !$(this.element).data('disable-refocus') Chris@17: ) { Chris@0: let target = false; Chris@0: Chris@17: for (let n = elementParents.length - 1; !target && n >= 0; n--) { Chris@17: target = document.querySelector( Chris@17: `[data-drupal-selector="${elementParents[n].getAttribute( Chris@17: 'data-drupal-selector', Chris@17: )}"]`, Chris@17: ); Chris@0: } Chris@0: Chris@0: if (target) { Chris@0: $(target).trigger('focus'); Chris@0: } Chris@0: } Chris@0: Chris@0: // Reattach behaviors, if they were detached in beforeSerialize(). The Chris@0: // attachBehaviors() called on the new content from processing the response Chris@0: // commands is not sufficient, because behaviors from the entire form need Chris@0: // to be reattached. Chris@17: if (this.$form && document.body.contains(this.$form.get(0))) { Chris@0: const settings = this.settings || drupalSettings; Chris@0: Drupal.attachBehaviors(this.$form.get(0), settings); Chris@0: } Chris@0: Chris@0: // Remove any response-specific settings so they don't get used on the next Chris@0: // call by mistake. Chris@0: this.settings = null; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Build an effect object to apply an effect when adding new HTML. Chris@0: * Chris@0: * @param {object} response Chris@0: * Drupal Ajax response. Chris@0: * @param {string} [response.effect] Chris@14: * Override the default value of {@link Drupal.Ajax#elementSettings}. Chris@0: * @param {string|number} [response.speed] Chris@14: * Override the default value of {@link Drupal.Ajax#elementSettings}. Chris@0: * Chris@0: * @return {object} Chris@0: * Returns an object with `showEffect`, `hideEffect` and `showSpeed` Chris@0: * properties. Chris@0: */ Chris@17: Drupal.Ajax.prototype.getEffect = function(response) { Chris@0: const type = response.effect || this.effect; Chris@0: const speed = response.speed || this.speed; Chris@0: Chris@0: const effect = {}; Chris@0: if (type === 'none') { Chris@0: effect.showEffect = 'show'; Chris@0: effect.hideEffect = 'hide'; Chris@0: effect.showSpeed = ''; Chris@17: } else if (type === 'fade') { Chris@0: effect.showEffect = 'fadeIn'; Chris@0: effect.hideEffect = 'fadeOut'; Chris@0: effect.showSpeed = speed; Chris@17: } else { Chris@0: effect.showEffect = `${type}Toggle`; Chris@0: effect.hideEffect = `${type}Toggle`; Chris@0: effect.showSpeed = speed; Chris@0: } Chris@0: Chris@0: return effect; Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Handler for the form redirection error. Chris@0: * Chris@0: * @param {object} xmlhttprequest Chris@0: * Native XMLHttpRequest object. Chris@0: * @param {string} uri Chris@0: * Ajax Request URI. Chris@0: * @param {string} [customMessage] Chris@0: * Extra message to print with the Ajax error. Chris@0: */ Chris@17: Drupal.Ajax.prototype.error = function(xmlhttprequest, uri, customMessage) { Chris@0: // Remove the progress element. Chris@0: if (this.progress.element) { Chris@0: $(this.progress.element).remove(); Chris@0: } Chris@0: if (this.progress.object) { Chris@0: this.progress.object.stopMonitoring(); Chris@0: } Chris@0: // Undo hide. Chris@0: $(this.wrapper).show(); Chris@0: // Re-enable the element. Chris@0: $(this.element).prop('disabled', false); Chris@17: // Reattach behaviors, if they were detached in beforeSerialize(), and the Chris@17: // form is still part of the document. Chris@17: if (this.$form && document.body.contains(this.$form.get(0))) { Chris@0: const settings = this.settings || drupalSettings; Chris@0: Drupal.attachBehaviors(this.$form.get(0), settings); Chris@0: } Chris@0: throw new Drupal.AjaxError(xmlhttprequest, uri, customMessage); Chris@0: }; Chris@0: Chris@0: /** Chris@17: * Provide a wrapper for new content via Ajax. Chris@17: * Chris@17: * Wrap the inserted markup when inserting multiple root elements with an Chris@17: * ajax effect. Chris@17: * Chris@17: * @param {jQuery} $newContent Chris@17: * Response elements after parsing. Chris@17: * @param {Drupal.Ajax} ajax Chris@17: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@17: * @param {object} response Chris@17: * The response from the Ajax request. Chris@17: * Chris@17: * @deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Chris@17: * Use data with desired wrapper. See https://www.drupal.org/node/2974880. Chris@17: * Chris@17: * @todo Add deprecation warning after it is possible. For more information Chris@17: * see: https://www.drupal.org/project/drupal/issues/2973400 Chris@17: * Chris@17: * @see https://www.drupal.org/node/2940704 Chris@17: */ Chris@17: Drupal.theme.ajaxWrapperNewContent = ($newContent, ajax, response) => Chris@17: (response.effect || ajax.effect) !== 'none' && Chris@17: $newContent.filter( Chris@17: i => Chris@17: !// We can not consider HTML comments or whitespace text as separate Chris@17: // roots, since they do not cause visual regression with effect. Chris@17: ( Chris@17: $newContent[i].nodeName === '#comment' || Chris@17: ($newContent[i].nodeName === '#text' && Chris@17: /^(\s|\n|\r)*$/.test($newContent[i].textContent)) Chris@17: ), Chris@17: ).length > 1 Chris@17: ? Drupal.theme('ajaxWrapperMultipleRootElements', $newContent) Chris@17: : $newContent; Chris@17: Chris@17: /** Chris@17: * Provide a wrapper for multiple root elements via Ajax. Chris@17: * Chris@17: * @param {jQuery} $elements Chris@17: * Response elements after parsing. Chris@17: * Chris@17: * @deprecated in Drupal 8.6.x and will be removed before Drupal 9.0.0. Chris@17: * Use data with desired wrapper. See https://www.drupal.org/node/2974880. Chris@17: * Chris@17: * @todo Add deprecation warning after it is possible. For more information Chris@17: * see: https://www.drupal.org/project/drupal/issues/2973400 Chris@17: * Chris@17: * @see https://www.drupal.org/node/2940704 Chris@17: */ Chris@17: Drupal.theme.ajaxWrapperMultipleRootElements = $elements => Chris@17: $('
').append($elements); Chris@17: Chris@17: /** Chris@0: * @typedef {object} Drupal.AjaxCommands~commandDefinition Chris@0: * Chris@0: * @prop {string} command Chris@0: * @prop {string} [method] Chris@0: * @prop {string} [selector] Chris@0: * @prop {string} [data] Chris@0: * @prop {object} [settings] Chris@0: * @prop {bool} [asterisk] Chris@0: * @prop {string} [text] Chris@0: * @prop {string} [title] Chris@0: * @prop {string} [url] Chris@0: * @prop {object} [argument] Chris@0: * @prop {string} [name] Chris@0: * @prop {string} [value] Chris@0: * @prop {string} [old] Chris@0: * @prop {string} [new] Chris@0: * @prop {bool} [merge] Chris@0: * @prop {Array} [args] Chris@0: * Chris@0: * @see Drupal.AjaxCommands Chris@0: */ Chris@0: Chris@0: /** Chris@0: * Provide a series of commands that the client will perform. Chris@0: * Chris@0: * @constructor Chris@0: */ Chris@17: Drupal.AjaxCommands = function() {}; Chris@0: Drupal.AjaxCommands.prototype = { Chris@0: /** Chris@0: * Command to insert new content into the DOM. Chris@0: * Chris@0: * @param {Drupal.Ajax} ajax Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.data Chris@0: * The data to use with the jQuery method. Chris@0: * @param {string} [response.method] Chris@0: * The jQuery DOM manipulation method to be used. Chris@0: * @param {string} [response.selector] Chris@0: * A optional jQuery selector string. Chris@0: * @param {object} [response.settings] Chris@0: * An optional array of settings that will be used. Chris@0: */ Chris@17: insert(ajax, response) { Chris@0: // Get information from the response. If it is not there, default to Chris@0: // our presets. Chris@17: const $wrapper = response.selector Chris@17: ? $(response.selector) Chris@17: : $(ajax.wrapper); Chris@0: const method = response.method || ajax.method; Chris@0: const effect = ajax.getEffect(response); Chris@0: Chris@17: // Apply any settings from the returned JSON if available. Chris@17: const settings = response.settings || ajax.settings || drupalSettings; Chris@0: Chris@17: // Parse response.data into an element collection. Chris@17: let $newContent = $($.parseHTML(response.data, document, true)); Chris@17: // For backward compatibility, in some cases a wrapper will be added. This Chris@17: // behavior will be removed before Drupal 9.0.0. If different behavior is Chris@17: // needed, the theme functions can be overriden. Chris@17: // @see https://www.drupal.org/node/2940704 Chris@17: $newContent = Drupal.theme( Chris@17: 'ajaxWrapperNewContent', Chris@17: $newContent, Chris@17: ajax, Chris@17: response, Chris@17: ); Chris@0: Chris@0: // If removing content from the wrapper, detach behaviors first. Chris@0: switch (method) { Chris@0: case 'html': Chris@0: case 'replaceWith': Chris@0: case 'replaceAll': Chris@0: case 'empty': Chris@0: case 'remove': Chris@0: Drupal.detachBehaviors($wrapper.get(0), settings); Chris@17: break; Chris@17: default: Chris@17: break; Chris@0: } Chris@0: Chris@0: // Add the new content to the page. Chris@14: $wrapper[method]($newContent); Chris@0: Chris@0: // Immediately hide the new content if we're using any effects. Chris@0: if (effect.showEffect !== 'show') { Chris@14: $newContent.hide(); Chris@0: } Chris@0: Chris@0: // Determine which effect to use and what content will receive the Chris@0: // effect, then show the new content. Chris@17: const $ajaxNewContent = $newContent.find('.ajax-new-content'); Chris@17: if ($ajaxNewContent.length) { Chris@17: $ajaxNewContent.hide(); Chris@14: $newContent.show(); Chris@17: $ajaxNewContent[effect.showEffect](effect.showSpeed); Chris@17: } else if (effect.showEffect !== 'show') { Chris@14: $newContent[effect.showEffect](effect.showSpeed); Chris@0: } Chris@0: Chris@0: // Attach all JavaScript behaviors to the new content, if it was Chris@0: // successfully added to the page, this if statement allows Chris@0: // `#ajax['wrapper']` to be optional. Chris@17: if ($newContent.parents('html').length) { Chris@17: // Attach behaviors to all element nodes. Chris@17: $newContent.each((index, element) => { Chris@17: if (element.nodeType === Node.ELEMENT_NODE) { Chris@17: Drupal.attachBehaviors(element, settings); Chris@17: } Chris@17: }); Chris@0: } Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to remove a chunk from the page. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.selector Chris@0: * A jQuery selector string. Chris@0: * @param {object} [response.settings] Chris@0: * An optional array of settings that will be used. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: remove(ajax, response, status) { Chris@0: const settings = response.settings || ajax.settings || drupalSettings; Chris@17: $(response.selector) Chris@17: .each(function() { Chris@17: Drupal.detachBehaviors(this, settings); Chris@17: }) Chris@0: .remove(); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to mark a chunk changed. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The JSON response object from the Ajax request. Chris@0: * @param {string} response.selector Chris@0: * A jQuery selector string. Chris@0: * @param {bool} [response.asterisk] Chris@0: * An optional CSS selector. If specified, an asterisk will be Chris@0: * appended to the HTML inside the provided selector. Chris@0: * @param {number} [status] Chris@0: * The request status. Chris@0: */ Chris@0: changed(ajax, response, status) { Chris@0: const $element = $(response.selector); Chris@0: if (!$element.hasClass('ajax-changed')) { Chris@0: $element.addClass('ajax-changed'); Chris@0: if (response.asterisk) { Chris@17: $element Chris@17: .find(response.asterisk) Chris@17: .append( Chris@17: ` * `, Chris@17: ); Chris@0: } Chris@0: } Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to provide an alert. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The JSON response from the Ajax request. Chris@0: * @param {string} response.text Chris@0: * The text that will be displayed in an alert dialog. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: alert(ajax, response, status) { Chris@0: window.alert(response.text, response.title); Chris@0: }, Chris@0: Chris@0: /** Chris@18: * Command to provide triggers audio UAs to read the supplied text. Chris@18: * Chris@18: * @param {Drupal.Ajax} [ajax] Chris@18: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@18: * @param {object} response Chris@18: * The JSON response from the Ajax request. Chris@18: * @param {string} [response.text] Chris@18: * The text that will be read. Chris@18: * @param {string} [response.priority] Chris@18: * An optional priority that will be used for the announcement. Chris@18: */ Chris@18: announce(ajax, response) { Chris@18: if (response.priority) { Chris@18: Drupal.announce(response.text, response.priority); Chris@18: } else { Chris@18: Drupal.announce(response.text); Chris@18: } Chris@18: }, Chris@18: Chris@18: /** Chris@0: * Command to set the window.location, redirecting the browser. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.url Chris@0: * The URL to redirect to. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: redirect(ajax, response, status) { Chris@0: window.location = response.url; Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to provide the jQuery css() function. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.selector Chris@0: * A jQuery selector string. Chris@0: * @param {object} response.argument Chris@0: * An array of key/value pairs to set in the CSS for the selector. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: css(ajax, response, status) { Chris@0: $(response.selector).css(response.argument); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to set the settings used for other commands in this response. Chris@0: * Chris@0: * This method will also remove expired `drupalSettings.ajax` settings. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {bool} response.merge Chris@0: * Determines whether the additional settings should be merged to the Chris@0: * global settings. Chris@0: * @param {object} response.settings Chris@0: * Contains additional settings to add to the global settings. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: settings(ajax, response, status) { Chris@0: const ajaxSettings = drupalSettings.ajax; Chris@0: Chris@0: // Clean up drupalSettings.ajax. Chris@0: if (ajaxSettings) { Chris@17: Drupal.ajax.expired().forEach(instance => { Chris@0: // If the Ajax object has been created through drupalSettings.ajax Chris@0: // it will have a selector. When there is no selector the object Chris@0: // has been initialized with a special class name picked up by the Chris@0: // Ajax behavior. Chris@0: Chris@0: if (instance.selector) { Chris@0: const selector = instance.selector.replace('#', ''); Chris@0: if (selector in ajaxSettings) { Chris@0: delete ajaxSettings[selector]; Chris@0: } Chris@0: } Chris@0: }); Chris@0: } Chris@0: Chris@0: if (response.merge) { Chris@0: $.extend(true, drupalSettings, response.settings); Chris@17: } else { Chris@0: ajax.settings = response.settings; Chris@0: } Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to attach data using jQuery's data API. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.name Chris@0: * The name or key (in the key value pair) of the data attached to this Chris@0: * selector. Chris@0: * @param {string} response.selector Chris@0: * A jQuery selector string. Chris@0: * @param {string|object} response.value Chris@0: * The value of to be attached. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: data(ajax, response, status) { Chris@0: $(response.selector).data(response.name, response.value); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to apply a jQuery method. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {Array} response.args Chris@0: * An array of arguments to the jQuery method, if any. Chris@0: * @param {string} response.method Chris@0: * The jQuery method to invoke. Chris@0: * @param {string} response.selector Chris@0: * A jQuery selector string. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: invoke(ajax, response, status) { Chris@0: const $element = $(response.selector); Chris@0: $element[response.method](...response.args); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to restripe a table. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.selector Chris@0: * A jQuery selector string. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: restripe(ajax, response, status) { Chris@0: // :even and :odd are reversed because jQuery counts from 0 and Chris@0: // we count from 1, so we're out of sync. Chris@0: // Match immediate children of the parent element to allow nesting. Chris@14: $(response.selector) Chris@14: .find('> tbody > tr:visible, > tr:visible') Chris@0: .removeClass('odd even') Chris@14: .filter(':even') Chris@14: .addClass('odd') Chris@14: .end() Chris@14: .filter(':odd') Chris@14: .addClass('even'); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to update a form's build ID. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.old Chris@0: * The old form build ID. Chris@0: * @param {string} response.new Chris@0: * The new form build ID. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: update_build_id(ajax, response, status) { Chris@17: $(`input[name="form_build_id"][value="${response.old}"]`).val( Chris@17: response.new, Chris@17: ); Chris@0: }, Chris@0: Chris@0: /** Chris@0: * Command to add css. Chris@0: * Chris@0: * Uses the proprietary addImport method if available as browsers which Chris@0: * support that method ignore @import statements in dynamically added Chris@0: * stylesheets. Chris@0: * Chris@0: * @param {Drupal.Ajax} [ajax] Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * The response from the Ajax request. Chris@0: * @param {string} response.data Chris@0: * A string that contains the styles to be added. Chris@0: * @param {number} [status] Chris@0: * The XMLHttpRequest status. Chris@0: */ Chris@0: add_css(ajax, response, status) { Chris@0: // Add the styles in the normal way. Chris@0: $('head').prepend(response.data); Chris@0: // Add imports in the styles using the addImport method if available. Chris@0: let match; Chris@17: const importMatch = /^@import url\("(.*)"\);$/gim; Chris@17: if ( Chris@17: document.styleSheets[0].addImport && Chris@17: importMatch.test(response.data) Chris@17: ) { Chris@0: importMatch.lastIndex = 0; Chris@0: do { Chris@0: match = importMatch.exec(response.data); Chris@0: document.styleSheets[0].addImport(match[1]); Chris@0: } while (match); Chris@0: } Chris@0: }, Chris@0: }; Chris@17: })(jQuery, window, Drupal, drupalSettings);