PageSourceSearch

https://nationaltrailraceway.com/modules/custom/trackside/js/trackside.utility.js?tm8104

js nationaltrailraceway.com collected 2026-10-02 05:51:45 UTC 53,961 bytes, 1,293 lines download raw bytes

1// This utility object is available site-wide.
2const trackside = (function($, Drupal) {
3  "use strict";
4
5  const General = {
6    /**
7     * Converts any HTML in the given string into their equivalent HTML entities.
8     *
9     * @param {string} stringToSanitize The string to sanitize.
10     * @return {string} The sanitized-of-HTML string.
11     */
12    htmlSanitize: function(stringToSanitize) {
13      return $("<div></div>").text(stringToSanitize).html();
14    },
15
16    /**
17     * Escapes an untrusted string for use within a regular expression.
18     * Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
19     *
20     * @param {string} string The string to escape.
21     * @return {string} The escaped string, now safe to use to build a regular expression.
22     */
23    escapeRegExp: function(string) {
24      return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
25    },
26
27    /**
28     * Converts an array of strings into the HTML markup of those strings in a list.
29     * By default places list items in `<li>` elements inside a parent `<ul>` element,
30     * but the markup used in configurable.
31     *
32     * @param {array} stringArray The array of strings to put in the list.
33     * @param {object} options An object of options properties. See default options inside method.
34     * @return {string} The HTML string of the built list, or if the given value was not an array,
35     *    will return an HTML comment with an error message in it.
36     */
37    arrayToHtmlList: function(stringArray, options = {}) {
38      if (!Array.isArray(stringArray))
39      {
40        const errorMessage = "The given value was not an array";
41
42        console.error(errorMessage);
43
44        return `<!-- ${errorMessage} -->`;
45      }
46
47      const defaultOptions = {
48        /*
49         * This option defines the element(s) that wrap the list items.
50         * Provide a period-delimited string of the element tags to wrap the items with.
51         * The last element tag in the list will wrap each list item, while all element tags
52         * before that will wrap the list in its entirety.
53         * For example, the default value of "ul.li" creates an `<li>` element for each list item, and then
54         * wraps all of those in a single `<ul>` element.
55         * If "div.ul.li" were to be used instead, it would result in the same output, but also wrapped
56         * in a single outermost `<div>` element.
57         */
58        wrap: "ul.li",
59      };
60
61      options = $.extend({}, defaultOptions, options || {});
62
63      let elementTags = options.wrap.split(".");
64
65      if (elementTags.length == 0)
66      {
67        console.error("Invalid tag elements specified; Using default option value");
68
69        elementTags = defaultOptions.wrap.split(".");
70      }
71
72      let wrapStart = "";
73      let wrapEnd = "";
74      let finalList = "";
75
76      for (var i = 0; i < elementTags.length; i++)
77      {
78        const tag = elementTags[i].trim().toLowerCase();
79        const isLastTag = i == elementTags.length - 1;
80
81        wrapStart += `<${tag}>`;
82        wrapEnd = `</${tag}>` + wrapEnd;
83
84        // Glue it all together when we get to the last wrapper element
85        if (isLastTag)
86        {
87          const itemGlue = `</${tag}><${tag}>`;
88
89          finalList = wrapStart
90            + stringArray.map(x => General.htmlSanitize(x).trim()).join(itemGlue)
91            + wrapEnd;
92        }
93      }
94
95      return finalList;
96    },
97
98    // AJAX error response helper function.
99    // Gathers the relevant details from the response into a string and returns it.
100    getAjaxFailureDetails: function(jqXHR, textStatus = "", errorThrown = "") {
101      console.error(jqXHR, textStatus, errorThrown);
102
103      let failureMessage = "";
104
105      try
106      {
107        if (jqXHR && jqXHR.status)
108        {
109          failureMessage = "HTTP " + jqXHR.status;
110        }
111
112        if (errorThrown || (jqXHR && jqXHR.statusText) || textStatus)
113        {
114          let specificError = "";
115          const unhelpfulErrorText = "error";
116
117          if (errorThrown && errorThrown !== unhelpfulErrorText)
118          {
119            specificError = errorThrown;
120          }
121          else if (jqXHR && jqXHR.statusText && jqXHR.statusText !== unhelpfulErrorText)
122          {
123            specificError = jqXHR.statusText;
124          }
125          else if (textStatus && textStatus !== unhelpfulErrorText)
126          {
127            specificError = textStatus;
128          }
129
130          if (specificError)
131          {
132            failureMessage += " - " + specificError;
133          }
134        }
135      }
136      catch (ex)
137      {
138        console.error(ex);
139
140        failureMessage += " Failed to gather error details. Exception details: " + ex;
141      }
142
143      return failureMessage.trim();
144    },
145
146    // Returns a unique integer
147    getUniqueID: function() {
148      return Date.now();
149    },
150
151    /**
152     * Combines all properties of the two given objects into a new object.
153     * Any properties of the "givenClasses" that match with a property of the "defaultClasses"
154     * will have any CSS class substring not already in the value of the "defaultClasses" property value
155     * appended to it, including appropriate space character separation.
156     *
157     * @param {object} defaultClasses An object with properties of strings of CSS classes.
158     *    Any property where `givenClasses` has a matching property will be output with these class substrings first.
159     * @param {object} givenClasses An object with properties of strings of CSS classes.
160     *    Any property where `defaultClasses` has a matching property will be output with these class substrings last.
161     * @return {object} An object with properties of strings of CSS classes built by combinin the two input objects.
162     */
163    combineCssClassObjectProperties: function(defaultClasses, givenClasses = {}) {
164      if (!givenClasses || Object.keys(givenClasses).length === 0)
165      {
166        return defaultClasses;
167      }
168
169      let combinedClasses = {};
170
171      // Check if the given class isn't already in a default property's value.
172      const shouldCombineClass = (defaultClassProp, classToCheckFor) => {
173        // Split apart the default classes string into an array of individual class strings
174        // so we can accurately check for the existance of a specific class string.
175        return !defaultClasses[defaultClassProp].split(" ").map(x => x.trim()).includes(classToCheckFor);
176      };
177
178      for (const classProp in givenClasses)
179      {
180        if (classProp in defaultClasses)
181        {
182          combinedClasses[classProp] = defaultClasses[classProp].trim()
183            + " "
184            + givenClasses[classProp].trim()
185              .split(" ")
186              .map(x => x.trim())
187              .filter(x => shouldCombineClass(classProp, x))
188              .join(" ");
189        }
190      }
191
192      return $.extend({}, defaultClasses, givenClasses, combinedClasses);
193    },
194
195    /**
196     * Combines deeply-nested options objects used for the dialogs in this utility.
197     * Handles special cases like `optionsObject.dialogOptions.classes` automatically.
198     *
199     * @param {object} defaultOptions The default options to serve as a base and be selectively overridden.
200     * @param {object} givenOptions The options to override some or all of the default options with.
201     * @return {object} The fully combined options, with special cases automatically handled.
202     */
203    combineDialogOptions: function(defaultOptions, givenOptions = {}) {
204      let specialCaseCombinedOptions = { dialogOptions: { classes: {} }};
205
206      if (givenOptions && 'dialogOptions' in givenOptions && 'classes' in givenOptions.dialogOptions)
207      {
208        specialCaseCombinedOptions.dialogOptions.classes = General.combineCssClassObjectProperties(
209          defaultOptions.dialogOptions.classes,
210          givenOptions.dialogOptions.classes
211        );
212      }
213
214      return $.extend(true, {}, defaultOptions, givenOptions || {}, specialCaseCombinedOptions);
215    }
216  };
217
218  const BaseDialog = {
219    defaultOptions: {
220      useDelay: false,
221      useDelayOnPageLoad: false,
222      delayLength: 500,
223
224      // Display modally, blocking interaction with other page elements
225      showAsModal: true,
226
227      // Allows you to specify the ID attribute of the outermost "content wrapping" element inside the dialog.
228      outerContentElementID: "",
229
230      // This uses jQuery UI's `beforeClose` event function option specified during dialog creation.
231      // It will end up listening for the 'dialogbeforeclose' custom event fired by jQuery UI.
232      // If this event is cancelled (`false` returned from it), the dialog will NOT close.
233      // This should have the signature `function (event, ui){}`, though notably `ui` will always be empty (it's only there for consistency).
234      onBeforeClose: null,
235
236      // This will be wired up to listen for jQuery UI's 'dialogclose' custom event right *after* the dialog is created.
237      // Wiring it up this way avoid conflicts with Drupal's use of the 'close' option during dialog creation.
238      // This should have the signature `function (event, ui){}`, though notably `ui` will always be empty (it's only there for consistency).
239      // NOTE - Use this instead of using `options.dialogOptions.close`.
240      onClose: null,
241
242      /**
243       * Event handler that listens for the Drupal dialog JS 'dialog:afterclose' event handler to fire, which will be after it closes the dialog.
244       * Its signature is: `(event, dialog, $element)`
245       *  event {event object}
246       *    The custom event object.
247       *  dialog {Drupal dialog object}
248       *    The Drupal dialog object. Note that this may or may not have our added properties on it,
249       *    depending on if it was recreated by Drupal or not. If we programmatically close the dialog, we'll likely
250       *    have them, but if the user uses some built-in dialog functionality such as the close X or clicks off the
251       *    side (for non-modal dialogs) then Drupal will likely re-create the object and we'll have to rebuild or
252       *    get the content element ID and/or unique dialog ID off the dialog element itself, either from its attributes
253       *    or out of the jQuery 'data' storage of our "dialogResult" object.
254       *    Also, for the `onAfterClose` event, the `returnValue` property of this object will be set (if one was provided).
255       *  $element (jQuery object)
256       *    The content element of the dialog as a jQuery object. We should be able to get any
257       *    necessary specifics about the dialog starting from this.
258       */
259      onAfterClose: null,
260
261      // These are the options that get used by jQuery UI's Dialog Widget
262      // Ref: https://api.jqueryui.com/dialog
263      dialogOptions: {
264        title: null, // jQuery UI Dialog default is `null`
265        minHeight: 150, // jQuery UI Dialog default is 150
266        width: 300, // jQuery UI Dialog default is 300
267
268        // Ref for other classes you can use: https://api.jqueryui.com/dialog/#theming
269        classes: {
270          // NOTE - "trackside-ui-dialog" is also used down in the `removeDialogManually()` method.
271          "ui-dialog": "trackside-ui-dialog",
272        },
273      }
274    },
275    _createAndShowDialog: function(content, options) {
276      // Ensure that the dialog method we use has been loaded
277      if (!Drupal.dialog)
278      {
279        // Fallback to just showing an built-in alert - even though it'll probably be HTML - so that at least
280        // the user can kinda see the content.
281        // If it's a jQuery object (has an `html` function), then get the underlying HTML.
282        alert(typeof content === "object" && 'html' in content ? content.html() : content);
283
284        console.error("`Drupal.dialog()` doesn't exist. Showing content via `alert()` as a fallback.");
285        console.debug(content);
286        console.debug(options);
287
288        return null;
289      }
290
291      // We always wrap any content in an outer single element to prevent `Drupal.dialog()`
292      // from creating a dialog for each top-level element if there are multiple.
293      // This will get turned into a jQuery element by `Drupal.dialog()` anyways,
294      // so we do it now (if it's not already) so we can work with it below after the dialog is added to the DOM.
295      const contentElement = $("<div></div>").append(content);
296      const contentElementID = options.outerContentElementID && outerContentElementID.outerContentElementID.trim().length
297        ? outerContentElementID.trim()
298        : ("trackside-ui-dialog-content-" + options.dialogUniqueID);
299
300      contentElement.attr("id", contentElementID);
301      contentElement.attr("data-dialog-unique-id", options.dialogUniqueID);
302
303      // Drupal's JS uses the jQuery UI Dialog Widget under the hood.
304      // Ref: https://api.jqueryui.com/dialog
305      // Ref: /web/core/misc/dialog/dialog.js
306      const drupalDialog = Drupal.dialog(contentElement, options.dialogOptions);
307
308      // NOTE - These properties we add to the Drupal dialog object might not exist while the dialog's chain of "close" functions
309      // is being processed, as Drupal's JS might recreate the dialog object from the close event's target element.
310      // They should always be available from the content element's attributes or the "dialogResult" jQuery data object on it.
311
312      // Allow matching by unique ID on the dialog object directly
313      drupalDialog.dialogUniqueID = options.dialogUniqueID;
314
315      // Add the ID of the content element (which will end up as the `.ui-dialog-content` element inside the dialog)
316      // to the object so we can accurately access it later in the DOM if we need to.
317      drupalDialog.contentElementID = contentElementID;
318
319      if (options.showAsModal)
320      {
321        // Show as a dialog with a tinted backdrop that prevents interaction with the page
322        drupalDialog.showModal();
323      }
324      else
325      {
326        // Show as a normal dialog
327        drupalDialog.show();
328      }
329
330      // Fire a custom event to indicate that we've shown the dialog and to allow any child dialog handler
331      // to get ahold of the dialog object and the final objects that built it.
332      $(window).trigger('trackside-base-dialog:after-show', [drupalDialog, contentElement, options]);
333
334      return drupalDialog;
335    },
336    /**
337     * Shows content in a Drupal dialog with some extra options and handling built around it.
338     * This is mostly meant to be used by "child" dialog classes, but there's no reason
339     * it can't be used to just show content dialogs directly as well.
340     *
341     * @param {string|jQuery} content The HTML content to show in the dialog, either as an HTML string or jQuery object.
342     * This content will be automatically wrapped in a `div` element, which will then have classes (such as `.ui-dialog-content`)
343     * and inline-styles (such as widths and heights) applied to it by the dialog creator.
344     * Reference the available options for putting CSS classes or setting the 'id' attribute of this outer content element.
345     * @param {object} options The options for building/displaying the dialog. The top-level options are specific to this handler.
346     * The 'dialogOptions' property is forwarded to Drupal's dialog builder, who then forwards it to jQuery UI's Dialog Widget.
347     * Any classes passed to 'dialogOptions.classes' will have their list of CSS classes properly combined with the CSS classes
348     * of the default options. "Child" dialog classes can (and are encouraged to) perform the same operation by
349     * using `General.combineCssClassObjectProperties()` before passing their built-up options into this function.
350     * Ref (Drupal):    /web/core/misc/dialog/dialog.js
351     * Ref (jQuery UI): https://api.jqueryui.com/dialog
352     * @returns {object} An object with feedback about how the dialog was shown and some helper methods to work with the dialog.
353     */
354    show: function(content = "", options = {}) {
355      options = General.combineDialogOptions(BaseDialog.defaultOptions, options);
356
357      let results = {
358        dialogUniqueID: options.dialogUniqueID || General.getUniqueID(),
359        contentElementID: "",
360        didUseDelay: false,
361        timeoutID: null,
362        drupalDialog: null,
363
364        // Will be (re)defined below once we show the dialog
365        close: () => { console.error("The `close` helper doesn't get defined until after we show the dialog."); },
366
367        // A shorthand function to the content element for use once the dialog is displayed / in the DOM.
368        getContentElement: function() {
369          return results.contentElementID ? $("#" + results.contentElementID) : null;
370        },
371
372        // Update the dialog's options using an `options` object to set one or more options.
373        // Example: { width: 600, height: 200 };
374        // Ref: https://api.jqueryui.com/dialog/#method-option
375        setDialogOptions: function(options) {
376          if (typeof options !== "object")
377          {
378            console.error('The `options` parameter must be an object. Not updating options and exiting early.');
379
380            return;
381          }
382
383          const contentElement = results.getContentElement();
384
385          if (!contentElement || contentElement.length === 0)
386          {
387            console.warn("Failed to find content element of dialog to set its dialog options");
388
389            return;
390          }
391
392          contentElement.dialog("option", options);
393        },
394
395        // Refresh the dialog's using its existing option values.
396        // This is useful after adjusting the dialog in some way, such as adding content and needing the dialog to re-center itself.
397        refreshDialog: function() {
398          const contentElement = results.getContentElement();
399
400          if (!contentElement || contentElement.length === 0)
401          {
402            console.warn("Failed to find content element of dialog to set its dialog options");
403
404            return;
405          }
406
407          contentElement.dialog("option", contentElement.dialog('option'));
408        },
409      };
410
411      if (!options.dialogUniqueID)
412      {
413        // Put the unique ID in the options so we can match up the dialog object with the results
414        options.dialogUniqueID = results.dialogUniqueID;
415      }
416
417      // This uses jQuery UI's `beforeClose` event function directly
418      // This should have the signature `function (event, ui){}`, though `ui` will always be empty.
419      // If this event is cancelled, the dialog will not close.
420      if (options.onBeforeClose)
421      {
422        if (!options.dialogOptions)
423        {
424          options.dialogOptions = {};
425        }
426
427        const givenOnBeforeClose = options.onBeforeClose;
428        delete options.onBeforeClose;
429
430        // If the function property to give directly to jQuery UI was also given, we'll combine them.
431        if (options.dialogOptions.beforeClose)
432        {
433          const givenBeforeClose = options.dialogOptions.beforeClose;
434
435          options.dialogOptions.beforeClose = function(event, ui) {
436            if (givenOnBeforeClose(event, ui) === false)
437            {
438              return false;
439            }
440
441            if (givenBeforeClose(event, ui) === false)
442            {
443              return false;
444            }
445          };
446        }
447        else
448        {
449          options.dialogOptions.beforeClose = givenOnBeforeClose;
450        }
451      }
452
453      let givenOnClose = null;
454      let givenClose = null;
455
456      // This will listen for jQuery UI's `close` custom event directly (AKA 'dialogclose').
457      // This should have the signature `function (event, ui){}`, though `ui` will always be empty.
458      if (options.onClose)
459      {
460        givenOnClose = options.onClose;
461        delete options.onClose;
462      }
463
464      // This intercepts an attempt to assign to the `close` function option and removes it so we don't overwrite Drupal's JS.
465      // This function will instead be wired up the same as `options.onClose` so as to prevent breakage,
466      // and that option should be used instead of this one.
467      if (options.dialogOptions && 'close' in options.dialogOptions)
468      {
469        console.warn('The `close` property of `options.dialogOptions` should NOT be specified directly, '
470          + 'as it will overwrite Drupal JS functionality and behavior. Use `options.onClose` and/or `options.onAfterClose` instead. '
471          + 'The given `close` function will be wired up the same as `options.onClose`, instead.');
472
473        givenClose = options.dialogOptions.close;
474        delete options.dialogOptions.close;
475      }
476
477      // Wire up listener for our `onAfterClose` handler, which listens for Drupal dialog's custom "after close" event to fire.
478      // You might want to use this instead of `onClose` for the different parameters provided to it, though it's probably slightly
479      // less reliable in the case when anything goes awry with Drupal's dialogs and all their interwoven and injected JS.
480      if (options.onAfterClose)
481      {
482        const onAfterClose = options.onAfterClose;
483        delete options.onAfterClose;
484
485        const onAfterCloseHandler = function(e, dialog, $element) {
486          // Confirm that this handler is for this specific dialog
487          if (results.dialogUniqueID == $element.attr("data-dialog-unique-id"))
488          {
489            // Remove our specific listener now that it's fired.
490            $(window).off('dialog:afterclose', onAfterCloseHandler);
491
492            // NOTE - The `value` property of `dialog` will be set when this is called.
493            onAfterClose(e, dialog, $element);
494          }
495        };
496
497        $(window).on('dialog:afterclose', onAfterCloseHandler);
498      }
499
500      const finishDialog = function() {
501        results.drupalDialog = BaseDialog._createAndShowDialog(content, options);
502
503        // Set up a helper function on our results object to close the dialog programmatically that already has
504        // the Drupal dialog object we need to reference in order to close the dialog embedded in it.
505        results.close = function(value = null) {
506          if (results.drupalDialog)
507          {
508            BaseDialog.close(results.drupalDialog, value);
509
510            results.drupalDialog = null;
511          }
512        };
513
514        // Hold onto the identifier of the content element since the Drupal dialog object
515        // gets recreated every time the dialog is opened or closed.
516        results.contentElementID = results.drupalDialog.contentElementID;
517
518        // Wire up our close event handler directly to the jQuery UI event so we avoid overwriting Drupal's use of the 'close' function property.
519        if (givenOnClose)
520        {
521          results.getContentElement().on("dialogclose", givenOnClose);
522        }
523
524        // Instead of trying to pass the 'close' event function option through to the Drupal dialog creation (which would overwrite their use of it),
525        // we will instead wire up the close event handler directly to the jQuery UI event.
526        if (givenClose)
527        {
528          results.getContentElement().on("dialogclose", givenClose);
529        }
530
531        // Attach our results object - including the Drupal dialog object with our added properties - to the content element of the dialog
532        // so we can more easily fetch it later.
533        results.getContentElement().data("dialogResults", results);
534      };
535
536      // When this option is enabled (not default), we won't show the dialog immediately.
537      // This is useful for dialogs that indicate loading, so we can wait for a short delay to try to avoid flashes of the loading overlay
538      // for requests that are both non-critical that we lock down the screen during and tend to respond quickly.
539      // If we attempt to close the dialog before this delay ends, then we'll avoid showing it altogether.
540      // We will also delay showing the dialog if the dialog method from Drupal's utilities isn't loaded yet.
541      if (options.useDelay || (options.useDelayOnPageLoad && !Drupal.dialog))
542      {
543        results.didUseDelay = true;
544
545        results.timeoutID = setTimeout(function(){
546          // We update the results object even though we've already returned it, just in case.
547          results.timeoutID = null;
548          finishDialog();
549        }, options.delayLength);
550      }
551      // When not using a delay (the default), just create and show the dialog immediately.
552      else
553      {
554        finishDialog();
555      }
556
557      return results;
558    },
559    /**
560     * Closes a displayed Drupal dialog.
561     *
562     * @param {string|jQuery|object} drupalDialog The object given back from Drupal.dialog, a selector for the dialog's content element,
563     *  or a jQuery object of the dialog's content element.
564     * @param {any} value (optional) A value to forward into the closing-related custom event handlers. See comments in the method.
565     */
566    close: function(drupalDialog = null, value = null) {
567      if (!drupalDialog)
568      {
569        return;
570      }
571
572      // Reconstruct the Drupal dialog object from the content element of the dialog, just like Drupal's JS does in its `close` event handler.
573      // The content element should have the "ui-dialog-content" 
573class.
574      if (drupalDialog instanceof jQuery || typeof drupalDialog === "string")
575      {
576        try
577        {
578          drupalDialog = Drupal.dialog(drupalDialog);
579        }
580        catch (error)
581        {
582          console.error(error);
583
584          return;
585        }
586      }
587
588      // Close and pitch the dialog, if there's one showing
589      if (drupalDialog)
590      {
591        try
592        {
593          // Fire a custom event to indicate that we're about to close the dialog and to allow any child dialog handler
594          // to get a final notification with the dialog object and the final "close value".
595          // Note that it's probably more useful to use the `onBeforeClose` option when creating the dialog - which uses
596          // jQuery UI's custom events - as those are more guaranteed to fire than these, which are more for the use
597          // by this utility script.
598          $(window).trigger('trackside-base-dialog:before-close', [drupalDialog, value]);
599        }
600        catch (error)
601        {
602          console.error(error);
603        }
604
605        try
606        {
607          // The `value` param here gets set as the `returnValue` property of the first parameter of the object passed
608          // to the "dialog:afterclose" custom event handler that gets fired on the `window` after the dialog closes,
609          // in case you need that for something.
610          // Ref: /web/core/misc/dialog/dialog.js
611          drupalDialog.close(value);
612        }
613        catch (error)
614        {
615          console.error(error);
616
617          BaseDialog.removeDialogManually(drupalDialog.contentElementID || "");
618        }
619
620        try
621        {
622          // Fire a custom event to indicate that we've closed the dialog and to allow any child dialog handler
623          // to get a final notification with the dialog object and the final "close value".
624          // Note that it's probably more useful to use the `onAfterClose` option when creating the dialog - which uses
625          // Drupal's custom dialog events - as those are more guaranteed to fire than these, which are more for the use
626          // by this utility script.
627          $(window).trigger('trackside-base-dialog:after-close', [drupalDialog, value]);
628        }
629        catch (error)
630        {
631          console.error(error);
632        }
633      }
634    },
635    /**
636     * Attempts to remove/cleanup a dialog and its related elements manually.
637     * Should only be used when closing it normally fails.
638     * Can look for a specific dialog content ID, or the only dialog shown.
639     *
640     * @param {string} dialogContentElementID The ID attribute of the content element of the dialog,
641     *  or a blank string to just look for the only dialog currently shown.
642     *  The content element typically has the `ui-dialog-content` class on it.
643     */
644    removeDialogManually: function(dialogContentElementID = "") {
645      try
646      {
647        // Attempt to locate the specific dialog or the only dialog.
648        let contentElement = null;
649        let didQueryByID = false;
650
651        if (dialogContentElementID && dialogContentElementID.trim().length > 0)
652        {
653          contentElement = $("#" + dialogContentElementID);
654          didQueryByID = true;
655        }
656
657        // If we looked by ID, then don't look for just any dialog.
658        // Otherwise, find the single dialog shown (if that's the case).
659        if (!didQueryByID && (!contentElement || contentElement.length === 0))
660        {
661          const contentElements = $(".ui-dialog-content");
662
663          if (contentElements.length !== 1)
664          {
665            return;
666          }
667        }
668
669        if (!contentElement || contentElement.length !== 1)
670        {
671          return;
672        }
673
674        // Attempt to manually pitch the dialog and its backdrop since closing normally failed
675        const contentParent = contentElement.parents(".ui-dialog.trackside-ui-dialog");
676
677        if (!contentParent.length)
678        {
679          return;
680        }
681
682        const overlay = contentParent.next(".ui-widget-overlay");
683
684        if (overlay.length)
685        {
686          overlay.remove();
687        }
688
689        contentParent.remove();
690      }
691      catch (error)
692      {
693        console.error(error);
694      }
695    }
696  };
697
698  const LoadingDialog = Object.seal({
699    _timeoutID: null,
700    lottieAnimation: null,
701    lottieContainerStorage: null,
702    drupalDialog: null,
703    enforcedOptions: {
704      // Display modally, blocking interaction with other page elements
705      showAsModal: true,
706
707      dialogOptions: {
708        // Prevent ESC keypress from closing dialog
709        closeOnEscape: false,
710
711        // This is the smallest we ever want the loading to be
712        minWidth: 200,
713
714        // These classes would get combined into any provided class options even if they were in the `defaultOptions`,
715        // but it's more clear that they're "enforced" if we put them here.
716        classes: {
717          "ui-dialog": "trackside-loading-dialog",
718          "ui-dialog-titlebar": "d-none", // hide the titlebar, which includes its close button
719        },
720      }
721    },
722    defaultLottieDialogOptions: {
723      minHeight: 200,
724      classes: {
725        "ui-dialog": "trackside-lottie-speedometer",
726      }
727    },
728    defaultOptions: {
729      useDelayOnPageLoad: true,
730
731      // If this get assigned values, it'll be used instead of the loading type's container's default "classes" property.
732      containerClasses: "",
733
734      lottieTemplate: "<div id='trackside-lottie-loading-container' class='{{container_
734classes}}'></div>",
735      lottieAnimationName: "loading-speedometer",
736      lottieFilePath: "/modules/custom/trackside/json/lottie_speedometer.json",
737      // Note that we have specific styling for the speedometer container and some of the animation's specific parts in utility.css
738      lottieContainerClasses: "trackside-lottie-speedometer-container",
739      lottieContainerStorageID: "trackside-lottie-loading-container-storage",
740
741      textTemplate: "<h3 class='{{container_classes}}'><i class='{{spinner_classes}}'></i>{{message_text}}</h3>",
742      textContainerClasses: "text-center my-1",
743      spinnerClasses: "fas fa-spinner fa-spin",
744      message: "",
745
746      imageTemplate: "<div class='{{container_classes}}'><img class='{{image_classes}}' src='{{image_source}}' /></div>",
747      imageContainerClasses: "d-flex flex-column justify-content-center align-items-center",
748      imageClasses: "",
749      imageSource: "",
750      imageNonGifClass: "opacity-pulsing",
751      isImageGif: null, // set to `true` or `false` manually, or if not set will look for ".gif" in the `imageSource` value
752
753      // Used tp calculate whether to use the `width` or `minWidth` value of the `dialogOptions` for the width of the loading dialog
754      widthBreakpoint: 600,
755
756      // These are the options that get used by jQuery UI's Dialog Widget
757      // Ref: https://api.jqueryui.com/dialog
758      dialogOptions: {
759        // This will only be used for the width if the browser window is larger than this (plus a small buffer)
760        width: 300, // jQuery UI Dialog default is 300
761        minHeight: 75, // jQuery UI Dialog default is 150
762        classes: {},
763      }
764    },
765
766    /**
767     * Calculates a reasonable dialog width for a loading dialog to be without being wider than the current browser window.
768     *
769     * @param {number} breakpoint [default: 600] The window width at which the loading dialog will switch to its larger width.
770     *    When the browser window is smaller than this, the loading dialog will use its smaller width.
771     * @return {number} The dialog width.
772     */
773    calculateDialogWidth: (breakpoint = 600, largeWidth = 300, smallWidth = 200) => {
774      const minHorizontalSpacing = 30;
775
776      return window.innerWidth >= breakpoint && window.innerWidth >= largeWidth + minHorizontalSpacing ? largeWidth : smallWidth;
777    },
778
779    /**
780     * Shows a modal loading dialog until the `close()` function is called.
781     *
782     * @param {object|string} options (optional) The options object. Reference the `defaultOptions` property
783     *    for available properties to provide values for. If a non-object is passed in (typically a string)
784     *    instead of an object, we'll assume it's a loading message and show the spinner with text template
785     *    instead of the default image template.
786     * @return {object} An object with results properties from `BaseDialog.show()`. Note that the results object
787     *    may have properties that haven't been filled in yet if a delay ended up being was used.
788     */
789    show: function(options = {}) {
790      if (LoadingDialog._timeoutID)
791      {
792        clearTimeout(LoadingDialog._timeoutID);
793        LoadingDialog._timeoutID = null;
794      }
795
796      if (LoadingDialog.drupalDialog)
797      {
798        LoadingDialog.close();
799      }
800
801      // If a non-object is passed in as the `options` parameter,
802      // assume it is a loading message to display.
803      if (options && typeof options !== "object")
804      {
805        options = { message: options };
806      }
807
808      // Ensures this is a string (or converts to one) and also puts whitespace between the spinner and the text
809      options.message = "message" in options && options.message ? (" " + options.message) : LoadingDialog.defaultOptions.message;
810
811      // Determine the loading dialog type/template we're using
812      const isText = options.message.trim().length > 0;
813      const isImage = options.imageSource && options.imageSource.trim().length > 0;
814      const isLottie = !isText && !isImage;
815
816      let defaultOptions = LoadingDialog.defaultOptions;
817
818      if (isLottie)
819      {
820        // Apply our Lottie-specific default options overtop of the base default ones
821        defaultOptions = General.combineDialogOptions(defaultOptions, { dialogOptions: Loa
821dingDialog.defaultLottieDialogOptions });
822      }
823
824      options = General.combineDialogOptions(defaultOptions, options);
825      options = General.combineDialogOptions(options, LoadingDialog.enforcedOptions);
826
827      if (isImage)
828      {
829        if (options.isImageGif === null)
830        {
831          const gifCheckRegex = /.+\.gif(\?[^?]*)?$/gi;
832          options.isImageGif = gifCheckRegex.test(options.imageSource);
833        }
834
835        // If we're using an image, but it's not a GIF, then we'll add a class to animate it.
836        if (!options.isImageGif)
837        {
838          options.imageClasses += " " + options.imageNonGifClass;
839        }
840      }
841
842      options.dialogOptions.width = LoadingDialog.calculateDialogWidth(
843        options.widthBreakpoint, options.dialogOptions.width, options.dialogOptions.minWidth
844      );
845      options.dialogUniqueID = General.getUniqueID();
846
847      let template = options.lottieTemplate;
848
849      if (isImage)
850      {
851        template = options.imageTemplate;
852      }
853      else if (isText)
854      {
855        template = options.textTemplate;
856      }
857
858      let containerClasses = options.containerClasses;
859
860      if (!containerClasses)
861      {
862        if (isLottie)
863        {
864          containerClasses = options.lottieContainerClasses;
865        }
866        else if (isImage)
867        {
868          containerClasses = options.imageContainerClasses;
869        }
870        else if (isText)
871        {
872          containerClasses = options.textContainerClasses;
873        }
874      }
875
876      // Drupal's JS uses the jQuery UI Dialog Widget under the hood.
877      // Ref: https://api.jqueryui.com/dialog
878      // Ref: /web/core/misc/dialog/dialog.js
879      const content = template
880        .replace(/\{\{container_classes\}\}/g, containerClasses)
881        .replace(/\{\{spinner_classes\}\}/g, options.spinnerClasses)
882        .replace(/\{\{message_text\}\}/g, options.message)
883        .replace(/\{\{image_classes\}\}/g, options.imageClasses)
884        .replace(/\{\{image_source\}\}/g, options.imageSource);
885
886      const afterShowCallback = function(event, handlerDrupalDialog, handlerContentElement, handlerOptions) {
887        if (!handlerOptions || !('dialogUniqueID' in handlerOptions) || handlerOptions.dialogUniqueID !== options.dialogUniqueID)
888        {
889          return;
890        }
891
892        $(window).off('trackside-base-dialog:after-show', afterShowCallback);
893
894        // Hold onto the now-showing dialog
895        LoadingDialog.drupalDialog = handlerDrupalDialog;
896        LoadingDialog._timeoutID = null;
897
898        if (isLottie)
899        {
900          if (!lottie)
901          {
902            console.error("`lottie` Not Found!");
903
904            return;
905          }
906
907          LoadingDialog.drupalDialog.lottieContainerStorageID = options.lottieContainerStorageID;
908
909          if (LoadingDialog.lottieAnimation && (!LoadingDialog.lottieAnimation.name || LoadingDialog.lottieAnimation.name !== options.lottieAnimationName))
910          {
911            try
912            {
913              LoadingDialog.lottieAnimation.destroy();
914            }
915            catch (error)
916            {
917              console.warn(error);
918            }
919
920            LoadingDialog.lottieAnimation = null;
921          }
922
923          // If we have an existing animation (that matches the one we were about to create) and a place it was stored,
924          // then re-use that one instead of init-ing a whole new one.
925          if (LoadingDialog.lottieAnimation && LoadingDialog.lottieContainerStorage && LoadingDialog.lottieContainerStorage.length)
926          {
927            // Move the content in the storage container into the fresh loading container in the dialog
928            $("#trackside-lottie-loading-container").append(LoadingDialog.lottieContainerStorage.children());
929
930            // Start the animation again
931            LoadingDialog.lottieAnimation.play();
932          }
933          else
934          {
935            // Create a new animation and hold onto a reference to it
936            LoadingDialog.lottieAnimation = lottie.loadAnimation({
937              container: $("#trackside-lottie-loading-container")[0], // Required
938              path: options.lottieFilePath, // Required
939              // Supported features for each renderer: https://github.com/airbnb/lottie-web/wiki/Features
940              // Per their doc: 'svg' has the most features, but 'html' can be more performant and supports 3d layers.
941              // Per testing with the speedometer animation: 'svg' is more performant.
942              // Check the 'trackside.utility' YML library entry for which "lottie" library JS we're loading to determine
943              // which renderer(s) are available to use.
944              renderer: 'svg', // Required - 'svg' or 'canvas' or 'html'
945              loop: true, // Optional
946              autoplay: true, // Optional
947              name: options.lottieAnimationName, // Optional - name for future reference
948            });
949
950            // If a container storage already exists, clear it out
951            if (LoadingDialog.lottieContainerStorage !== null)
952            {
953              try
954              {
955                LoadingDialog.lottieContainerStorage.remove();
956              }
957              catch (error)
958              {
959                console.warn(error);
960              }
961
962              LoadingDialog.lottieContainerStorage = null;
963            }
964          }
965        }
966      };
967
968      // Listen for the dialog to be shown so we can get the Drupal dialog object whether it was shown immediately or on a delay.
969      $(window).on('trackside-base-dialog:after-show', afterShowCallback);
970
971      const baseShowResults = BaseDialog.show(content, options);
972
973      if (baseShowResults.didUseDelay)
974      {
975        LoadingDialog._timeoutID = baseShowResults.timeoutID;
976      }
977
978      // NOTE - These results may have properties that haven't been filled in yet if a delay was used.
979      return baseShowResults;
980    },
981
982    /**
983     * Closes the display loading dialog, if any.
984     *
985     * @param {any} value (optional) A value to forward into the "dialog:afterclose" custom event handler.
986     * See comments in the `BaseDialog.close()` method.
987     */
988    close: function(value = null) {
989      // If we close the loading dialog before the delay is over, then don't even bother showing the loading dialog.
990      if (LoadingDialog._timeoutID)
991      {
992        clearTimeout(LoadingDialog._timeoutID);
993        LoadingDialog._timeoutID = null;
994      }
995
996      if (!LoadingDialog.drupalDialog)
997      {
998        LoadingDialog.drupalDialog = null;
999
1000        return;
1001      }
1002
1003      // If we have a lottie animation, we'll attempt to move it to a hidden storage location elsewhere in the DOM
1004      // - before we pitch the current loading dialog - so we don't have to reload it every time we show a loading dialog.
1005      if (LoadingDialog.lottieAnimation)
1006      {
1007        const lottieContainer = $("#trackside-lottie-loading-container");
1008
1009        if (lottieContainer.length !== 1)
1010        {
1011          try
1012          {
1013            LoadingDialog.lottieAnimation.destroy();
1014          }
1015          catch (error)
1016          {
1017            console.warn(error);
1018          }
1019
1020          LoadingDialog.lottieAnimation = null;
1021        }
1022        else
1023        {
1024          // Stop the animation from running while stored
1025          LoadingDialog.lottieAnimation.stop();
1026
1027          // If we don't have a matching storage element already, then create a new one and add it to the DOM
1028          if (!LoadingDialog.lottieContainerStorage || LoadingDialog.lottieContainerStorage.attr("id") !== LoadingDialog.drupalDialog.lottieContainerStorageID)
1029          {
1030            const containerStorageID = LoadingDialog.drupalDialog.lottieContainerStorageID;
1031            $(document.body).append("<div id='" + containerStorageID + "'></div>");
1032            LoadingDialog.lottieContainerStorage = $("#" + containerStorageID);
1033          }
1034
1035          // Move the child elements of the lottie container across the DOM: out of the dialog and into the storage element.
1036          // It's important we only store the children and not the container itself as we'd likely end up with a container with that ID in storage
1037          // and a container with that ID in the loading dialog (from the template) that we'd be trying to load the animation elements back into.
1038          LoadingDialog.lottieContainerStorage.append(lottieContainer.children());
1039        }
1040      }
1041
1042      BaseDialog.close(LoadingDialog.drupalDialog, value);
1043      LoadingDialog.drupalDialog = null;
1044    }
1045  });
1046
1047  // There should always be a matching set of default options for each of these initialized into `AlertDialog.defaultTypeOptions`
1048  const AlertType = Object.freeze({
1049    SUCCESS: Symbol("success"),
1050    WARNING: Symbol("warning"),
1051    ERROR: Symbol("error"),
1052    INFO: Symbol("info"),
1053    PRIMARY: Symbol("primary"), // this is the default and its values are also already built into `AlertDialog.defaultOptions`
1054    SECONDARY: Symbol("secondary"),
1055    LIGHT: Symbol("light"),
1056    DARK: Symbol("dark"),
1057  });
1058
1059  const AlertDialog = Object.seal({
1060    enforcedOptions: {
1061      // Display modally, blocking interaction with other page elements
1062      showAsModal: true,
1063
1064      dialogOptions: {
1065        // Prevent ESC keypress from closing dialog
1066        closeOnEscape: false,
1067
1068        // This is the smallest we ever want the alert to be
1069        minWidth: 300,
1070
1071        // These classes would get combined into any provided class options even if they were in the `defaultOptions`,
1072        // but it's more clear that they're "enforced" if we put them here.
1073        classes: {
1074          "ui-dialog": "trackside-alert-dialog",
1075        },
1076      }
1077    },
1078    defaultOptions: {
1079      containerTemplate: '<div class="{{container_classes}}" style="{{container_styles}}">
1079{{container_content}}</div>',
1080      containerClasses: "",
1081      containerStyles: "",
1082
1083      // HTML content to show before or after the alert element
1084      beforeAlert: "",
1085      afterAlert: "",
1086
1087      // Provide objects in this array matching this signature to have them built into alert elements and placed after the main alert element in the dialog.
1088      // Only the 'message' property is required.
1089      // { message: "", options: {}, alertType: some AlertType }
1090      additionalAlerts: [],
1091
1092      // Used to construct alert elements
1093      alertTemplate: '<div class="{{alert_classes}}" style="{{alert_styles}}"><{{header_element}} class="{{header_classes}}">{{header_text}}</{{header_element}}>{{header_separator}}{{before_message}}{{before_separator}}{{message_text}}{{after_separator}}{{after_message}}</div>',
1094      alertClasses: "alert alert-primary",
1095      alertStyles: "",
1096
1097      headerElement: "strong",
1098      headerClasses: "text-primary",
1099      headerText: "Notice",
1100
1101      beforeMessage: "",
1102      afterMessage: "",
1103
1104      // These separator strings are only added to the message if their associated string has content
1105      headerSeparator: "<br>",
1106      beforeMessageSeparator: "<br><br>",
1107      afterMessageSeparator: "<br><br>",
1108
1109      // NOTE - See BaseDialog's `defaultOptions` for more options, such as event handlers.
1110
1111      // These are the options that get used by jQuery UI's Dialog Widget
1112      // Ref: https://api.jqueryui.com/dialog
1113      dialogOptions: {
1114        width: 800, // jQuery UI Dialog default is 300
1115        maxWidth: 800, // jQuery UI Dialog default is `false`
1116        minHeight: 75, // jQuery UI Dialog default is 150
1117        classes: {},
1118      }
1119    },
1120
1121    defaultTypeOptions: new Map([
1122      [AlertType.ERROR, {
1123        alertClasses: "alert alert-danger",
1124        headerClasses: "text-danger",
1125        headerText: "Error!",
1126      }],
1127      [AlertType.WARNING, {
1128        alertClasses: "alert alert-warning",
1129        headerClasses: "text-warning",
1130        headerText: "Warning!",
1131      }],
1132      [AlertType.SUCCESS, {
1133        alertClasses: "alert alert-success",
1134        headerClasses: "text-success",
1135        headerText: "Success!",
1136      }],
1137      [AlertType.INFO, {
1138        alertClasses: "alert alert-info",
1139        headerClasses: "text-info",
1140      }],
1141      [AlertType.PRIMARY, {
1142        alertClasses: "alert alert-primary",
1143        headerClasses: "text-primary",
1144      }],
1145      [AlertType.SECONDARY, {
1146        alertClasses: "alert alert-secondary",
1147        headerClasses: "text-secondary",
1148      }],
1149      [AlertType.LIGHT, {
1150        alertClasses: "alert alert-light bg-dark",
1151        headerClasses: "text-light",
1152      }],
1153      [AlertType.DARK, {
1154        alertClasses: "alert alert-dark",
1155        headerClasses: "text-dark",
1156      }],
1157    ]),
1158
1159    /**
1160     * Calculates a reasonable dialog width for an alert dialog to be without being wider than the current browser window.
1161     *
1162     * @param {number} maxWidth [default: 800] The maximum width the alert dialog should be.
1163     *    If this value is smaller than `midWidth` it will be ignored.
1164     * @param {number} midWidth [default: 600] For windows larger that this width plus some horizontal spacing,
1165     *    this will be the dialog width. For windows smaller than this width plus some horizontal spacing,
1166     *    the dialog width will be set based on the window size in order to safely maximize width.
1167     * @return {number} The dialog width.
1168     */
1169    calculateDialogWidth: (maxWidth = 800, midWidth = 600) => {
1170      const minHorizontalSpacing = 30;
1171      const horizontalSpacing = minHorizontalSpacing + 20;
1172
1173      if (maxWidth > midWidth && window.innerWidth >= maxWidth + horizontalSpacing)
1174      {
1175        return maxWidth;
1176      }
1177
1178      if (window.innerWidth < midWidth + horizontalSpacing)
1179      {
1180        return window.innerWidth - minHorizontalSpacing;
1181      }
1182
1183      return midWidth;
1184    },
1185
1186    // Builds just an `.alert` element. Can be used for extra alerts for the dialog or to build alert HTML for within a page.
1187    buildAlertContent: function(message, options = {}, alertType = null) {
1188      // Since we only use top-level option properties here, we don't do a "deep" merge or need to use our helper functions
1189      // that do CSS class property string combining or anything.
1190      // Also, even though we have a default for the alert-type-options-fetching function, we don't want to merge that in
1191      // in case those options were already merged in before the options were passed into this function.
1192      // Besides, that's why we have the "default" alert type values built into the main default options already.
1193      options = $.extend({},
1194        AlertDialog.defaultOptions,
1195        alertType ? AlertDialog.defaultTypeOptions.get(alertType) : {},
1196        options || {}
1197      );
1198
1199      return options.alertTemplate
1200        .replace(/\{\{alert_classes\}\}/g, options.alertClasses)
1201        .replace(/\{\{alert_styles\}\}/g, options.alertStyles)
1202        .replace(/\{\{header_element\}\}/g, options.headerElement)
1203        .replace(/\{\{header_classes\}\}/g, options.headerClasses)
1204        .replace(/\{\{header_text\}\}/g, options.headerText)
1205        .replace(/\{\{header_separator\}\}/g, options.headerText === "" ? "" : options.headerSeparator)
1206        .replace(/\{\{before_message\}\}/g, options.beforeMessage)
1207        .replace(/\{\{before_separator\}\}/g, options.beforeMessage === "" ? "" : options.beforeMessageSeparator)
1208        .replace(/\{\{message_text\}\}/g, message)
1209        .replace(/\{\{after_separator\}\}/g, options.afterMessage === "" ? "" : options.afterMessageSeparator)
1210        .replace(/\{\{after_message\}\}/g, options.afterMessage);
1211    },
1212
1213    /**
1214     * Shows a modal alert dialog.
1215     *
1216     * @param {string} message This is the HTML content to place inside the alert.
1217     * @param {object} options The options object. Reference the `defaultOptions` property for available properties to provide values for.
1218     * @param {AlertType} The alert type to use default values from. See `AlertDialog.defaultTypeOptions` for the properties each `AlertType` is mapped to.
1219     * @return {object} An object with results properties from `BaseDialog.show()` and with a `close()` function injected into it
1220     *   to close the resulting dialog.
1221     */
1222    show: function(message, options = {}, alertType = null) {
1223      let typedDefaultOptions = AlertDialog.defaultOptions;
1224
1225      if (alertType)
1226      {
1227        const typeOptions = AlertDialog.defaultTypeOptions.get(alertType);
1228        typedDefaultOptions = General.combineDialogOptions(typedDefaultOptions, typeOptions);
1229      }
1230
1231      // Combine all the levels of options
1232      options = General.combineDialogOptions(typedDefaultOptions, options);
1233      options = General.combineDialogOptions(options, AlertDialog.enforcedOptions);
1234      options.dialogOptions.width = AlertDialog.calculateDialogWidth(options.dialogOptions.width);
1235      options.dialogUniqueID = General.getUniqueID();
1236
1237      if (options.dialogOptions.width > options.dialogOptions.maxWidth)
1238      {
1239        console.warn(`WARNING - options.dialogOptions.width (${options.dialogOptions.width}) > options.dialogOptions.maxWidth (${options.dialogOptions.maxWidth})`);
1240      }
1241
1242      // Since we already combined any alert "type" that may have been passed in into the options,
1243      // we don't pass it to this method.
1244      const mainAlertContent = AlertDialog.buildAlertContent(message, options);
1245      let additionalAlertContents = [];
1246
1247      if (options.additionalAlerts && options.additionalAlerts.length)
1248      {
1249        additionalAlertContents = options.additionalAlerts.map(optionsObject => optionsObject.message
1250          ? AlertDialog.buildAlertContent(optionsObject.message, optionsObject.options || {}, optionsObject.alertType || null)
1251          : ""
1252        );
1253      }
1254
1255      const content = options.containerTemplate
1256        .replace(/\{\{container_classes\}\}/g, options.containerClasses)
1257        .replace(/\{\{container_styles\}\}/g, options.containerStyles)
1258        .replace(/\{\{container_content\}\}/g, options.beforeAlert + mainAlertContent + additionalAlertContents.join("") + options.afterAlert);
1259
1260      return BaseDialog.show(content, options);
1261    },
1262
1263    // Shorthand methods for the most common alert uses
1264    showSuccess: function(message, options = {}) {
1265      return AlertDialog.show(message, options, AlertType.SUCCESS);
1266    },
1267    showWarning: function(message, options = {}) {
1268      return AlertDialog.show(message, options, AlertType.WARNING);
1269    },
1270    showError: function(message, options = {}) {
1271      return AlertDialog.show(message, options, AlertType.ERROR);
1272    },
1273    showInfo: function(message, options = {}) {
1274      return AlertDialog.show(message, options, AlertType.INFO);
1275    },
1276  });
1277
1278  // How we make everything accessible
1279  const _export = Object.freeze({
1280    alert: AlertDialog,
1281    AlertType: AlertType,
1282    dialog: BaseDialog,
1283    loading: LoadingDialog,
1284
1285    // General functions exported directly
1286    arrayToHtmlList: General.arrayToHtmlList,
1287    escapeRegExp: General.escapeRegExp,
1288    getAjaxFailureDetails: General.getAjaxFailureDetails,
1289    htmlSanitize: General.htmlSanitize,
1290  });
1291
1292  return _export;
1293}(jQuery, Drupal));

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.