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