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