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.