"use strict"; /** * Rsx_Modal Component * * Instance of a modal dialog. Handles lifecycle, sizing, and user interaction. * Typically created and managed by the Modal static API class. */ class Rsx_Modal extends Component { on_create() { this.data.title = ''; this.data.body_content = null; this.data.buttons = []; this.data.closable = true; this.data.max_width = 800; this.data.close_on_submit = true; this.data.is_visible = false; this.data.result_promise = null; this.data.resolve_fn = null; // Store reference to bootstrap modal instance this._bs_modal = null; this._resize_handler = null; } on_ready() { const that = this; // Set up close button handler this.$sid('close_btn').on('click', function (e) { e.preventDefault(); if (that.data.closable) { that.close(false); } }); // Set up backdrop click handler this.$sid('backdrop').on('click', function (e) { if (that.data.closable && e.target === this) { that.close(false); } }); // Set up ESC key handler $(document).on('keydown.rsx_modal_' + this._cid, function (e) { if (e.key === 'Escape' && that.data.closable && that.data.is_visible) { that.close(false); } }); // Set up resize handler this._resize_handler = debounce(() => { if (that.data.is_visible) { that._apply_sizing(); } }, 100); $(window).on('resize.rsx_modal_' + this._cid, this._resize_handler); } /** * Configure and show the modal * @param {Object} options - Modal options (title, body, buttons, etc.) * @param {Object} internal_options - Internal options (skip_backdrop, animate) */ async show(options) { let internal_options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; const that = this; const skip_backdrop = internal_options.skip_backdrop || false; const should_animate = internal_options.animate || false; console.log('[Rsx_Modal] show() called with options:', options); // Store options this.data.title = options.title || ''; this.data.closable = options.closable !== undefined ? options.closable : true; this.data.max_width = options.max_width || 800; this.data.close_on_submit = options.close_on_submit !== undefined ? options.close_on_submit : true; this.data.buttons = options.buttons || []; this.data.skip_backdrop = skip_backdrop; this.data.icon = options.icon || null; console.log('[Rsx_Modal] Setting title to:', this.data.title); console.log('[Rsx_Modal] Title element:', this.$sid('title')); // Set title this.$sid('title').text(this.data.title); // Show/hide close button based on closable if (this.data.closable) { this.$sid('close_btn').show(); } else { this.$sid('close_btn').hide(); } // Set body content (with optional icon) this._set_body_content(options.body, this.data.icon); // Set buttons this._set_buttons(); // Create promise that will resolve when modal closes const result_promise = new Promise(resolve => { that.data.resolve_fn = resolve; }); // Show modal and backdrop this.data.is_visible = true; // Append to body so it's on top (don't append backdrop if using shared) if (!skip_backdrop) { $('body').append(this.$sid('backdrop')); } $('body').append(this.$); // Apply sizing before showing this._apply_sizing(); // Fade in modal (and backdrop if not using shared) await this._fade_in(should_animate); // Auto-focus first input element this._focus_first_input(); return result_promise; } /** * Set body content with optional icon */ _set_body_content(body, icon) { const $body = this.$sid('body'); $body.empty(); // If icon provided, add it if (icon) { const $icon = $(``); $body.append($icon); $body.addClass('has-icon'); } else { $body.removeClass('has-icon'); } // Get or create body content wrapper let $content = this.$sid('body_content'); if (!$content.exists()) { $content = $(''); $body.append($content); } if (typeof body === 'string') { // Text content - escape and convert newlines const escaped = $('
').text(body).html().replace(/\n/g, '
'); $content.html(escaped); } else if (body instanceof jQuery) { // jQuery element $content.append(body); } else if (body && typeof body === 'object') { // Assume it's a jqhtml component instance $content.append(body.$); } } /** * Set buttons in footer */ _set_buttons() { const that = this; const $footer = this.$sid('footer'); $footer.empty(); if (this.data.buttons.length === 0) { $footer.hide(); return; } $footer.show(); for (let button_def of this.data.buttons) { const $button = $('