components_confirm_dialog.js


/**
 * Modal confirmation dialog with optional injected content (e.g. a select element).
 * Clicking the overlay background counts as a cancel.
 */
export class ConfirmDialog {
    /**
    * @param {string} titleText - Header text for the dialog (may contain HTML)
    * @param {object} callbacks - Callback functions for dialog actions
    * @param {function} [callbacks.onConfirm] - Called when the confirm button is clicked
    * @param {function} [callbacks.onDismiss] - Called when cancel or the overlay is clicked
    * @param {string} [warningText=''] - Body text shown below the title
    * @param {string} [confirmText='Confirm'] - Label for the confirm button
    * @param {string} [cancelText='Cancel'] - Label for the cancel button
    * @param {HTMLElement|null} [contentInjection=null] - Optional element inserted into the body
    */
    constructor(titleText, { onConfirm, onDismiss }, warningText = '', confirmText = 'Confirm', cancelText='Cancel', contentInjection = null) {
        this._onConfirm = onConfirm ?? (() => {});
        this._onDismiss = onDismiss ?? (() => {});

        this.#buildDialog(titleText, warningText, confirmText, cancelText, contentInjection);
    }

    /**
     * Builds and appends the dialog overlay and modal to the document body.
     * @param {string} titleText - Header text for the dialog (may contain HTML)
     * @param {string} warningText - Body text shown below the title
     * @param {string} confirmText - Label for the confirm button
     * @param {string} cancelText - Label for the cancel button
     * @param {HTMLElement|null} contentInjection - Optional element inserted into the body
     */
    #buildDialog(titleText, warningText, confirmText, cancelText, contentInjection) {
        const overlay = document.createElement('div');
        overlay.className = 'confirm-dialog-overlay';

        const modal = document.createElement('div');
        modal.className = 'confirm-dialog-modal';
        modal.style.cssText = 'max-height:none;width:380px;padding:0;';

        const header = document.createElement('div');
        header.className = 'confirm-dialog-header';
        header.innerHTML = `<span>${titleText}</span>`;
        modal.appendChild(header);

        const body = document.createElement('div');
        body.style.cssText = 'padding:1rem 1.25rem;display:flex;flex-direction:column;gap:0.85rem;';

        const warning = document.createElement('div');
        warning.style.cssText = 'font-family:var(--font-mono);font-size:var(--fs-caption);color:var(--text);line-height:1.6;';
        warning.textContent = warningText;
        body.appendChild(warning);

        // If an injection has been provided, inject it now
        if (contentInjection) {
            body.appendChild(contentInjection);
        }

        const actions = document.createElement('div');
        actions.style.cssText = 'display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.25rem;';

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'sample-btn';
        cancelBtn.textContent = cancelText;
        cancelBtn.style.cssText = 'padding:0.35rem 0.75rem;font-size:var(--fs-label);';
        cancelBtn.addEventListener('click', () => {
            overlay.remove();
            this._onDismiss();
        });
        actions.appendChild(cancelBtn);

        const confirmBtn = document.createElement('button');
        confirmBtn.className = 'sample-btn';
        confirmBtn.textContent = confirmText;

        confirmBtn.style.cssText = 'padding:0.35rem 0.75rem;font-size:var(--fs-label);border-color:var(--danger-border);color:var(--danger-muted);';
        confirmBtn.onmouseenter = () => { confirmBtn.style.background = 'var(--danger-faint)'; confirmBtn.style.borderColor = 'var(--danger)'; };
        confirmBtn.onmouseleave = () => { confirmBtn.style.background = ''; confirmBtn.style.borderColor = 'var(--danger-border)'; };

        confirmBtn.addEventListener('click', () => {
            overlay.remove();
            this._onConfirm();
        });
        actions.appendChild(confirmBtn);

        body.appendChild(actions);
        modal.appendChild(body);
        overlay.appendChild(modal);
        // if the background of the window is clicked while this dialog is opened, it counts as cancel
        // (only when the mousedown also started on the overlay, so drag-release doesn't dismiss)
        let _mouseDownOnOverlay = false;
        overlay.addEventListener('mousedown', (e) => { _mouseDownOnOverlay = e.target === overlay; });
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay && _mouseDownOnOverlay) {
                overlay.remove();
                this._onDismiss();
            }
        });
        document.body.appendChild(overlay);
    }
}