/** * @file * Defines the Drupal JavaScript API. */ /** * A jQuery object, typically the return value from a `$(selector)` call. * * Holds an HTMLElement or a collection of HTMLElements. * * @typedef {object} jQuery * * @prop {number} length=0 * Number of elements contained in the jQuery object. */ /** * Variable generated by Drupal that holds all translated strings from PHP. * * Content of this variable is automatically created by Drupal when using the * Interface Translation module. It holds the translation of strings used on * the page. * * This variable is used to pass data from the backend to the frontend. Data * contained in `drupalSettings` is used during behavior initialization. * * @global * * @var {object} drupalTranslations */ /** * Global Drupal object. * * All Drupal JavaScript APIs are contained in this namespace. * * @global * * @namespace */ window.Drupal = { behaviors: {}, locale: {} }; // JavaScript should be made compatible with libraries other than jQuery by // wrapping it in an anonymous closure. (function ( Drupal, drupalSettings, drupalTranslations, console, Proxy, Reflect, ) { /** * Helper to rethrow errors asynchronously. * * This way Errors bubbles up outside of the original callstack, making it * easier to debug errors in the browser. * * @param {Error|string} error * The error to be thrown. */ Drupal.throwError = function (error) { setTimeout(() => { throw error; }, 0); }; /** * Custom error thrown after attach/detach if one or more behaviors failed. * Initializes the JavaScript behaviors for page loads and Ajax requests. * * @callback Drupal~behaviorAttach * * @param {Document|HTMLElement} context * An element to detach behaviors from. * @param {?object} settings * An object containing settings for the current context. It is rarely used. * * @see Drupal.attachBehaviors */ /** * Reverts and cleans up JavaScript behavior initialization. * * @callback Drupal~behaviorDetach * * @param {Document|HTMLElement} context * An element to attach behaviors to. * @param {object} settings * An object containing settings for the current context. * @param {string} trigger * One of `'unload'`, `'move'`, or `'serialize'`. * * @see Drupal.detachBehaviors */ /** * @typedef {object} Drupal~behavior * * @prop {Drupal~behaviorAttach} attach * Function run on page load and after an Ajax call. * @prop {Drupal~behaviorDetach} [detach] * Function run when content is serialized or removed from the page. */ /** * Holds all initialization methods. * * @namespace Drupal.behaviors * * @type {Object.} */ /** * Defines a behavior to be run during attach and detach phases. * * Attaches all registered behaviors to a page element. * * Behaviors are event-triggered actions that attach to page elements, * enhancing default non-JavaScript UIs. Behaviors are registered in the * {@link Drupal.behaviors} object using the method 'attach' and optionally * also 'detach'. * * {@link Drupal.attachBehaviors} is added below to the `jQuery.ready` event * and therefore runs on initial page load. Developers implementing Ajax in * their solutions should also call this function after new page content has * been loaded, feeding in an element to be processed, in order to attach all * behaviors to the new content. * * Behaviors should use `var elements = * once('behavior-name', selector, context);` to ensure the behavior is * attached only once to a given element. (Doing so enables the reprocessing * of given elements, which may be needed on occasion despite the ability to * limit behavior attachment to a particular element.) * * @example * Drupal.behaviors.behaviorName = { * attach: function (context, settings) { * // ... * }, * detach: function (context, settings, trigger) { * // ... * } * }; * * @param {Document|HTMLElement} [context=document] * An element to attach behaviors to. * @param {object} [settings=drupalSettings] * An object containing settings for the current context. If none is given, * the global {@link drupalSettings} object is used. * * @see Drupal~behaviorAttach * @see Drupal.detachBehaviors * * @throws {Drupal~DrupalBehaviorError} */ Drupal.attachBehaviors = function (context, settings) { context = context || document; settings = settings || drupalSettings; const behaviors = Drupal.behaviors; // Execute all of them. Object.keys(behaviors || {}).forEach((i) => { if (typeof behaviors[i].attach === 'function') { // Don't stop the execution of behaviors in case of an error. try { behaviors[i].attach(context, settings); } catch (e) { Drupal.throwError(e); } } }); }; /** * Detaches registered behaviors from a page element. * * Developers implementing Ajax in their solutions should call this function * before page content is about to be removed, feeding in an element to be * processed, in order to allow special behaviors to detach from the content. * * Such implementations should use `once.filter()` and `once.remove()` to find * elements with their corresponding `Drupal.behaviors.behaviorName.attach` * implementation, i.e. `once.remove('behaviorName', selector, context)`, * to ensure the behavior is detached only from previously processed elements. * * @param {Document|HTMLElement} [context=document] * An element to detach behaviors from. * @param {object} [settings=drupalSettings] * An object containing settings for the current context. If none given, * the global {@link drupalSettings} object is used. * @param {string} [trigger='unload'] * A string containing what's causing the behaviors to be detached. The * possible triggers are: * - `'unload'`: The context element is being removed from the DOM. * - `'move'`: The element is about to be moved within the DOM (for example, * during a tabledrag row swap). After the move is completed, * {@link Drupal.attachBehaviors} is called, so that the behavior can undo * whatever it did in response to the move. Many behaviors won't need to * do anything simply in response to the element being moved, but because * IFRAME elements reload their "src" when being moved within the DOM, * behaviors bound to IFRAME elements (like WYSIWYG editors) may need to * take some action. * - `'serialize'`: When an Ajax form is submitted, this is called with the * form as the context. This provides every behavior within the form an * opportunity to ensure that the field elements have correct content * in them before the form is serialized. The canonical use-case is so * that WYSIWYG editors can update the hidden textarea to which they are * bound. * * @throws {Drupal~DrupalBehaviorError} * * @see Drupal~behaviorDetach * @see Drupal.attachBehaviors */ Drupal.detachBehaviors = function (context, settings, trigger) { context = context || document; settings = settings || drupalSettings; trigger = trigger || 'unload'; const behaviors = Drupal.behaviors; // Execute all of them. Object.keys(behaviors || {}).forEach((i) => { if (typeof behaviors[i].detach === 'function') { // Don't stop the execution of behaviors in case of an error. try { behaviors[i].detach(context, settings, trigger); } catch (e) { Drupal.throwError(e); } } }); }; /** * Encodes special characters in a plain-text string for display as HTML. * * @param {string} str * The string to be encoded. * * @return {string} * The encoded string. * * @ingroup sanitization */ Drupal.checkPlain = function (str) { str = str .toString() .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); return str; }; /** * Replaces placeholders with sanitized values in a string. * * @param {string} str * A string with placeholders. * @param {object} args * An object of replacements pairs to make. Incidences of any key in this * array are replaced with the corresponding value. Based on the first * character of the key, the value is escaped and/or themed: * - `'!variable'`: inserted as is. * - `'@variable'`: escape plain text to HTML ({@link Drupal.checkPlain}). * - `'%variable'`: escape text and theme as a placeholder for user- * submitted content ({@link Drupal.checkPlain} + * `{@link Drupal.theme}('placeholder')`). * * @return {string} * The formatted string. * * @see Drupal.t */ Drupal.formatString = function (str, args) { // Keep args intact. const processedArgs = {}; // Transform arguments before inserting them. Object.keys(args || {}).forEach((key) => { switch (key.charAt(0)) { // Escaped only. case '@': processedArgs[key] = Drupal.checkPlain(args[key]); break; // Pass-through. case '!': processedArgs[key] = args[key]; break; // Escaped and placeholder. default: processedArgs[key] = Drupal.theme('placeholder', args[key]); break; } }); return Drupal.stringReplace(str, processedArgs, null); }; /** * Replaces substring. * * The longest keys will be tried first. Once a substring has been replaced, * its new value will not be searched again. * * @param {string} str * A string with placeholders. * @param {object} args * Key-value pairs. * @param {Array|null} keys * Array of keys from `args`. Internal use only. * * @return {string} * The replaced string. */ Drupal.stringReplace = function (str, args, keys) { if (str.length === 0) { return str; } // If the array of keys is not passed then collect the keys from the args. if (!Array.isArray(keys)) { keys = Object.keys(args || {}); // Order the keys by the character length. The shortest one is the first. keys.sort((a, b) => a.length - b.length); } if (keys.length === 0) { return str; } // Take next longest one from the end. const key = keys.pop(); const fragments = str.split(key); if (keys.length) { for (let i = 0; i < fragments.length; i++) { // Process each fragment with a copy of remaining keys. fragments[i] = Drupal.stringReplace(fragments[i], args, keys.slice(0)); } } return fragments.join(args[key]); }; /** * Translates strings to the page language, or a given language. * * See the documentation of the server-side t() function for further details. * * @param {string} str * A string containing the English text to translate. * @param {Object.} [args] * An object of replacements pairs to make after translation. Incidences * of any key in this array are replaced with the corresponding value. * See {@link Drupal.formatString}. * @param {object} [options] * Additional options for translation. * @param {string} [options.context=''] * The context the source string belongs to. * * @return {string} * The formatted string. * The translated string. */ Drupal.t = function (str, args, options) { options = options || {}; options.context = options.context || ''; // Fetch the localized version of the string. if ( typeof drupalTranslations !== 'undefined' && drupalTranslations.strings && drupalTranslations.strings[options.context] && drupalTranslations.strings[options.context][str] ) { str = drupalTranslations.strings[options.context][str]; } if (args) { str = Drupal.formatString(str, args); } return str; }; /** * Returns the URL to a Drupal page. * * @param {string} path * Drupal path to transform to URL. * * @return {string} * The full URL. */ Drupal.url = function (path) { return drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + path; }; /** * Returns the passed in URL as an absolute URL. * * @param {string} url * The URL string to be normalized to an absolute URL. * * @return {string} * The normalized, absolute URL. * * @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js * @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript * @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53 */ Drupal.url.toAbsolute = function (url) { const urlParsingNode = document.createElement('a'); // Decode the URL first; this is required by IE <= 6. Decoding non-UTF-8 // strings may throw an exception. try { url = decodeURIComponent(url); } catch (e) { // Empty. } urlParsingNode.setAttribute('href', url); // IE <= 7 normalizes the URL when assigned to the anchor node similar to // the other browsers. return urlParsingNode.cloneNode(false).href; }; /** * Returns true if the URL is within Drupal's base path. * * @param {string} url * The URL string to be tested. * * @return {boolean} * `true` if local. * * @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58 */ Drupal.url.isLocal = function (url) { // Always use browser-derived absolute URLs in the comparison, to avoid // attempts to break out of the base path using directory traversal. let absoluteUrl = Drupal.url.toAbsolute(url); let { protocol } = window.location; // Consider URLs that match this site's base URL but use HTTPS instead of HTTP // as local as well. if (protocol === 'http:' && absoluteUrl.indexOf('https:') === 0) { protocol = 'https:'; } let baseUrl = `${protocol}//${ window.location.host }${drupalSettings.path.baseUrl.slice(0, -1)}`; // Decoding non-UTF-8 strings may throw an exception. try { absoluteUrl = decodeURIComponent(absoluteUrl); } catch (e) { // Empty. } try { baseUrl = decodeURIComponent(baseUrl); } catch (e) { // Empty. } // The given URL matches the site's base URL, or has a path under the site's // base URL. return absoluteUrl === baseUrl || absoluteUrl.indexOf(`${baseUrl}/`) === 0; }; /** * Formats a string containing a count of items. * * This function ensures that the string is pluralized correctly. Since * {@link Drupal.t} is called by this function, make sure not to pass * already-localized strings to it. * * See the documentation of the server-side * \Drupal\Core\StringTranslation\TranslationInterface::formatPlural() * function for more details. * * @param {number} count * The item count to display. * @param {string} singular * The string for the singular case. Make sure it is clear this is singular, * to ease translation (e.g. use "1 new comment" instead of "1 new"). Do not * use @count in the singular string. * @param {string} plural * The string for the plural case. Make sure it is clear this is plural, to * ease translation. Use @count in place of the item count, as in "@count * new comments". * @param {object} [args] * An object of replacements pairs to make after translation. Incidences * of any key in this array are replaced with the corresponding value. * See {@link Drupal.formatString}. * Note that you do not need to include @count in this array. * This replacement is done automatically for the plural case. * @param {object} [options] * The options to pass to the {@link Drupal.t} function. * * @return {string} * A translated string. */ Drupal.formatPlural = function (count, singular, plural, args, options) { args = args || {}; args['@count'] = count; const pluralDelimiter = drupalSettings.pluralDelimiter; const translations = Drupal.t( singular + pluralDelimiter + plural, args, options, ).split(pluralDelimiter); let index = 0; // Determine the index of the plural form. if ( typeof drupalTranslations !== 'undefined' && drupalTranslations.pluralFormula ) { index = count in drupalTranslations.pluralFormula ? drupalTranslations.pluralFormula[count] : drupalTranslations.pluralFormula.default; } else if (args['@count'] !== 1) { index = 1; } return translations[index]; }; /** * Encodes a Drupal path for use in a URL. * * For aesthetic reasons slashes are not escaped. * * @param {string} item * Unencoded path. * * @return {string} * The encoded path. */ Drupal.encodePath = function (item) { return window.encodeURIComponent(item).replace(/%2F/g, '/'); }; /** * Triggers deprecation error. * * Deprecation errors are only triggered if deprecation errors haven't * been suppressed. * * @param {Object} deprecation * The deprecation options. * @param {string} deprecation.message * The deprecation message. * * @see https://www.drupal.org/core/deprecation#javascript */ Drupal.deprecationError = ({ message }) => { if ( drupalSettings.suppressDeprecationErrors === false && typeof console !== 'undefined' && console.warn ) { console.warn(`[Deprecation] ${message}`); } }; /** * Triggers deprecation error when object property is being used. * * @param {Object} deprecation * The deprecation options. * @param {Object} deprecation.target * The targeted object. * @param {string} deprecation.deprecatedProperty * A key of the deprecated property. * @param {string} deprecation.message * The deprecation message. * @returns {Object} * * @see https://www.drupal.org/core/deprecation#javascript */ Drupal.deprecatedProperty = ({ target, deprecatedProperty, message }) => { // Proxy and Reflect are not supported by all browsers. Unsupported browsers // are ignored since this is a development feature. if (!Proxy || !Reflect) { return target; } return new Proxy(target, { get: (target, key, ...rest) => { if (key === deprecatedProperty) { Drupal.deprecationError({ message }); } return Reflect.get(target, key, ...rest); }, }); }; /** * Generates the themed representation of a Drupal object. * * All requests for themed output must go through this function. It examines * the request and routes it to the appropriate theme function. If the current * theme does not provide an override function, the generic theme function is * called. * * @example * To retrieve the HTML for text that should be emphasized and * displayed as a placeholder inside a sentence. * Drupal.theme('placeholder', text); * * @namespace * * @param {function} func * The name of the theme function to call. * @param {...args} * Additional arguments to pass along to the theme function. * * @return {string|object|HTMLElement|jQuery} * Any data the theme function returns. This could be a plain HTML string, * but also a complex object. */ Drupal.theme = function (func, ...args) { if (func in Drupal.theme) { return Drupal.theme[func](...args); } }; /** * Formats text for emphasized display in a placeholder inside a sentence. * * @param {string} str * The text to format (plain-text). * * @return {string} * The formatted text (html). */ Drupal.theme.placeholder = function (str) { return `${Drupal.checkPlain(str)}`; }; /** * Determine if an element is visible. * * @param {HTMLElement} elem * The element to check. * * @return {boolean} * True if the element is visible. */ Drupal.elementIsVisible = function (elem) { return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); }; /** * Determine if an element is hidden. * * @param {HTMLElement} elem * The element to check. * * @return {boolean} * True if the element is hidden. */ Drupal.elementIsHidden = function (elem) { return !Drupal.elementIsVisible(elem); }; })( Drupal, window.drupalSettings, window.drupalTranslations, window.console, window.Proxy, window.Reflect, );