| 1 | /** |
| 2 | * Unified action loader system - shows loader overlay with optional toast notifications. |
| 3 | * Designed to be flexible and reusable for various long-running operations. |
| 4 | * |
| 5 | * Features: |
| 6 | * - Stacking multiple loaders - overlay stays single, but toasts can stack |
| 7 | * - Blocking and non-blocking modes |
| 8 | * - Stoppable or static toasts |
| 9 | * - Class-based handle system for fine-grained control |
| 10 | * |
| 11 | * @module action-loader |
| 12 | */ |
| 13 | |
| 14 | import { t } from './i18n.js'; |
| 15 | import { stopGeneration } from '../script.js'; |
| 16 | import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 17 | |
| 18 | /** |
| 19 | * Enum representing the toast display mode for the action loader. |
| 20 | * @readonly |
| 21 | * @enum {string} |
| 22 | */ |
| 23 | export const ActionLoaderToastMode = { |
| 24 | /** No toast is displayed */ |
| 25 | NONE: 'none', |
| 26 | /** Toast is displayed without stop button (non-interactable) */ |
| 27 | STATIC: 'static', |
| 28 | /** Toast is displayed with stop button (default) */ |
| 29 | STOPPABLE: 'stoppable', |
| 30 | }; |
| 31 | |
| 32 | /** |
| 33 | * @typedef {object} ActionLoaderOptions |
| 34 | * @property {boolean} [blocking=true] - Whether to show the blocking overlay. Set to false for non-blocking toast-only loaders. |
| 35 | * @property {ActionLoaderToastMode} [toastMode='stoppable'] - Toast display mode |
| 36 | * @property {string} [slug=null] - Unique slug for the loader to identify it easily via code or CSS |
| 37 | * @property {string} [message='Generating...'] - The message to display in the toast |
| 38 | * @property {string} [title] - Optional title for the toast notification |
| 39 | * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button |
| 40 | * @property {HTMLElement|string|null} [overlayContent=null] - Custom content for the overlay (replaces default spinner) |
| 41 | * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()` |
| 42 | * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped). |
| 43 | */ |
| 44 | |
| 45 | /** Counter for generating unique loader IDs */ |
| 46 | let loaderIdCounter = 0; |
| 47 | |
| 48 | /** @type {Set<ActionLoaderHandle>} Set of all active loader handles */ |
| 49 | const activeHandles = new Set(); |
| 50 | |
| 51 | /** |
| 52 | * Generates a unique loader ID. |
| 53 | * @returns {string} Unique loader ID |
| 54 | */ |
| 55 | function generateLoaderId() { |
| 56 | return `loader_${++loaderIdCounter}`; |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Checks if there are any active blocking loaders. |
| 61 | * @returns {boolean} True if at least one blocking loader is active |
| 62 | */ |
| 63 | function hasBlockingLoaders() { |
| 64 | for (const handle of activeHandles) { |
| 65 | if (handle.isBlocking && handle.isActive) { |
| 66 | return true; |
| 67 | } |
| 68 | } |
| 69 | return false; |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Class representing an action loader handle. |
| 74 | * Manages its own toast, stop handler, and lifecycle. |
| 75 | */ |
| 76 | export class ActionLoaderHandle { |
| 77 | /** |
| 78 | * A special empty handle that is already disposed. Useful as a default value to avoid null checks. |
| 79 | * Does not generate any id, toast, or overlay, and all its methods are no-ops. |
| 80 | * @type {ActionLoaderHandle} |
| 81 | */ |
| 82 | static get EMPTY() { |
| 83 | return new ActionLoaderHandle({ predisposed: true }); |
| 84 | } |
| 85 | |
| 86 | /** @type {string} Unique identifier for this handle */ |
| 87 | #id; |
| 88 | |
| 89 | /** @type {string|null} Unique slug for the loader */ |
| 90 | #slug = null; |
| 91 | |
| 92 | /** @type {JQuery<HTMLElement>|null} The toast element for this loader */ |
| 93 | #toast = null; |
| 94 | |
| 95 | /** @type {(() => void)|null} Custom stop handler */ |
| 96 | #onStop = null; |
| 97 | |
| 98 | /** @type {(() => void)|null} Custom hide handler */ |
| 99 | #onHide = null; |
| 100 | |
| 101 | /** @type {boolean} Whether this loader blocks the UI with an overlay */ |
| 102 | #blocking = true; |
| 103 | |
| 104 | /** @type {boolean} Whether this handle has been disposed */ |
| 105 | #disposed = false; |
| 106 | |
| 107 | /** |
| 108 | * Creates a new ActionLoaderHandle. |
| 109 | * @param {object} options - Configuration options |
| 110 | * @param {boolean} [options.blocking=true] - Whether to show blocking overlay |
| 111 | * @param {ActionLoaderToastMode} [options.toastMode] - Toast display mode |
| 112 | * @param {string|null} [options.slug] - Unique slug for the loader (to identify it easily via code or CSS) |
| 113 | * @param {string} [options.message='Generating...'] - Message to display in the toast |
| 114 | * @param {string} [options.title] - Title for the toast notification |
| 115 | * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button |
| 116 | * @param {boolean} [options.predisposed=false] - Whether this handle is already disposed (for special use) |
| 117 | * @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner) |
| 118 | * @param {(() => void)|null} [options.onStop] - Custom stop handler |
| 119 | * @param {(() => void)|null} [options.onHide] - Custom hide handler |
| 120 | */ |
| 121 | constructor({ |
| 122 | blocking = true, |
| 123 | toastMode = ActionLoaderToastMode.STOPPABLE, |
| 124 | slug = null, |
| 125 | message = t`Generating...`, |
| 126 | title = '', |
| 127 | stopTooltip = t`Stop`, |
| 128 | overlayContent = null, |
| 129 | onStop = null, |
| 130 | onHide = null, |
| 131 | predisposed = false, |
| 132 | } = {}) { |
| 133 | if (predisposed) { |
| 134 | this.#disposed = true; |
| 135 | return; |
| 136 | } |
| 137 | |
| 138 | this.#id = generateLoaderId(); |
| 139 | this.#slug = slug; |
| 140 | this.#blocking = blocking; |
| 141 | this.#onStop = onStop; |
| 142 | this.#onHide = onHide; |
| 143 | |
| 144 | // Warn if non-blocking loader has no toast - it won't be visible to the user |
| 145 | if (!blocking && toastMode === ActionLoaderToastMode.NONE && !overlayContent) { |
| 146 | console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.'); |
| 147 | } |
| 148 | |
| 149 | // Show the blocking loader overlay if this is the first blocking handle |
| 150 | if (blocking && !hasBlockingLoaders() && !isOverlayDisplayed()) { |
| 151 | showOverlay(overlayContent); |
| 152 | } |
| 153 | |
| 154 | // Register this handle |
| 155 | activeHandles.add(this); |
| 156 | |
| 157 | // Create toast if needed |
| 158 | if (toastMode !== ActionLoaderToastMode.NONE) { |
| 159 | this.#createToast(message, title, toastMode, stopTooltip); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Creates the toast element for this loader. |
| 165 | * @param {string} message - Message to display |
| 166 | * @param {string} title - Title for the toast |
| 167 | * @param {ActionLoaderToastMode} toastMode - Toast mode |
| 168 | * @param {string} stopTooltip - Tooltip for stop button |
| 169 | */ |
| 170 | #createToast(message, title, toastMode, stopTooltip) { |
| 171 | const toastContent = document.createElement('div'); |
| 172 | toastContent.className = 'action-loader-toast'; |
| 173 | |
| 174 | if (this.#slug) { |
| 175 | toastContent.dataset.slug = this.#slug; |
| 176 | } |
| 177 | toastContent.dataset.loaderId = this.#id; |
| 178 | toastContent.dataset.blocking = this.#blocking.toString(); |
| 179 | |
| 180 | const messageSpan = document.createElement('span'); |
| 181 | messageSpan.className = 'action-loader-message'; |
| 182 | messageSpan.textContent = message; |
| 183 | toastContent.appendChild(messageSpan); |
| 184 | |
| 185 | // Add stop button if mode is STOPPABLE |
| 186 | if (toastMode === ActionLoaderToastMode.STOPPABLE) { |
| 187 | const stopButton = document.createElement('i'); |
| 188 | stopButton.className = 'fa-solid fa-stop-circle action-loader-stop interactable'; |
| 189 | stopButton.title = stopTooltip; |
| 190 | stopButton.addEventListener('click', (e) => { |
| 191 | e.preventDefault(); |
| 192 | e.stopPropagation(); |
| 193 | this.stop(); |
| 194 | }); |
| 195 | toastContent.appendChild(stopButton); |
| 196 | } |
| 197 | |
| 198 | // Show toast with no timeout (sticky) |
| 199 | this.#toast = toastr.info($(toastContent), title, { |
| 200 | timeOut: 0, |
| 201 | extendedTimeOut: 0, |
| 202 | tapToDismiss: false, |
| 203 | escapeHtml: false, |
| 204 | }); |
| 205 | } |
| 206 | |
| 207 | /** |
| 208 | * Clears the toast element for this loader. |
| 209 | */ |
| 210 | #clearToast() { |
| 211 | if (this.#toast) { |
| 212 | toastr.clear(this.#toast, { force: true }); // Need to force as the toast might have focus/hover |
| 213 | this.#toast = null; |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | /** |
| 218 | * Disposes this handle, removing it from active handles and hiding overlay if last. |
| 219 | */ |
| 220 | async #dispose() { |
| 221 | if (this.#disposed) return; |
| 222 | this.#disposed = true; |
| 223 | |
| 224 | this.#clearToast(); |
| 225 | activeHandles.delete(this); |
| 226 | |
| 227 | // Hide the overlay if this was the last blocking handle |
| 228 | if (this.#blocking && !hasBlockingLoaders()) { |
| 229 | await hideOverlay(); |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * The unique identifier for this loader handle. |
| 235 | * @returns {string} |
| 236 | */ |
| 237 | get id() { |
| 238 | return this.#id; |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * The unique slug for this loader handle, used to identify it easily via code or CSS. |
| 243 | * @returns {string|null} |
| 244 | */ |
| 245 | get slug() { |
| 246 | return this.#slug; |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Whether this handle is still active (not disposed). |
| 251 | * @returns {boolean} |
| 252 | */ |
| 253 | get isActive() { |
| 254 | return !this.#disposed; |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Whether this loader blocks the UI with an overlay. |
| 259 | * @returns {boolean} |
| 260 | */ |
| 261 | get isBlocking() { |
| 262 | return this.#blocking; |
| 263 | } |
| 264 | |
| 265 | /** |
| 266 | * Triggers the stop action on this loader. |
| 267 | * Calls the custom onStop handler if provided, otherwise calls stopGeneration(). |
| 268 | * Then hides this loader. |
| 269 | */ |
| 270 | async stop() { |
| 271 | if (this.#disposed) return; |
| 272 | |
| 273 | // Call custom stop handler or default |
| 274 | if (this.#onStop) { |
| 275 | try { |
| 276 | await this.#onStop(); |
| 277 | } catch (e) { |
| 278 | console.error('Error executing onStop handler', e); |
| 279 | } |
| 280 | } else { |
| 281 | stopGeneration(); |
| 282 | } |
| 283 | |
| 284 | // Dispose without calling onHide (stop is different from hide) |
| 285 | await this.#dispose(); |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * Hides this loader and clears its toast. |
| 290 | * Calls the custom onHide handler if provided. |
| 291 | */ |
| 292 | async hide() { |
| 293 | if (this.#disposed) return; |
| 294 | |
| 295 | // Call custom hide handler if provided |
| 296 | if (this.#onHide) { |
| 297 | try { |
| 298 | await this.#onHide(); |
| 299 | } catch (e) { |
| 300 | console.error('Error executing onHide handler', e); |
| 301 | } |
| 302 | } |
| 303 | |
| 304 | await this.#dispose(); |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * Action loader utility API. |
| 310 | * Provides a convenient interface for showing and managing loading indicators. |
| 311 | * |
| 312 | * Read the functions documentation for more details. |
| 313 | * |
| 314 | * @example |
| 315 | * // Basic usage |
| 316 | * const handle = loader.show({ message: 'Loading...' }); |
| 317 | * await someOperation(); |
| 318 | * handle.hide(); |
| 319 | * |
| 320 | * @example |
| 321 | * // Non-blocking background task |
| 322 | * const handle = loader.show({ blocking: false, message: 'Processing...' }); |
| 323 | * |
| 324 | * @example |
| 325 | * // Hide all active loaders |
| 326 | * loader.hide(); |
| 327 | */ |
| 328 | export const loader = { |
| 329 | /** |
| 330 | * Shows an action loader with optional toast notification. |
| 331 | * Returns a handle to control the loader. |
| 332 | * @type {typeof showActionLoader} |
| 333 | */ |
| 334 | show: showActionLoader, |
| 335 | |
| 336 | /** |
| 337 | * Hides a specific loader by handle, or all loaders if no handle provided. |
| 338 | * @type {typeof hideActionLoader} |
| 339 | */ |
| 340 | hide: hideActionLoader, |
| 341 | |
| 342 | /** |
| 343 | * Gets all currently active loader handles. |
| 344 | * @type {typeof getActiveLoaderHandles} |
| 345 | */ |
| 346 | active: getActiveLoaderHandles, |
| 347 | |
| 348 | /** |
| 349 | * Gets a loader handle by its ID. |
| 350 | * @type {typeof getLoaderHandleById} |
| 351 | */ |
| 352 | get: getLoaderHandleById, |
| 353 | |
| 354 | /** |
| 355 | * Checks if any blocking loader overlay is currently displayed. |
| 356 | * @returns {boolean} True if a blocking overlay is shown |
| 357 | */ |
| 358 | isBlocking: isOverlayDisplayed, |
| 359 | |
| 360 | /** |
| 361 | * Toast display mode constants. |
| 362 | * @type {typeof ActionLoaderToastMode} |
| 363 | */ |
| 364 | ToastMode: ActionLoaderToastMode, |
| 365 | |
| 366 | /** |
| 367 | * The ActionLoaderHandle class. |
| 368 | * @type {typeof ActionLoaderHandle} |
| 369 | */ |
| 370 | Handle: ActionLoaderHandle, |
| 371 | |
| 372 | /** |
| 373 | * Creates a fresh default loader overlay element. |
| 374 | * @type {typeof createDefaultLoaderOverlay} |
| 375 | */ |
| 376 | createOverlay: createDefaultLoaderOverlay, |
| 377 | }; |
| 378 | |
| 379 | /** |
| 380 | * Shows an action loader with an optional stoppable toast notification. |
| 381 | * Multiple loaders can be stacked - the overlay stays single, but each gets its own toast. |
| 382 | * When the last loader is hidden, the overlay is removed. |
| 383 | * |
| 384 | * With default arguments, will function as a generation loader / wrapper. |
| 385 | * |
| 386 | * @param {ActionLoaderOptions} [options={}] - Configuration options |
| 387 | * @returns {ActionLoaderHandle} Handle to control the loader |
| 388 | * |
| 389 | * @example |
| 390 | * // Basic usage |
| 391 | * const loader = showActionLoader({ message: 'Generating title...' }); |
| 392 | * try { |
| 393 | * const result = await generateRaw({ prompt }); |
| 394 | * // process result |
| 395 | * } finally { |
| 396 | * await loader.hide(); |
| 397 | * } |
| 398 | * |
| 399 | * @example |
| 400 | * // With custom stop and hide handlers |
| 401 | * const loader = showActionLoader({ |
| 402 | * message: 'Downloading...', |
| 403 | * stopTooltip: 'Cancel download', |
| 404 | * onStop: () => myCustomCancelFunction(), |
| 405 | * onHide: () => console.log('Loader hidden'), |
| 406 | * }); |
| 407 | * |
| 408 | * @example |
| 409 | * // Stacking multiple loaders |
| 410 | * const loader1 = showActionLoader({ message: 'Task 1...' }); |
| 411 | * const loader2 = showActionLoader({ message: 'Task 2...' }); |
| 412 | * await loader1.hide(); // Overlay stays, loader2 still active |
| 413 | * await loader2.hide(); // Now overlay hides |
| 414 | * |
| 415 | * @example |
| 416 | * // Non-blocking loader (toast only, no overlay) |
| 417 | * const loader = showActionLoader({ |
| 418 | * message: 'Captioning image...', |
| 419 | * blocking: false, |
| 420 | * onStop: () => abortCaptioning(), |
| 421 | * }); |
| 422 | */ |
| 423 | export function showActionLoader(options = {}) { |
| 424 | return new ActionLoaderHandle(options); |
| 425 | } |
| 426 | |
| 427 | /** |
| 428 | * Hides a specific action loader by handle, or all active loaders if no handle provided. |
| 429 | * @param {ActionLoaderHandle|null} [handle=null] - Specific handle to hide, or undefined to hide all |
| 430 | * @returns {Promise<boolean>} Whether any loader was hidden |
| 431 | */ |
| 432 | export async function hideActionLoader(handle = null) { |
| 433 | if (handle instanceof ActionLoaderHandle) { |
| 434 | if (handle.isActive) { |
| 435 | await handle.hide(); |
| 436 | return true; |
| 437 | } |
| 438 | return false; |
| 439 | } |
| 440 | |
| 441 | // No handle provided - hide all active loaders |
| 442 | const handles = getActiveLoaderHandles(); |
| 443 | for (const h of handles) { |
| 444 | await h.hide(); |
| 445 | } |
| 446 | return handles.length > 0; |
| 447 | } |
| 448 | |
| 449 | /** |
| 450 | * Gets all currently active loader handles. |
| 451 | * @returns {ActionLoaderHandle[]} Array of active handles |
| 452 | */ |
| 453 | export function getActiveLoaderHandles() { |
| 454 | return Array.from(activeHandles); |
| 455 | } |
| 456 | |
| 457 | /** |
| 458 | * Gets a loader handle by its ID. |
| 459 | * @param {string} id - The handle ID |
| 460 | * @returns {ActionLoaderHandle|undefined} The handle, or undefined if not found |
| 461 | */ |
| 462 | export function getLoaderHandleById(id) { |
| 463 | for (const handle of activeHandles) { |
| 464 | if (handle.id === id) { |
| 465 | return handle; |
| 466 | } |
| 467 | } |
| 468 | return undefined; |
| 469 | } |
| 470 | |
| 471 | // ============================================================================ |
| 472 | // Internal overlay management |
| 473 | // ============================================================================ |
| 474 | |
| 475 | /** @type {Popup|null} The current loader overlay popup */ |
| 476 | let loaderPopup = null; |
| 477 | |
| 478 | /** Whether the initial HTML preloader has been removed */ |
| 479 | let preloaderYoinked = false; |
| 480 | |
| 481 | /** |
| 482 | * Creates the default loader overlay element. |
| 483 | * Always returns a fresh element instance. |
| 484 | * |
| 485 | * @returns {HTMLDivElement} A new loader overlay element |
| 486 | */ |
| 487 | export function createDefaultLoaderOverlay() { |
| 488 | const loaderElement = document.createElement('div'); |
| 489 | loaderElement.id = 'loader'; |
| 490 | |
| 491 | const spinnerElement = document.createElement('div'); |
| 492 | spinnerElement.id = 'load-spinner'; |
| 493 | spinnerElement.className = 'fa-solid fa-gear fa-spin fa-3x'; |
| 494 | |
| 495 | loaderElement.appendChild(spinnerElement); |
| 496 | |
| 497 | return loaderElement; |
| 498 | } |
| 499 | |
| 500 | /** |
| 501 | * Normalizes custom overlay content into a value supported by Popup. |
| 502 | * @param {string|HTMLElement|null} customContent - Custom overlay content |
| 503 | * @returns {string|HTMLElement} Content for Popup |
| 504 | */ |
| 505 | function getOverlayContent(customContent) { |
| 506 | if (typeof customContent === 'string') { |
| 507 | return customContent; |
| 508 | } |
| 509 | |
| 510 | if (customContent instanceof HTMLElement) { |
| 511 | return customContent; |
| 512 | } |
| 513 | |
| 514 | return createDefaultLoaderOverlay(); |
| 515 | } |
| 516 | |
| 517 | /** |
| 518 | * Checks if the loader overlay is currently displayed. |
| 519 | * @returns {boolean} True if overlay is shown |
| 520 | */ |
| 521 | function isOverlayDisplayed() { |
| 522 | return !!loaderPopup; |
| 523 | } |
| 524 | |
| 525 | /** |
| 526 | * Shows the blocking loader overlay. |
| 527 | * Internal function - use showActionLoader() instead. |
| 528 | * @param {HTMLElement|string|null} [customContent] - Custom content for the overlay |
| 529 | */ |
| 530 | function showOverlay(customContent = null) { |
| 531 | // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes |
| 532 | if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); |
| 533 | |
| 534 | const content = getOverlayContent(customContent); |
| 535 | |
| 536 | loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, { |
| 537 | allowEscapeClose: false, |
| 538 | transparent: true, |
| 539 | animation: 'none', |
| 540 | wide: true, |
| 541 | large: true, |
| 542 | }); |
| 543 | |
| 544 | // No close button, loaders are not closable |
| 545 | loaderPopup.closeButton.style.display = 'none'; |
| 546 | |
| 547 | loaderPopup.show(); |
| 548 | } |
| 549 | |
| 550 | /** |
| 551 | * Hides the blocking loader overlay with animation. |
| 552 | * Internal function - use hideActionLoader() instead. |
| 553 | * @returns {Promise<void>} |
| 554 | */ |
| 555 | async function hideOverlay() { |
| 556 | if (!loaderPopup) { |
| 557 | return Promise.resolve(); |
| 558 | } |
| 559 | |
| 560 | return new Promise((resolve) => { |
| 561 | const loaderElement = $('#loader'); |
| 562 | const spinner = $('#load-spinner'); |
| 563 | |
| 564 | if (!loaderElement.length) { |
| 565 | console.warn('Loader element not found, skipping animation'); |
| 566 | cleanup(); |
| 567 | return; |
| 568 | } |
| 569 | |
| 570 | // Check if transitions are enabled on spinner (which has the transition property) |
| 571 | const transitionDuration = spinner.length && spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; |
| 572 | const hasTransitions = parseFloat(transitionDuration) > 0; |
| 573 | |
| 574 | if (hasTransitions) { |
| 575 | Promise.race([ |
| 576 | new Promise((r) => setTimeout(r, 500)), // Fallback timeout |
| 577 | new Promise((r) => loaderElement.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), |
| 578 | ]).finally(cleanup); |
| 579 | } else { |
| 580 | cleanup(); |
| 581 | } |
| 582 | |
| 583 | function cleanup() { |
| 584 | loaderElement.remove(); |
| 585 | // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS |
| 586 | // If it's present, we remove it once and then it's gone. |
| 587 | yoinkPreloader(); |
| 588 | |
| 589 | loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE) |
| 590 | .catch((err) => console.error('Error completing loaderPopup:', err)) |
| 591 | .finally(() => { |
| 592 | loaderPopup = null; |
| 593 | resolve(); |
| 594 | }); |
| 595 | } |
| 596 | |
| 597 | // Apply the blur styles to the entire loader element |
| 598 | loaderElement.css({ |
| 599 | 'filter': 'blur(15px)', |
| 600 | 'opacity': '0', |
| 601 | }); |
| 602 | }); |
| 603 | } |
| 604 | |
| 605 | /** |
| 606 | * Removes the initial HTML preloader element. |
| 607 | * Called once after the first loader hide. |
| 608 | */ |
| 609 | function yoinkPreloader() { |
| 610 | if (preloaderYoinked) return; |
| 611 | document.getElementById('preloader')?.remove(); |
| 612 | preloaderYoinked = true; |
| 613 | } |
| 614 | |
| 615 | // ============================================================================ |
| 616 | // End internal overlay management |
| 617 | // ============================================================================ |