1/** 2 * @file 3 * Defines the Drupal JavaScript API. 4 */ 5 6/** 7 * A jQuery object, typically the return value from a `$(selector)` call. 8 * 9 * Holds an HTMLElement or a collection of HTMLElements. 10 * 11 * @typedef {object} jQuery 12 * 13 * @prop {number} length=0 14 * Number of elements contained in the jQuery object. 15 */ 16 17/** 18 * Variable generated by Drupal that holds all translated strings from PHP. 19 * 20 * Content of this variable is automatically created by Drupal when using the 21 * Interface Translation module. It holds the translation of strings used on 22 * the page. 23 * 24 * This variable is used to pass data from the backend to the frontend. Data 25 * contained in `drupalSettings` is used during behavior initialization. 26 * 27 * @global 28 * 29 * @var {object} drupalTranslations 30 */ 31 32/** 33 * Global Drupal object. 34 * 35 * All Drupal JavaScript APIs are contained in this namespace. 36 * 37 * @global 38 * 39 * @namespace 40 */ 41window.Drupal = { behaviors: {}, locale: {} }; 42 43// JavaScript should be made compatible with libraries other than jQuery by 44// wrapping it in an anonymous closure. 45(function ( 46 Drupal, 47 drupalSettings, 48 drupalTranslations,
49 console, 50 Proxy, 51 Reflect, 52) { 53 /** 54 * Helper to rethrow errors asynchronously. 55 * 56 * This way Errors bubbles up outside of the original callstack, making it 57 * easier to debug errors in the browser. 58 * 59 * @param {Error|string} error 60 * The error to be thrown. 61 */ 62 Drupal.throwError = function (error) { 63 setTimeout(() => { 64 throw error; 65 }, 0); 66 }; 67 68 /** 69 * Custom error thrown after attach/detach if one or more behaviors failed. 70 * Initializes the JavaScript behaviors for page loads and Ajax requests. 71 * 72 * @callback Drupal~behaviorAttach 73 * 74 * @param {Document|HTMLElement} context 75 * An element to detach behaviors from. 76 * @param {?object} settings 77 * An object containing settings for the current context. It is rarely used. 78 * 79 * @see Drupal.attachBehaviors 80 */ 81 82 /** 83 * Reverts and cleans up JavaScript behavior initialization. 84 * 85 * @callback Drupal~behaviorDetach 86 * 87 * @param {Document|HTMLElement} context 88 * An element to attach behaviors to. 89 * @param {object} settings 90 * An object containing settings for the current context. 91 * @param {string} trigger 92 * One of `'unload'`, `'move'`, or `'serialize'`. 93 * 94 * @see Drupal.detachBehaviors 95 */ 96 97 /** 98 * @typedef {object} Drupal~behavior 99 * 100 * @prop {Drupal~behaviorAttach} attach 101 * Function run on page load and after an Ajax call. 102 * @prop {Drupal~behaviorDetach} [detach] 103 * Function run when content is serialized or removed from the page. 104 */ 105 106 /** 107 * Holds all initialization methods. 108 * 109 * @namespace Drupal.behaviors 110 * 111 * @type {Object.<string, Drupal~behavior>} 112 */ 113 114 /** 115 * Defines a behavior to be run during attach and detach phases. 116 * 117 * Attaches all registered behaviors to a page element. 118 * 119 * Behaviors are event-triggered actions that attach to page elements, 120 * enhancing default non-JavaScript UIs. Behaviors are registered in the 121 * {@link Drupal.behaviors} object using the method 'attach' and optionally 122 * also 'detach'. 123 * 124 * {@link Drupal.attachBehaviors} is added below to the `jQuery.ready` event 125 * and therefore runs on initial page load. Developers implementing Ajax in 126 * their solutions should also call this function after new page content has 127 * been loaded, feeding in an element to be processed, in order to attach all 128 * behaviors to the new content. 129 * 130 * Behaviors should use `var elements = 131 * once('behavior-name', selector, context);` to ensure the behavior is 132 * attached only once to a given element. (Doing so enables the reprocessing 133 * of given elements, which may be needed on occasion despite the ability to 134 * limit behavior attachment to a particular element.) 135 * 136 * @example 137 * Drupal.behaviors.behaviorName = { 138 * attach: function (context, settings) { 139 * // ... 140 * }, 141 * detach: function (context, settings, trigger) { 142 * // ... 143 * } 144 * }; 145 * 146 * @param {Document|HTMLElement} [context=document] 147 * An element to attach behaviors to. 148 * @param {object} [settings=drupalSettings] 149 * An object containing settings for the current context. If none is given, 150 * the global {@link drupalSettings} object is used. 151 * 152 * @see Drupal~behaviorAttach 153 * @see Drupal.detachBehaviors 154 * 155 * @throws {Drupal~DrupalBehaviorError} 156 */ 157 Drupal.attachBehaviors = function (context, settings) { 158 context = context || document; 159 settings = settings || drupalSettings; 160 const behaviors = Drupal.behaviors; 161 // Execute all of them. 162 Object.keys(behaviors || {}).forEach((i) => { 163 if (typeof behaviors[i].attach === 'function') { 164 // Don't stop the execution of behaviors in case of an error. 165 try { 166 behaviors[i].attach(context, settings); 167 } catch (e) { 168 Drupal.throwError(e); 169 } 170 } 171 }); 172 }; 173 174 /** 175 * Detaches registered behaviors from a page element. 176 * 177 * Developers implementing Ajax in their solutions should call this function 178 * before page content is about to be removed, feeding in an element to be 179 * processed, in order to allow special behaviors to detach from the content. 180 * 181 * Such implementations should use `once.filter()` and `once.remove()` to find 182 * elements with their corresponding `Drupal.behaviors.behaviorName.attach` 183 * implementation, i.e. `once.remove('behaviorName', selector, context)`, 184 * to ensure the behavior is detached only from previously processed elements. 185 * 186 * @param {Document|HTMLElement} [context=document] 187 * An element to detach behaviors from. 188 * @param {object} [settings=drupalSettings]
189 * An object containing settings for the current context. If none given, 190 * the global {@link drupalSettings} object is used. 191 * @param {string} [trigger='unload'] 192 * A string containing what's causing the behaviors to be detached. The 193 * possible triggers are: 194 * - `'unload'`: The context element is being removed from the DOM. 195 * - `'move'`: The element is about to be moved within the DOM (for example, 196 * during a tabledrag row swap). After the move is completed, 197 * {@link Drupal.attachBehaviors} is called, so that the behavior can undo 198 * whatever it did in response to the move. Many behaviors won't need to 199 * do anything simply in response to the element being moved, but because 200 * IFRAME elements reload their "src" when being moved within the DOM, 201 * behaviors bound to IFRAME elements (like WYSIWYG editors) may need to 202 * take some action. 203 * - `'serialize'`: When an Ajax form is submitted, this is called with the 204 * form as the context. This provides every behavior within the form an 205 * opportunity to ensure that the field elements have correct content 206 * in them before the form is serialized. The canonical use-case is so 207 * that WYSIWYG editors can update the hidden textarea to which they are 208 * bound. 209 * 210 * @throws {Drupal~DrupalBehaviorError} 211 * 212 * @see Drupal~behaviorDetach 213 * @see Drupal.attachBehaviors 214 */ 215 Drupal.detachBehaviors = function (context, settings, trigger) { 216 context = context || document; 217 settings = settings || drupalSettings; 218 trigger = trigger || 'unload'; 219 const behaviors = Drupal.behaviors; 220 // Execute all of them. 221 Object.keys(behaviors || {}).forEach((i) => { 222 if (typeof behaviors[i].detach === 'function') { 223 // Don't stop the execution of behaviors in case of an error. 224 try { 225 behaviors[i].detach(context, settings, trigger); 226 } catch (e) { 227 Drupal.throwError(e); 228 } 229 } 230 }); 231 }; 232 233 /** 234 * Encodes special characters in a plain-text string for display as HTML. 235 * 236 * @param {string} str 237 * The string to be encoded. 238 * 239 * @return {string} 240 * The encoded string. 241 * 242 * @ingroup sanitization 243 */ 244 Drupal.checkPlain = function (str) { 245 str = str 246 .toString() 247 .replace(/&/g, '&') 248 .replace(/</g, '<') 249 .replace(/>/g, '>') 250 .replace(/"/g, '"') 251 .replace(/'/g, '''); 252 return str; 253 }; 254 255 /** 256 * Replaces placeholders with sanitized values in a string. 257 * 258 * @param {string} str 259 * A string with placeholders. 260 * @param {object} args 261 * An object of replacements pairs to make. Incidences of any key in this 262 * array are replaced with the corresponding value. Based on the first 263 * character of the key, the value is escaped and/or themed: 264 * - `'!variable'`: inserted as is. 265 * - `'@variable'`: escape plain text to HTML ({@link Drupal.checkPlain}). 266 * - `'%variable'`: escape text and theme as a placeholder for user- 267 * submitted content ({@link Drupal.checkPlain} + 268 * `{@link Drupal.theme}('placeholder')`). 269 * 270 * @return {string} 271 * The formatted string. 272 * 273 * @see Drupal.t 274 */ 275 Drupal.formatString = function (str, args) { 276 // Keep args intact. 277 const processedArgs = {}; 278 // Transform arguments before inserting them. 279 Object.keys(args || {}).forEach((key) => { 280 switch (key.charAt(0)) { 281 // Escaped only. 282 case '@': 283 processedArgs[key] = Drupal.checkPlain(args[key]); 284 break; 285 286 // Pass-through. 287 case '!': 288 processedArgs[key] = args[key]; 289 break; 290 291 // Escaped and placeholder. 292 default: 293 processedArgs[key] = Drupal.theme('placeholder', args[key]); 294 break; 295 } 296 }); 297 298 return Drupal.stringReplace(str, processedArgs, null); 299 }; 300 301 /** 302 * Replaces substring. 303 * 304 * The longest keys will be tried first. Once a substring has been replaced, 305 * its new value will not be searched again. 306 * 307 * @param {string} str 308 * A string with placeholders. 309 * @param {object} args 310 * Key-value pairs. 311 * @param {Array|null} keys 312 * Array of keys from `args`. Internal use only. 313 * 314 * @return {string} 315 * The replaced string. 316 */ 317 Drupal.stringReplace = function (str, args, keys) { 318 if (str.length === 0) { 319 return str; 320 } 321 322 // If the array of keys is not passed then collect the keys from the args. 323 if (!Array.isArray(keys)) { 324 keys = Object.keys(args || {}); 325 326 // Order the keys by the character length. The shortest one is the first. 327 keys.sort((a, b) => a.length - b.length); 328 } 329 330 if (keys.length === 0) { 331 return str; 332 } 333 334 // Take next longest one from the end.
335 const key = keys.pop(); 336 const fragments = str.split(key); 337 338 if (keys.length) { 339 for (let i = 0; i < fragments.length; i++) { 340 // Process each fragment with a copy of remaining keys. 341 fragments[i] = Drupal.stringReplace(fragments[i], args, keys.slice(0)); 342 } 343 } 344 345 return fragments.join(args[key]); 346 }; 347 348 /** 349 * Translates strings to the page language, or a given language. 350 * 351 * See the documentation of the server-side t() function for further details. 352 * 353 * @param {string} str 354 * A string containing the English text to translate. 355 * @param {Object.<string, string>} [args] 356 * An object of replacements pairs to make after translation. Incidences 357 * of any key in this array are replaced with the corresponding value. 358 * See {@link Drupal.formatString}. 359 * @param {object} [options] 360 * Additional options for translation. 361 * @param {string} [options.context=''] 362 * The context the source string belongs to. 363 * 364 * @return {string} 365 * The formatted string. 366 * The translated string. 367 */ 368 Drupal.t = function (str, args, options) { 369 options = options || {}; 370 options.context = options.context || ''; 371 372 // Fetch the localized version of the string. 373 if (drupalTranslations?.strings?.[options.context]?.[str]) { 374 str = drupalTranslations.strings[options.context][str]; 375 } 376 377 if (args) { 378 str = Drupal.formatString(str, args); 379 } 380 return str; 381 }; 382 383 /** 384 * Returns the URL to a Drupal page. 385 * 386 * @param {string} path 387 * Drupal path to transform to URL. 388 * 389 * @return {string} 390 * The full URL. 391 */ 392 Drupal.url = function (path) { 393 return drupalSettings.path.baseUrl + drupalSettings.path.pathPrefix + path; 394 }; 395 396 /** 397 * Returns the passed in URL as an absolute URL. 398 * 399 * @param {string} url 400 * The URL string to be normalized to an absolute URL. 401 * 402 * @return {string} 403 * The normalized, absolute URL. 404 * 405 * @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js 406 * @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript 407 */ 408 Drupal.url.toAbsolute = function (url) { 409 const urlParsingNode = document.createElement('a'); 410 411 // Decode the URL first; this is required by IE <= 6. Decoding non-UTF-8 412 // strings may throw an exception. 413 try { 414 url = decodeURIComponent(url); 415 } catch (e) { 416 // Empty. 417 } 418 419 urlParsingNode.setAttribute('href', url); 420 421 return urlParsingNode.href; 422 }; 423 424 /** 425 * Returns true if the URL is within Drupal's base path. 426 * 427 * @param {string} url 428 * The URL string to be tested. 429 * 430 * @return {boolean} 431 * `true` if local. 432 * 433 * @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58 434 */ 435 Drupal.url.isLocal = function (url) { 436 // Always use browser-derived absolute URLs in the comparison, to avoid 437 // attempts to break out of the base path using directory traversal. 438 let absoluteUrl = Drupal.url.toAbsolute(url); 439 let { protocol } = window.location; 440 441 // Consider URLs that match this site's base URL but use HTTPS instead of HTTP 442 // as local as well. 443 if (protocol === 'http:' && absoluteUrl.startsWith('https:')) { 444 protocol = 'https:'; 445 } 446 let baseUrl = `${protocol}//${ 447 window.location.host 448 }${drupalSettings.path.baseUrl.slice(0, -1)}`; 449 450 // Decoding non-UTF-8 strings may throw an exception. 451 try { 452 absoluteUrl = decodeURIComponent(absoluteUrl); 453 } catch (e) { 454 // Empty. 455 } 456 try { 457 baseUrl = decodeURIComponent(baseUrl); 458 } catch (e) { 459 // Empty. 460 } 461 462 // The given URL matches the site's base URL, or has a path under the site's 463 // base URL. 464 return absoluteUrl === baseUrl || absoluteUrl.startsWith(`${baseUrl}/`); 465 }; 466 467 /** 468 * Formats a string containing a count of items. 469 * 470 * This function ensures that the string is pluralized correctly. Since 471 * {@link Drupal.t} is called by this function, make sure not to pass 472 * already-localized strings to it. 473 * 474 * See the documentation of the server-side 475 * \Drupal\Core\StringTranslation\TranslationInterface::formatPlural() 476 * function for more details. 477 * 478 * @param {number} count 479 * The item count to display. 480 * @param {string} singular 481 * The string for the singular case. Make sure it is clear this is singular, 482 * to ease translation (e.g. use "1 new comment" instead of "1 new"). Do not 483 * use @count in the singular string. 484 * @param {string} plural 485 * The string for the plural case. Make sure it is clear this is plural, to 486 * ease translation. Use @count in place of the item count, as in "@count 487 * new comments". 488 * @param {object} [args] 489 * An object of replacements pairs to make after translation. Incidences 490 * of any key in this array are replaced with the corresponding value. 491 * See {@link Drupal.formatString}.
492 * Note that you do not need to include @count in this array. 493 * This replacement is done automatically for the plural case. 494 * @param {object} [options] 495 * The options to pass to the {@link Drupal.t} function. 496 * 497 * @return {string} 498 * A translated string. 499 */ 500 Drupal.formatPlural = function (count, singular, plural, args, options) { 501 args = args || {}; 502 args['@count'] = count; 503 504 const pluralDelimiter = drupalSettings.pluralDelimiter; 505 const translations = Drupal.t( 506 singular + pluralDelimiter + plural, 507 args, 508 options, 509 ).split(pluralDelimiter); 510 let index = 0; 511 512 // Determine the index of the plural form. 513 if (drupalTranslations?.pluralFormula) { 514 index = 515 count in drupalTranslations.pluralFormula 516 ? drupalTranslations.pluralFormula[count] 517 : drupalTranslations.pluralFormula.default; 518 } else if (args['@count'] !== 1) { 519 index = 1; 520 } 521 522 return translations[index]; 523 }; 524 525 /** 526 * Encodes a Drupal path for use in a URL. 527 * 528 * For aesthetic reasons slashes are not escaped. 529 * 530 * @param {string} item 531 * Unencoded path. 532 * 533 * @return {string} 534 * The encoded path. 535 */ 536 Drupal.encodePath = function (item) { 537 return window.encodeURIComponent(item).replace(/%2F/g, '/'); 538 }; 539 540 /** 541 * Triggers deprecation error. 542 * 543 * Deprecation errors are only triggered if deprecation errors haven't 544 * been suppressed. 545 * 546 * @param {Object} deprecation 547 * The deprecation options. 548 * @param {string} deprecation.message 549 * The deprecation message. 550 * 551 * @see https://www.drupal.org/core/deprecation#javascript 552 */ 553 Drupal.deprecationError = ({ message }) => { 554 if (drupalSettings.suppressDeprecationErrors === false && console?.warn) { 555 console.warn(`[Deprecation] ${message}`); 556 } 557 }; 558 559 /** 560 * Triggers deprecation error when object property is being used. 561 * 562 * @param {Object} deprecation 563 * The deprecation options. 564 * @param {Object} deprecation.target 565 * The targeted object. 566 * @param {string} deprecation.deprecatedProperty 567 * A key of the deprecated property. 568 * @param {string} deprecation.message 569 * The deprecation message. 570 * @returns {Object} 571 * 572 * @see https://www.drupal.org/core/deprecation#javascript 573 */ 574 Drupal.deprecatedProperty = ({ target, deprecatedProperty, message }) => { 575 // Proxy and Reflect are not supported by all browsers. Unsupported browsers 576 // are ignored since this is a development feature. 577 if (!Proxy || !Reflect) { 578 return target; 579 } 580 581 return new Proxy(target, { 582 get: (target, key, ...rest) => { 583 if (key === deprecatedProperty) { 584 Drupal.deprecationError({ message }); 585 } 586 return Reflect.get(target, key, ...rest); 587 }, 588 }); 589 }; 590 591 /** 592 * Generates the themed representation of a Drupal object. 593 * 594 * All requests for themed output must go through this function. It examines 595 * the request and routes it to the appropriate theme function. If the current 596 * theme does not provide an override function, the generic theme function is 597 * called. 598 * 599 * @example 600 * <caption>To retrieve the HTML for text that should be emphasized and 601 * displayed as a placeholder inside a sentence.</caption> 602 * Drupal.theme('placeholder', text); 603 * 604 * @namespace 605 * 606 * @param {function} func 607 * The name of the theme function to call. 608 * @param {...args} 609 * Additional arguments to pass along to the theme function. 610 * 611 * @return {string|object|HTMLElement|jQuery} 612 * Any data the theme function returns. This could be a plain HTML string, 613 * but also a complex object. 614 */ 615 Drupal.theme = function (func, ...args) { 616 if (typeof Drupal.theme?.[func] === 'function') { 617 return Drupal.theme[func](...args); 618 } 619 620 Drupal.throwError( 621 new TypeError(`Drupal.theme.${func} must be function type.`), 622 ); 623 }; 624 625 /** 626 * Formats text for emphasized display in a placeholder inside a sentence. 627 * 628 * @param {string} str 629 * The text to format (plain-text). 630 * 631 * @return {string} 632 * The formatted text (html). 633 */ 634 Drupal.theme.placeholder = function (str) { 635 return `<em class="placeholder">${Drupal.checkPlain(str)}</em>`; 636 }; 637 638 /** 639 * Determine if an element is visible. 640 * 641 * @param {HTMLElement} elem 642 * The element to check. 643 * 644 * @return {boolean} 645 * True if the element is visible. 646 */ 647 Drupal.elementIsVisible = function (elem) { 648 return !!( 649 elem.offsetWidth || 650 elem.offsetHeight || 651 elem.getClientRects().length 652 ); 653 }; 654 655 /** 656 * Determine if an element is hidden. 657 * 658 * @param {HTMLElement} elem 659 * The element to check. 660 * 661 * @return {boolean} 662 * True if the element is hidden. 663 */ 664 Drupal.elementIsHidden = function (elem) { 665 return !Drupal.elementIsVisible(elem); 666 }; 667})( 668 Drupal, 669 window.drupalSettings, 670 window.drupalTranslations, 671 window.console, 672 window.Proxy, 673 window.Reflect, 674);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.