diff options
Diffstat (limited to 'core/misc/drupal.js')
-rw-r--r-- | core/misc/drupal.js | 547 |
1 files changed, 488 insertions, 59 deletions
diff --git a/core/misc/drupal.js b/core/misc/drupal.js index 79e64d06a67..86359715dfd 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -1,28 +1,167 @@ /** -* DO NOT EDIT THIS FILE. -* See the following change record for more information, -* https://www.drupal.org/node/2815083 -* @preserve -**/ - -window.Drupal = { - behaviors: {}, - locale: {} -}; - -(function (Drupal, drupalSettings, drupalTranslations, console, Proxy, Reflect) { + * @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 {HTMLDocument|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 {HTMLDocument|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.<string, Drupal~behavior>} + */ + + /** + * 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 {HTMLDocument|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; - Object.keys(behaviors || {}).forEach(i => { + // 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) { @@ -32,13 +171,56 @@ window.Drupal = { }); }; + /** + * 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 {HTMLDocument|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; - Object.keys(behaviors || {}).forEach(i => { + // 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) { @@ -48,38 +230,100 @@ window.Drupal = { }); }; + /** + * 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, '"').replace(/'/g, '''); + str = str + .toString() + .replace(/&/g, '&') + .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 = {}; - Object.keys(args || {}).forEach(key => { + // 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); } @@ -87,11 +331,13 @@ window.Drupal = { 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)); } } @@ -99,68 +345,187 @@ window.Drupal = { 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.<string, string>} [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 || ''; - if (typeof drupalTranslations !== 'undefined' && drupalTranslations.strings && drupalTranslations.strings[options.context] && drupalTranslations.strings[options.context][str]) { + // 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) {} + } 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 {bool} + * `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; + 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)}`; - 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) {} - + } catch (e) { + // Empty. + } try { baseUrl = decodeURIComponent(baseUrl); - } catch (e) {} + } 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. Please 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. Please 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); + const translations = Drupal.t( + singular + pluralDelimiter + plural, + args, + options, + ).split(pluralDelimiter); let index = 0; - if (typeof drupalTranslations !== 'undefined' && drupalTranslations.pluralFormula) { - index = count in drupalTranslations.pluralFormula ? drupalTranslations.pluralFormula[count] : drupalTranslations.pluralFormula.default; + // 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; } @@ -168,59 +533,123 @@ window.Drupal = { 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, '/'); }; - Drupal.deprecationError = _ref => { - let { - message - } = _ref; - - if (drupalSettings.suppressDeprecationErrors === false && typeof console !== 'undefined' && console.warn) { + /** + * 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}`); } }; - Drupal.deprecatedProperty = _ref2 => { - let { - target, - deprecatedProperty, - message - } = _ref2; - + /** + * 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: function (target, key) { + get: (target, key, ...rest) => { if (key === deprecatedProperty) { - Drupal.deprecationError({ - message - }); + Drupal.deprecationError({ message }); } - - for (var _len = arguments.length, rest = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { - rest[_key - 2] = arguments[_key]; - } - return Reflect.get(target, key, ...rest); - } + }, }); }; - Drupal.theme = function (func) { + /** + * 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 + * <caption>To retrieve the HTML for text that should be emphasized and + * displayed as a placeholder inside a sentence.</caption> + * 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) { - for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { - args[_key2 - 1] = arguments[_key2]; - } - 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 `<em class="placeholder">${Drupal.checkPlain(str)}</em>`; }; -})(Drupal, window.drupalSettings, window.drupalTranslations, window.console, window.Proxy, window.Reflect);
\ No newline at end of file +})( + Drupal, + window.drupalSettings, + window.drupalTranslations, + window.console, + window.Proxy, + window.Reflect, +); |