feat: auto-generate self-signed certs, hoover modal processing guard, refactor backends/proxy pages
This commit is contained in:
@@ -6,10 +6,9 @@
|
||||
* avoid fighting with the main render cycle.
|
||||
*/
|
||||
|
||||
import { esc } from '../helpers.js?v=8';
|
||||
import { att_esc } from '../helpers.js?v=8';
|
||||
import { apiSubmit } from '../api.js?v=8';
|
||||
import { createDom } from '../vdom.js?v=8';
|
||||
import { esc, att_esc } from '../helpers.js?v=9';
|
||||
import { apiSubmit } from '../api.js?v=9';
|
||||
import { createDom } from '../vdom.js?v=9';
|
||||
|
||||
/**
|
||||
* Render Hoover VNodes into a modal content element.
|
||||
@@ -33,7 +32,13 @@ function _renderModals() {
|
||||
_modalQueue.forEach((m, idx) => {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'modal-overlay active';
|
||||
wrap.onclick = (e) => { if (e.target === wrap) closeModal(idx); };
|
||||
wrap.onclick = (e) => {
|
||||
if (e.target === wrap) {
|
||||
if (m._processing) return;
|
||||
if (m._hasInputs && !confirm('Discard changes?')) return;
|
||||
closeModal(idx);
|
||||
}
|
||||
};
|
||||
const content = document.createElement('div');
|
||||
content.className = 'modal';
|
||||
content.onclick = (e) => e.stopPropagation();
|
||||
@@ -55,12 +60,36 @@ function _renderModals() {
|
||||
*/
|
||||
export function openModal(content) {
|
||||
const entry = typeof content === 'function'
|
||||
? { renderFn: content, id: _modalQueue.length }
|
||||
: { id: _modalQueue.length, renderFn: (inner) => modalVNodes(inner, content) };
|
||||
? { renderFn: content, id: _modalQueue.length, _processing: false, _hasInputs: false }
|
||||
: { id: _modalQueue.length, renderFn: (inner) => modalVNodes(inner, content), _processing: false, _hasInputs: false };
|
||||
_modalQueue.push(entry);
|
||||
_renderModals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the topmost modal (or specified index) has an active async operation.
|
||||
*
|
||||
* @param {number} [idx] – Modal index (defaults to topmost)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isModalProcessing(idx) {
|
||||
if (idx === undefined) idx = _modalQueue.length - 1;
|
||||
if (idx < 0 || idx >= _modalQueue.length) return false;
|
||||
return _modalQueue[idx]._processing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the processing flag on the topmost modal (or specified index).
|
||||
*
|
||||
* @param {boolean} flag – Whether the modal is currently processing
|
||||
* @param {number} [idx] – Modal index (defaults to topmost)
|
||||
*/
|
||||
export function setModalProcessing(flag, idx) {
|
||||
if (idx === undefined) idx = _modalQueue.length - 1;
|
||||
if (idx < 0 || idx >= _modalQueue.length) return;
|
||||
_modalQueue[idx]._processing = flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a modal by index. Closes the topmost modal if index is omitted.
|
||||
*
|
||||
@@ -135,16 +164,39 @@ export function formModal(inner, title, fields, actions) {
|
||||
+ (f.type !== 'text' ? ' type="' + att_esc(f.type) + '"' : '')
|
||||
+ (f.value !== undefined ? ' value="' + att_esc(f.value) + '"' : '')
|
||||
+ (f.placeholder ? ' placeholder="' + att_esc(f.placeholder) + '"' : '')
|
||||
+ (f.checked ? ' checked' : '')
|
||||
+ '></' + 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>';
|
||||
}).join('') + '</div><div class="modal-actions"></div>';
|
||||
|
||||
actions.forEach(a => {
|
||||
const btn = inner.querySelector('[data-action="' + att_esc(a.action) + '"]');
|
||||
if (btn) btn.addEventListener('click', a.handler);
|
||||
});
|
||||
// Mark modal as having editable inputs
|
||||
const topEntry = _modalQueue[_modalQueue.length - 1];
|
||||
if (topEntry) topEntry._hasInputs = true;
|
||||
|
||||
// Build action buttons with processing-aware rendering
|
||||
const actionsBar = inner.querySelector('.modal-actions');
|
||||
const actionBtns = [];
|
||||
for (const a of actions) {
|
||||
const actionId = 'am-' + a.action + '-' + (_modalQueue.length - 1);
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn ' + a.cls;
|
||||
btn.id = actionId;
|
||||
if (a.processing && isModalProcessing(_modalQueue.length - 1)) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="btn-spinner"></span>';
|
||||
} else {
|
||||
btn.appendChild(document.createTextNode(a.label));
|
||||
}
|
||||
// Store reference for later re-binding
|
||||
actionBtns.push({ btn, action: a });
|
||||
if (a.handler) {
|
||||
const origHandler = a.handler;
|
||||
btn.addEventListener('click', () => {
|
||||
refreshModals();
|
||||
origHandler();
|
||||
});
|
||||
}
|
||||
actionsBar.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user