Mercurial > hg > cmmr2012-drupal-site
diff core/misc/drupal.es6.js @ 0:c75dbcec494b
Initial commit from drush-created site
author | Chris Cannam |
---|---|
date | Thu, 05 Jul 2018 14:24:15 +0000 |
parents | |
children | a9cd425dd02b |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core/misc/drupal.es6.js Thu Jul 05 14:24:15 2018 +0000 @@ -0,0 +1,574 @@ +/** + * @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) { + /** + * 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 = + * $(context).find(selector).once('behavior-name');` 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; + // 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 `.findOnce()` and `.removeOnce()` to find + * elements with their corresponding `Drupal.behaviors.behaviorName.attach` + * implementation, i.e. `.removeOnce('behaviorName')`, 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; + // 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, '"') + .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.<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 || ''; + + // 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 {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 = location.protocol; + + // 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}//${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. 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); + 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, '/'); + }; + + /** + * 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) { + 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));