You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
674 lines
20 KiB
674 lines
20 KiB
/** |
|
* @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.<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 {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, '"') |
|
.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 (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 |
|
*/ |
|
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); |
|
|
|
return urlParsingNode.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.startsWith('https:')) { |
|
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.startsWith(`${baseUrl}/`); |
|
}; |
|
|
|
/** |
|
* 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 (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 && 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 |
|
* <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 (typeof Drupal.theme?.[func] === 'function') { |
|
return Drupal.theme[func](...args); |
|
} |
|
|
|
Drupal.throwError( |
|
new TypeError(`Drupal.theme.${func} must be function type.`), |
|
); |
|
}; |
|
|
|
/** |
|
* 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>`; |
|
}; |
|
|
|
/** |
|
* 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, |
|
);
|
|
|