"use strict"; function _d1f5a3cb_defineProperty(e, r, t) { return (r = _d1f5a3cb_toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _d1f5a3cb_toPropertyKey(t) { var i = _d1f5a3cb_toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _d1f5a3cb_toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * Modal Static API * * Primary interface for displaying modals throughout the application. * Provides simple methods for common dialogs and flexible options for custom modals. * * Usage: * await Modal.alert("File saved") * if (await Modal.confirm("Delete?")) { ... } * let name = await Modal.prompt("Enter name:") * let result = await Modal.show({ title, body, buttons }) */ class Modal { /** * Initialize global handlers (called automatically on first modal) * @private */ static _init() { if (this._initialized) return; this._initialized = true; // Create shared backdrop element this._backdrop = $(''); $('body').append(this._backdrop); } /** * Calculate scrollbar width * @private * @returns {number} */ static _get_scrollbar_width() { // Create temporary element to measure scrollbar width const $outer = $('
').css({ visibility: 'hidden', overflow: 'scroll', width: '100px', position: 'absolute', top: '-9999px' }); $('body').append($outer); const width_with_scrollbar = $outer[0].offsetWidth; const $inner = $('
').css('width', '100%'); $outer.append($inner); const width_without_scrollbar = $inner[0].offsetWidth; $outer.remove(); return width_with_scrollbar - width_without_scrollbar; } /** * Lock body scroll and compensate for scrollbar width * Only locks if we haven't already saved the original state (first modal in chain) * @private */ static _lock_body_scroll() { // Cancel any pending unlock timeout if (this._unlock_timeout) { clearTimeout(this._unlock_timeout); this._unlock_timeout = null; } // Only lock scroll if we haven't already saved state (first modal) // This is the true indicator - not backdrop visibility which can be transitional if (this._original_body_overflow === null) { const $body = $('body'); // Store original values this._original_body_overflow = $body.css('overflow'); this._original_body_padding = $body.css('padding-right'); // Check if body currently has vertical scroll const has_scrollbar = document.body.scrollHeight > window.innerHeight; // If there's a scrollbar, add padding to compensate for its removal if (has_scrollbar) { const scrollbar_width = this._get_scrollbar_width(); const current_padding = int(this._original_body_padding) || 0; $body.css('padding-right', current_padding + scrollbar_width + 'px'); } // Lock scroll $body.css('overflow', 'hidden'); } } /** * Unlock body scroll and restore original state * Uses delayed check to ensure no other modals are opening * @private */ static _unlock_body_scroll() { // Clear any existing timeout if (this._unlock_timeout) { clearTimeout(this._unlock_timeout); } // Minimal delay before unlocking this._unlock_timeout = setTimeout(() => { // Double-check no modal is currently open and queue is empty if (!this._current && this._queue.length === 0) { const $body = $('body'); // Restore original values if (this._original_body_overflow !== null) { $body.css('overflow', this._original_body_overflow); this._original_body_overflow = null; } if (this._original_body_padding !== null) { $body.css('padding-right', this._original_body_padding); this._original_body_padding = null; } } this._unlock_timeout = null; }, 50); // Minimal safety buffer } /** * Show the shared backdrop (instant - no animation) * @private */ static async _show_backdrop() { if (!this._backdrop.hasClass('show')) { // Lock body scroll before showing backdrop this._lock_body_scroll(); this._backdrop.css('display', 'block').addClass('show'); // No delay - return immediately } } /** * Hide the shared backdrop (instant - no animation) * @private */ static async _hide_backdrop() { this._backdrop.removeClass('show').css('display', 'none'); // Unlock body scroll after backdrop is hidden this._unlock_body_scroll(); } /** * Create a new Rsx_Modal instance * @private */ static async _create_modal() { // Create modal component using jQuery plugin const $modal_element = $('
'); // Create component instance directly (returns the component) const modal_instance = $modal_element.component('Rsx_Modal', {}); // Wait for component to be fully ready (DOM elements queryable) await new Promise(resolve => { modal_instance.on('ready', () => { console.log('[Modal] Component ready, elements:', { title: modal_instance.$sid('title').length, body: modal_instance.$sid('body').length, footer: modal_instance.$sid('footer').length }); resolve(); }); }); return modal_instance; } /** * Show a modal and manage queue * @private */ static async _show_modal(options) { return new Promise(resolve => { this._queue.push({ options, resolve }); // Process queue if no modal currently showing if (!this._current) { this._process_queue(); } }); } /** * Process the modal queue * @private */ static async _process_queue() { if (this._queue.length === 0) { this._current = null; // Hide backdrop when queue is empty await this._hide_backdrop(); return; } const { options, resolve } = this._queue.shift(); // Ensure initialized this._init(); // Show backdrop if not already visible (instant - no delay between modals) const backdrop_visible = this._backdrop.hasClass('show'); if (!backdrop_visible) { await this._show_backdrop(); } // No delay between sequential modals - immediate transition // Create modal instance const modal_instance = await this._create_modal(); this._current = modal_instance; // Determine if we should animate based on: // 1. Desktop viewport (>= 1000px) // 2. More than 1 second since last modal closed const viewport_width = $(window).width(); const is_desktop = viewport_width >= 1000; const time_since_last_close = Date.now() - this._last_close_timestamp; const should_animate = is_desktop && time_since_last_close > 1000; // Show modal and wait for result (modal won't create its own backdrop) const result = await modal_instance.show(options, { skip_backdrop: true, animate: should_animate }); // Record close timestamp BEFORE resolving (ensures it's set before next modal can start) this._last_close_timestamp = Date.now(); // Resolve the promise with the result resolve(result); // Clear current and process next this._current = null; this._process_queue(); } // ================================================================================ // State Management Methods // ================================================================================ /** * Check if a modal is currently open * @returns {boolean} */ static is_open() { return this._current !== null; } /** * Get the currently open modal instance * @returns {Rsx_Modal|null} */ static get_current() { return this._current; } /** * Force close the current modal * @returns {Promise} */ static async close() { if (this._current) { await this._current.close(false); } } /** * Apply validation errors to the current modal * @param {Object} errors - Error object {field: message} */ static apply_errors(errors) { if (this._current) { this._current.apply_errors(errors); } } // ================================================================================ // Simple Dialog Methods // ================================================================================ /** * Show an alert dialog * @param {string|jQuery} title_or_body - Message (if only 1 arg) or Title (if 2 args). Can be string or jQuery element. * @param {string|jQuery} body - Message body (if 2 args). Can be string or jQuery element. * @param {string} button_label - Button text (default: "OK") * @returns {Promise} */ static async alert(title_or_body) { let body = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; let button_label = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'OK'; let title = 'Notice'; let message = title_or_body; if (body !== null) { title = title_or_body; message = body; } await this._show_modal({ title: title, body: message, buttons: [{ label: button_label, value: true, class: 'btn-primary', default: true }], closable: true, close_on_submit: true }); } /** * Show a confirmation dialog * @param {string|jQuery} title_or_body - Message (if 1-2 args) or Title (if 3-4 args). Can be string or jQuery element. * @param {string|jQuery} body - Message body (optional). Can be string or jQuery element. * @param {string} confirm_label - Confirm button text (default: "Confirm") * @param {string} cancel_label - Cancel button text (default: "Cancel") * @returns {Promise} */ static async confirm(title_or_body) { let body = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; let confirm_label = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'Confirm'; let cancel_label = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 'Cancel'; let title = 'Confirm'; let message = title_or_body; if (body !== null) { title = title_or_body; message = body; } const result = await this._show_modal({ title: title, body: message, buttons: [{ label: cancel_label, value: false, class: 'btn-secondary' }, { label: confirm_label, value: true, class: 'btn-primary', default: true }], closable: true, close_on_submit: true }); return result === true; } /** * Show a prompt dialog for text input * @param {string|jQuery} title_or_body - Message (if 1-3 args) or Title (if 4 args). Can be string or jQuery element. * @param {string|jQuery} body - Message body (optional). Can be string or jQuery element. * @param {string} default_value - Default input value * @param {boolean} multiline - Show textarea instead of input * @param {string} error - Optional error message to display as validation feedback * @returns {Promise} */ static async prompt(title_or_body) { let body = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; let default_value = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ''; let multiline = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; let error = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null; let title = 'Input'; let message = title_or_body; // Handle overloaded arguments if (typeof body === 'string' && body !== '') { title = title_or_body; message = body; } // Create input element with minimum width constraints const $input = multiline ? $('') : $(''); $input.val(default_value); // Mark as invalid if there's an error if (error) { $input.addClass('is-invalid'); } // Create body with message and input let $body; if (message instanceof jQuery) { // If message is a jQuery element, use it as the container and append input $body = message.append($input); } else { // If message is a string, create wrapper with text and input (36px spacing) $body = $('
').append($('
').text(message)).append($input); } // Add error message if provided if (error) { const $error = $('
').text(error); $body.append($error); } const result = await this._show_modal({ title: title, body: $body, buttons: [{ label: 'Cancel', value: false, class: 'btn-secondary' }, { label: 'Submit', value: null, // Will be replaced by callback class: 'btn-primary', default: true, callback: function () { return $input.val(); } }], closable: true, close_on_submit: true, max_width: 500 }); // Focus and select input after modal shows requestAnimationFrame(() => { $input.focus(); if (!multiline) { $input.select(); } }); return result; } /** * Show an error dialog with red alert styling * * Can appear over other modals to show critical uncaught exceptions. * Used primarily for Ajax errors that weren't caught by application code. * * @param {string|Error|Object} error - Error message string, Error object, or structured error * @param {string} title - Modal title (default: "Error") * @returns {Promise} */ static async error(error) { let title = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Error'; let message = ''; // Handle different error types if (typeof error === 'string') { message = error; } else if (error instanceof Error) { message = error.message || error.toString(); } else if (error && error.message) { message = error.message; } else if (error && error.error) { // Fatal error with details const details = error.error; if (details.file && details.line) { message = `Uncaught Fatal Error in ${details.file}:${details.line}:\n\n${details.error}`; } else { message = details.error || 'An unknown error occurred'; } } else { message = 'An unknown error occurred'; } // Create error body with red alert styling const $body = $('