Files
vacuum-wall/webui/static/hoover/components/modal.js
T

104 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Hoover — components/modal.js
*
* Modal overlay system: openModal, closeModal, closeAllModals, formModal.
* Renders directly into #modal-root using DOM manipulation (not vdom) to
* avoid fighting with the main render cycle.
*/
import { esc } from '../helpers.js';
import { att_esc } from '../helpers.js';
const _modalQueue = [];
function _renderModals() {
const root = document.getElementById('modal-root');
if (!root) return;
root.innerHTML = '';
_modalQueue.forEach((m, idx) => {
const wrap = document.createElement('div');
wrap.className = 'modal-overlay active';
wrap.onclick = (e) => { if (e.target === wrap) closeModal(idx); };
const content = document.createElement('div');
content.className = 'modal';
content.onclick = (e) => e.stopPropagation();
if (m.renderFn) {
try { m.renderFn(content, idx); }
catch (err) { content.textContent = err.message; }
}
wrap.appendChild(content);
root.appendChild(wrap);
});
}
/**
* Open a modal dialog.
*
* @param {function} renderFn (contentEl, idx) => void, renders into contentEl
*/
export function openModal(renderFn) {
_modalQueue.push({ renderFn, id: _modalQueue.length });
_renderModals();
}
/**
* Close a modal by index. Closes the topmost modal if index is omitted.
*
* @param {number} [idx]
*/
export function closeModal(idx) {
if (idx === undefined) idx = _modalQueue.length - 1;
if (idx >= 0 && idx < _modalQueue.length) _modalQueue.splice(idx, 1);
_renderModals();
}
/**
* Close all open modals.
*/
export function closeAllModals() {
_modalQueue.length = 0;
_renderModals();
}
/**
* Render a standard modal layout: title, form fields, action buttons.
*
* @param {HTMLElement} inner Modal content element to fill
* @param {string} title Modal title
* @param {object[]} fields Form field descriptors
* @param {object[]} actions Action button descriptors
*
* Field shape:
* { label, id, [tag: 'input'|'select'|'textarea'], [type], [value], [placeholder], [options] }
*
* Action shape:
* { label, cls, action, handler }
*/
export function formModal(inner, title, fields, actions) {
inner.innerHTML = '<h2 class="modal-title">' + esc(title) + '</h2><div class="modal-body">'
+ fields.map(f => {
if (f.tag === 'select')
return '<div class="form-group"><label>' + esc(f.label) + '</label><select id="' + att_esc(f.id) + '">'
+ (f.options || []).map(o =>
typeof o === 'string'
? '<option value="' + att_esc(o) + '">' + esc(o) + '</option>'
: '<option value="' + att_esc(o[0]) + '"' + (o[1] ? ' selected' : '') + '>' + esc(o[1]) + '</option>',
).join('') + '</select></div>';
const tag = f.tag || 'input';
return '<div class="form-group"><label>' + esc(f.label) + '</label><' + tag + ' id="' + att_esc(f.id) + '"'
+ (f.type !== 'text' ? ' type="' + att_esc(f.type) + '"' : '')
+ (f.value !== undefined ? ' value="' + att_esc(f.value) + '"' : '')
+ (f.placeholder ? ' placeholder="' + att_esc(f.placeholder) + '"' : '')
+ '></' + tag + '></div>';
}).join('') + '</div><div class="modal-actions">'
+ actions.map(a =>
'<button class="btn ' + att_esc(a.cls) + '" data-action="' + att_esc(a.action) + '">' + esc(a.label) + '</button>',
).join('') + '</div>';
actions.forEach(a => {
const btn = inner.querySelector('[data-action="' + att_esc(a.action) + '"]');
if (btn) btn.addEventListener('click', a.handler);
});
}