From 77316ec72accf48b8f782c9b51e0ffed9a4a84ee Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Tue, 26 May 2026 17:13:03 +0200 Subject: [PATCH] review of DOMContentLoaded --- frontend/aliases.html | 10 +- frontend/devices.html | 14 +- frontend/hosts.html | 12 +- frontend/js/aliases.js | 292 ++++++++++++++++++++++------------------- frontend/js/devices.js | 292 ++++++++++++++++++++++------------------- frontend/js/hosts.js | 292 ++++++++++++++++++++++------------------- frontend/js/leases.js | 252 +++++++++++++++++++---------------- frontend/leases.html | 14 +- 8 files changed, 643 insertions(+), 535 deletions(-) diff --git a/frontend/aliases.html b/frontend/aliases.html index 4d587f1..4321002 100644 --- a/frontend/aliases.html +++ b/frontend/aliases.html @@ -112,11 +112,11 @@ - - - - - + + + + + diff --git a/frontend/devices.html b/frontend/devices.html index f908c9d..3b9d857 100644 --- a/frontend/devices.html +++ b/frontend/devices.html @@ -112,13 +112,13 @@
Alias Target DescriptionOptions Actions Alias Target DescriptionOptions Actions
- - - - - - - + + + + + + + diff --git a/frontend/hosts.html b/frontend/hosts.html index 69dd032..00bd53b 100644 --- a/frontend/hosts.html +++ b/frontend/hosts.html @@ -112,12 +112,12 @@
IP Address MAC AddressHostname DescriptionState Active Actions IP Address MAC AddressHostname DescriptionState Active Actions
- - - - - - + + + + + + diff --git a/frontend/js/aliases.js b/frontend/js/aliases.js index 2d88a9f..a04b343 100644 --- a/frontend/js/aliases.js +++ b/frontend/js/aliases.js @@ -586,9 +586,16 @@ const actionHandlers = { }; // ----------------------------- -// DOMContentLoaded: initialize everything +// DOMContentLoaded: bootstrap app // ----------------------------- document.addEventListener("DOMContentLoaded", async () => { + await initApp(); +}); + +// ----------------------------- +// APP INIT +// ----------------------------- +async function initApp() { // Load modals (Bootstrap 5 requires JS initialization for dynamic content) try { @@ -598,9 +605,6 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading modals", false); } - // Init UI sort (aria-sort, arrows) - initSortableTable(); - // Load data (aliases) try { await loadAliases(); @@ -609,149 +613,171 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading aliases:", false); } + initUI(); + initEvents(); +} + +// ----------------------------- +// UI INIT +// ----------------------------- +function initUI() { + // Init UI sort (aria-sort, arrows) + initSortableTable(); + initSearch(); + initModalLifecycle(); +} + +// ----------------------------- +// SEARCH +// ----------------------------- +function initSearch() { // search bar const input = document.getElementById("searchInput"); - if (input) { - // clean input on load - input.value = ""; - // live filter for each keystroke - input.addEventListener("input", filterAliases); - // Escape management when focus is in the input - input.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) - e.stopPropagation(); // evita che arrivi al listener globale - resetSorting(sortState); - clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) - filterAliases(''); // ripristina tabella - } - }); - } + if (!input) return; - // global ESC key listener to clear search and reset sorting - document.addEventListener("keydown", (e) => { - // Ignore if focus is in a typing field - const tag = (e.target.tagName || "").toLowerCase(); - const isTypingField = - tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; - - if (e.key === "Escape" && !isTypingField) { - // Prevent default form submission - e.preventDefault(); - resetSorting(sortState); - clearSearch(); - filterAliases(''); - } - }); + // clean input on load + input.value = ""; +} + +// ----------------------------- +// MODAL LIFECYCLE (ADD / EDIT) +// ----------------------------- +function initModalLifecycle() { // Modal show/hidden events to prepare/reset the form const modalEl = document.getElementById('addAliasModal'); - if (modalEl) { - - // store who opened the modal - let lastTriggerEl = null; - - // When shown, determine Add or Edit mode - modalEl.addEventListener('show.bs.modal', async (ev) => { - lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) - const formEl = document.getElementById('addAliasForm'); - - // Security check - if (!formEl) return; - - // check Add or Edit mode - const idAttr = lastTriggerEl?.getAttribute?.('data-alias-id'); - const id = idAttr ? Number(idAttr) : null; - - if (Number.isFinite(id)) { - // Edit Mode - try { - await editAlias(id); - } catch (err) { - showToast(err?.message || "Error loading alias for edit", false); - // Close modal - const closeOnShown = () => { - closeAddAliasModal(lastTriggerEl); - modalEl.removeEventListener('shown.bs.modal', closeOnShown); - }; - modalEl.addEventListener('shown.bs.modal', closeOnShown); - } - } else { - // Add Mode - clearAddAliasForm(); - // Set focus to the first input field when modal is shown - const focusOnShown = () => { - document.getElementById('aliasName')?.focus({ preventScroll: true }); - modalEl.removeEventListener('shown.bs.modal', focusOnShown); - }; - modalEl.addEventListener('shown.bs.modal', focusOnShown); + if (!modalEl) return; + + // store who opened the modal + let lastTriggerEl = null; + + // When shown, determine Add or Edit mode + modalEl.addEventListener('show.bs.modal', async (ev) => { + lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) + + // check Add or Edit mode based on presence of data-host-id in the trigger element + const id = Number(lastTriggerEl?.dataset?.aliasId); + + if (Number.isFinite(id)) { + // EDIT MODE + try { + await editAlias(id); + } catch (err) { + showToast(err?.message || "Error loading alias", false); + // Close modal + modalEl.addEventListener('shown.bs.modal', () => { + closeAddAliasModal(lastTriggerEl); + }, { once: true }); } - }); + } else { + // ADD MODE + clearAddAliasForm(); + // Set focus to the first input field when modal is shown + modalEl.addEventListener('shown.bs.modal', () => { + document.getElementById('aliasName')?.focus({ preventScroll: true }); + }, { once: true }); + } + }); - // When hiding, restore focus to the trigger element - modalEl.addEventListener('hide.bs.modal', () => { - const active = document.activeElement; - if (active && modalEl.contains(active)) { - if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') { - lastTriggerEl.focus({ preventScroll: true }); - } else { - active.blur(); - } - } - }); + // When hiding, restore focus to the trigger element + modalEl.addEventListener('hide.bs.modal', () => { + const active = document.activeElement; + if (active && modalEl.contains(active)) { + lastTriggerEl?.focus?.({ preventScroll: true }) || active.blur(); + } + }); - // When hidden, reset the form - modalEl.addEventListener('hidden.bs.modal', () => { - // reset form fields - clearAddAliasForm(); - // pulizia ref del trigger - lastTriggerEl = null; - }); - } + // When hidden, reset the form + modalEl.addEventListener('hidden.bs.modal', () => { + // reset form fields + clearAddAliasForm(); + // pulizia ref del trigger + lastTriggerEl = null; + }); +} - // Button event delegation (click) - document.addEventListener('click', async (e) => { - const el = e.target.closest('[data-action]'); - if (!el) return; +// ----------------------------- +// GLOBAL EVENTS INIT +// ----------------------------- +function initEvents() { + document.addEventListener('click', handleActionClick); + document.addEventListener('click', handleSortClick); + document.addEventListener('keydown', handleKeyboard); + document.addEventListener('submit', handleForms); +} - const action = el.dataset.action; - const handler = actionHandlers[action]; - if (!handler) return; +// ----------------------------- +// CLICK (DATA-ACTION) +// ----------------------------- +async function handleActionClick(e) { + const el = e.target.closest('[data-action]'); + if (!el) return; - // Execute handler - try { - await handler(e, el); - } catch (err) { - console.error(err?.message || 'Action error'); - showToast(err?.message || 'Action error', false); - } - }); + const action = el.dataset.action; + const handler = actionHandlers[action]; + if (!handler) return; + + // Execute handler + try { + await handler(e, el); + } catch (err) { + console.error(err?.message || 'Action error'); + showToast(err?.message || 'Action error', false); + } +} + +// ----------------------------- +// KEYBOARD (ESC + accessibility) +// ----------------------------- +function handleKeyboard(e) { + // Ignore if focus is in a typing field + const tag = (e.target.tagName || "").toLowerCase(); + const isTypingField = + tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; + + // global ESC key listener to clear search and reset sorting + if (e.key === "Escape" && !isTypingField) { + // Prevent default form submission + e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) + resetSorting(sortState); + clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) + filterAliases(''); // ripristina tabella + } // Button event delegation (Enter, Space) - document.addEventListener('keydown', async (e) => { - const isEnter = e.key === 'Enter'; - const isSpace = e.key === ' ' || e.key === 'Spacebar'; - if (!isEnter && !isSpace) return; - - const el = e.target.closest('[data-action]'); - if (!el) return; - - // Space/Enter - if (el.tagName === 'BUTTON') return; - // Trigger click event - el.click(); - }); + const isEnter = e.key === 'Enter'; + const isSpace = e.key === ' '; + if (!isEnter && !isSpace) return; - // Submit Form - const form = document.getElementById('addAliasForm'); - if (form) { - form.addEventListener('submit', handleAddAliasSubmit); + const el = e.target.closest('[data-action]'); + if (!el) return; + + // Space/Enter + if (el.tagName === 'BUTTON') return; + // Trigger click event + el.click(); +} + +// ----------------------------- +// FORM SUBMIT (delegation) +// ----------------------------- +function handleForms(e) { + if (e.target.id === 'addAliasForm') { + handleAddAliasSubmit(e); } +} - // Submit Sort - const headers = document.querySelectorAll('thead th'); - headers.forEach((th) => { - if (th.dataset.sortable === 'false') return; - th.addEventListener('click', () => sortTable(th.cellIndex, sortState)); - }); -}); +// ----------------------------- +// SORT CLICK +// ----------------------------- +function handleSortClick(e) { + const th = e.target.closest('th[data-sortable="true"]'); + if (!th) return; + + if (th.dataset.sortable === 'false') return; + + const colIndex = Number(th.dataset.sort); + if (!Number.isInteger(colIndex)) return; + + sortTable(colIndex, sortState); +} diff --git a/frontend/js/devices.js b/frontend/js/devices.js index af886f7..6102906 100644 --- a/frontend/js/devices.js +++ b/frontend/js/devices.js @@ -703,9 +703,16 @@ const actionHandlers = { }; // ----------------------------- -// DOMContentLoaded: initialize everything +// DOMContentLoaded: bootstrap app // ----------------------------- document.addEventListener("DOMContentLoaded", async () => { + await initApp(); +}); + +// ----------------------------- +// APP INIT +// ----------------------------- +async function initApp() { // Load modals (Bootstrap 5 requires JS initialization for dynamic content) try { @@ -715,9 +722,6 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading modals", false); } - // Init UI sort (aria-sort, arrows) - initSortableTable(); - // Load data (devices) try { await loadDevices(); @@ -726,149 +730,171 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading devices", false); } + initUI(); + initEvents(); +} + +// ----------------------------- +// UI INIT +// ----------------------------- +function initUI() { + // Init UI sort (aria-sort, arrows) + initSortableTable(); + initSearch(); + initModalLifecycle(); +} + +// ----------------------------- +// SEARCH +// ----------------------------- +function initSearch() { // search bar const input = document.getElementById("searchInput"); - if (input) { - // clean input on load - input.value = ""; - // live filter for each keystroke - input.addEventListener("input", filterDevices); - // Escape management when focus is in the input - input.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) - e.stopPropagation(); // evita che arrivi al listener globale - resetSorting(sortState); - clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) - filterDevices(''); // ripristina tabella - } - }); - } + if (!input) return; - // global ESC key listener to clear search and reset sorting - document.addEventListener("keydown", (e) => { - // Ignore if focus is in a typing field - const tag = (e.target.tagName || "").toLowerCase(); - const isTypingField = - tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; - - if (e.key === "Escape" && !isTypingField) { - // Prevent default form submission - e.preventDefault(); - resetSorting(sortState); - clearSearch(); - filterDevices(''); - } - }); + // clean input on load + input.value = ""; +} + +// ----------------------------- +// MODAL LIFECYCLE (ADD / EDIT) +// ----------------------------- +function initModalLifecycle() { // Modal show/hidden events to prepare/reset the form const modalEl = document.getElementById('addHostModal'); - if (modalEl) { - - // store who opened the modal - let lastTriggerEl = null; - - // When shown, determine Add or Edit mode - modalEl.addEventListener('show.bs.modal', async (ev) => { - lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) - const formEl = document.getElementById('addHostForm'); - - // Security check - if (!formEl) return; - - // check Add or Edit mode - const idAttr = lastTriggerEl?.getAttribute?.('data-device-id'); - const id = idAttr ? idAttr : null; - - if (id !== null) { - // Edit Mode - try { - await editHost(id); - } catch (err) { - showToast(err?.message || "Error loading host for edit", false); - // Close modal - const closeOnShown = () => { - closeAddHostModal(lastTriggerEl); - modalEl.removeEventListener('shown.bs.modal', closeOnShown); - }; - modalEl.addEventListener('shown.bs.modal', closeOnShown); - } - } else { - // Add Mode - clearAddHostForm(); - // Set focus to the first input field when modal is shown - const focusOnShown = () => { - document.getElementById('hostName')?.focus({ preventScroll: true }); - modalEl.removeEventListener('shown.bs.modal', focusOnShown); - }; - modalEl.addEventListener('shown.bs.modal', focusOnShown); + if (!modalEl) return; + + // store who opened the modal + let lastTriggerEl = null; + + // When shown, determine Add or Edit mode + modalEl.addEventListener('show.bs.modal', async (ev) => { + lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) + + // check Add or Edit mode based on presence of data-host-id in the trigger element + const id = lastTriggerEl?.dataset?.deviceId ?? null; + + if (id !== null) { + // EDIT MODE + try { + await editHost(id); + } catch (err) { + showToast(err?.message || "Error loading host", false); + // Close modal + modalEl.addEventListener('shown.bs.modal', () => { + closeAddHostModal(lastTriggerEl); + }, { once: true }); } - }); + } else { + // ADD MODE + clearAddHostForm(); + // Set focus to the first input field when modal is shown + modalEl.addEventListener('shown.bs.modal', () => { + document.getElementById('hostName')?.focus({ preventScroll: true }); + }, { once: true }); + } + }); - // When hiding, restore focus to the trigger element - modalEl.addEventListener('hide.bs.modal', () => { - const active = document.activeElement; - if (active && modalEl.contains(active)) { - if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') { - lastTriggerEl.focus({ preventScroll: true }); - } else { - active.blur(); - } - } - }); + // When hiding, restore focus to the trigger element + modalEl.addEventListener('hide.bs.modal', () => { + const active = document.activeElement; + if (active && modalEl.contains(active)) { + lastTriggerEl?.focus?.({ preventScroll: true }) || active.blur(); + } + }); - // When hidden, reset the form - modalEl.addEventListener('hidden.bs.modal', () => { - // reset form fields - clearAddHostForm(); - // pulizia ref del trigger - lastTriggerEl = null; - }); - } + // When hidden, reset the form + modalEl.addEventListener('hidden.bs.modal', () => { + // reset form fields + clearAddHostForm(); + // pulizia ref del trigger + lastTriggerEl = null; + }); +} - // Button event delegation (click) - document.addEventListener('click', async (e) => { - const el = e.target.closest('[data-action]'); - if (!el) return; +// ----------------------------- +// GLOBAL EVENTS INIT +// ----------------------------- +function initEvents() { + document.addEventListener('click', handleActionClick); + document.addEventListener('click', handleSortClick); + document.addEventListener('keydown', handleKeyboard); + document.addEventListener('submit', handleForms); +} - const action = el.dataset.action; - const handler = actionHandlers[action]; - if (!handler) return; +// ----------------------------- +// CLICK (DATA-ACTION) +// ----------------------------- +async function handleActionClick(e) { + const el = e.target.closest('[data-action]'); + if (!el) return; - // Execute handler - try { - await handler(e, el); - } catch (err) { - console.error(err?.message || 'Action error'); - showToast(err?.message || 'Action error', false); - } - }); + const action = el.dataset.action; + const handler = actionHandlers[action]; + if (!handler) return; + + // Execute handler + try { + await handler(e, el); + } catch (err) { + console.error(err?.message || 'Action error'); + showToast(err?.message || 'Action error', false); + } +} + +// ----------------------------- +// KEYBOARD (ESC + accessibility) +// ----------------------------- +function handleKeyboard(e) { + // Ignore if focus is in a typing field + const tag = (e.target.tagName || "").toLowerCase(); + const isTypingField = + tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; + + // global ESC key listener to clear search and reset sorting + if (e.key === "Escape" && !isTypingField) { + // Prevent default form submission + e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) + resetSorting(sortState); + clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) + filterDevices(''); // ripristina tabella + } // Button event delegation (Enter, Space) - document.addEventListener('keydown', async (e) => { - const isEnter = e.key === 'Enter'; - const isSpace = e.key === ' ' || e.key === 'Spacebar'; - if (!isEnter && !isSpace) return; - - const el = e.target.closest('[data-action]'); - if (!el) return; - - // Space/Enter - if (el.tagName === 'BUTTON') return; - // Trigger click event - el.click(); - }); + const isEnter = e.key === 'Enter'; + const isSpace = e.key === ' '; + if (!isEnter && !isSpace) return; - // Submit Form - const form = document.getElementById('addHostForm'); - if (form) { - form.addEventListener('submit', handleAddHostSubmit); + const el = e.target.closest('[data-action]'); + if (!el) return; + + // Space/Enter + if (el.tagName === 'BUTTON') return; + // Trigger click event + el.click(); +} + +// ----------------------------- +// FORM SUBMIT (delegation) +// ----------------------------- +function handleForms(e) { + if (e.target.id === 'addHostForm') { + handleAddHostSubmit(e); } +} - // Submit Sort - const headers = document.querySelectorAll('thead th'); - headers.forEach((th) => { - if (th.dataset.sortable === 'false') return; - th.addEventListener('click', () => sortTable(th.cellIndex, sortState)); - }); -}); +// ----------------------------- +// SORT CLICK +// ----------------------------- +function handleSortClick(e) { + const th = e.target.closest('th[data-sortable="true"]'); + if (!th) return; + + if (th.dataset.sortable === 'false') return; + + const colIndex = Number(th.dataset.sort); + if (!Number.isInteger(colIndex)) return; + + sortTable(colIndex, sortState); +} diff --git a/frontend/js/hosts.js b/frontend/js/hosts.js index 9b7795c..f65e328 100644 --- a/frontend/js/hosts.js +++ b/frontend/js/hosts.js @@ -610,9 +610,16 @@ const actionHandlers = { }; // ----------------------------- -// DOMContentLoaded: initialize everything +// DOMContentLoaded: bootstrap app // ----------------------------- document.addEventListener("DOMContentLoaded", async () => { + await initApp(); +}); + +// ----------------------------- +// APP INIT +// ----------------------------- +async function initApp() { // Load modals (Bootstrap 5 requires JS initialization for dynamic content) try { @@ -622,9 +629,6 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading modals", false); } - // Init UI sort (aria-sort, arrows) - initSortableTable(); - // Load data (hosts) try { await loadHosts(); @@ -633,149 +637,171 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading hosts", false); } + initUI(); + initEvents(); +} + +// ----------------------------- +// UI INIT +// ----------------------------- +function initUI() { + // Init UI sort (aria-sort, arrows) + initSortableTable(); + initSearch(); + initModalLifecycle(); +} + +// ----------------------------- +// SEARCH +// ----------------------------- +function initSearch() { // search bar const input = document.getElementById("searchInput"); - if (input) { - // clean input on load - input.value = ""; - // live filter for each keystroke - input.addEventListener("input", filterHosts); - // Escape management when focus is in the input - input.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) - e.stopPropagation(); // evita che arrivi al listener globale - resetSorting(sortState); - clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) - filterHosts(''); // ripristina tabella - } - }); - } + if (!input) return; - // global ESC key listener to clear search and reset sorting - document.addEventListener("keydown", (e) => { - // Ignore if focus is in a typing field - const tag = (e.target.tagName || "").toLowerCase(); - const isTypingField = - tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; - - if (e.key === "Escape" && !isTypingField) { - // Prevent default form submission - e.preventDefault(); - resetSorting(sortState); - clearSearch(); - filterHosts(''); - } - }); + // clean input on load + input.value = ""; +} + +// ----------------------------- +// MODAL LIFECYCLE (ADD / EDIT) +// ----------------------------- +function initModalLifecycle() { // Modal show/hidden events to prepare/reset the form const modalEl = document.getElementById('addHostModal'); - if (modalEl) { - - // store who opened the modal - let lastTriggerEl = null; - - // When shown, determine Add or Edit mode - modalEl.addEventListener('show.bs.modal', async (ev) => { - lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) - const formEl = document.getElementById('addHostForm'); - - // Security check - if (!formEl) return; - - // check Add or Edit mode - const idAttr = lastTriggerEl?.getAttribute?.('data-host-id'); - const id = idAttr ? Number(idAttr) : null; - - if (Number.isFinite(id)) { - // Edit Mode - try { - await editHost(id); - } catch (err) { - showToast(err?.message || "Error loading host for edit", false); - // Close modal - const closeOnShown = () => { - closeAddHostModal(lastTriggerEl); - modalEl.removeEventListener('shown.bs.modal', closeOnShown); - }; - modalEl.addEventListener('shown.bs.modal', closeOnShown); - } - } else { - // Add Mode - clearAddHostForm(); - // Set focus to the first input field when modal is shown - const focusOnShown = () => { - document.getElementById('hostName')?.focus({ preventScroll: true }); - modalEl.removeEventListener('shown.bs.modal', focusOnShown); - }; - modalEl.addEventListener('shown.bs.modal', focusOnShown); + if (!modalEl) return; + + // store who opened the modal + let lastTriggerEl = null; + + // When shown, determine Add or Edit mode + modalEl.addEventListener('show.bs.modal', async (ev) => { + lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) + + // check Add or Edit mode based on presence of data-host-id in the trigger element + const id = Number(lastTriggerEl?.dataset?.hostId); + + if (Number.isFinite(id)) { + // EDIT MODE + try { + await editHost(id); + } catch (err) { + showToast(err?.message || "Error loading host", false); + // Close modal + modalEl.addEventListener('shown.bs.modal', () => { + closeAddHostModal(lastTriggerEl); + }, { once: true }); } - }); + } else { + // ADD MODE + clearAddHostForm(); + // Set focus to the first input field when modal is shown + modalEl.addEventListener('shown.bs.modal', () => { + document.getElementById('hostName')?.focus({ preventScroll: true }); + }, { once: true }); + } + }); - // When hiding, restore focus to the trigger element - modalEl.addEventListener('hide.bs.modal', () => { - const active = document.activeElement; - if (active && modalEl.contains(active)) { - if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') { - lastTriggerEl.focus({ preventScroll: true }); - } else { - active.blur(); - } - } - }); + // When hiding, restore focus to the trigger element + modalEl.addEventListener('hide.bs.modal', () => { + const active = document.activeElement; + if (active && modalEl.contains(active)) { + lastTriggerEl?.focus?.({ preventScroll: true }) || active.blur(); + } + }); - // When hidden, reset the form - modalEl.addEventListener('hidden.bs.modal', () => { - // reset form fields - clearAddHostForm(); - // pulizia ref del trigger - lastTriggerEl = null; - }); - } + // When hidden, reset the form + modalEl.addEventListener('hidden.bs.modal', () => { + // reset form fields + clearAddHostForm(); + // pulizia ref del trigger + lastTriggerEl = null; + }); +} - // Button event delegation (click) - document.addEventListener('click', async (e) => { - const el = e.target.closest('[data-action]'); - if (!el) return; +// ----------------------------- +// GLOBAL EVENTS INIT +// ----------------------------- +function initEvents() { + document.addEventListener('click', handleActionClick); + document.addEventListener('click', handleSortClick); + document.addEventListener('keydown', handleKeyboard); + document.addEventListener('submit', handleForms); +} - const action = el.dataset.action; - const handler = actionHandlers[action]; - if (!handler) return; +// ----------------------------- +// CLICK (DATA-ACTION) +// ----------------------------- +async function handleActionClick(e) { + const el = e.target.closest('[data-action]'); + if (!el) return; - // Execute handler - try { - await handler(e, el); - } catch (err) { - console.error(err?.message || 'Action error'); - showToast(err?.message || 'Action error', false); - } - }); + const action = el.dataset.action; + const handler = actionHandlers[action]; + if (!handler) return; + + // Execute handler + try { + await handler(e, el); + } catch (err) { + console.error(err?.message || 'Action error'); + showToast(err?.message || 'Action error', false); + } +} + +// ----------------------------- +// KEYBOARD (ESC + accessibility) +// ----------------------------- +function handleKeyboard(e) { + // Ignore if focus is in a typing field + const tag = (e.target.tagName || "").toLowerCase(); + const isTypingField = + tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; + + // global ESC key listener to clear search and reset sorting + if (e.key === "Escape" && !isTypingField) { + // Prevent default form submission + e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) + resetSorting(sortState); + clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) + filterHosts(''); // ripristina tabella + } // Button event delegation (Enter, Space) - document.addEventListener('keydown', async (e) => { - const isEnter = e.key === 'Enter'; - const isSpace = e.key === ' ' || e.key === 'Spacebar'; - if (!isEnter && !isSpace) return; - - const el = e.target.closest('[data-action]'); - if (!el) return; - - // Space/Enter - if (el.tagName === 'BUTTON') return; - // Trigger click event - el.click(); - }); + const isEnter = e.key === 'Enter'; + const isSpace = e.key === ' '; + if (!isEnter && !isSpace) return; - // Submit Form - const form = document.getElementById('addHostForm'); - if (form) { - form.addEventListener('submit', handleAddHostSubmit); + const el = e.target.closest('[data-action]'); + if (!el) return; + + // Space/Enter + if (el.tagName === 'BUTTON') return; + // Trigger click event + el.click(); +} + +// ----------------------------- +// FORM SUBMIT (delegation) +// ----------------------------- +function handleForms(e) { + if (e.target.id === 'addHostForm') { + handleAddHostSubmit(e); } +} - // Submit Sort - const headers = document.querySelectorAll('thead th'); - headers.forEach((th) => { - if (th.dataset.sortable === 'false') return; - th.addEventListener('click', () => sortTable(th.cellIndex, sortState)); - }); -}); +// ----------------------------- +// SORT CLICK +// ----------------------------- +function handleSortClick(e) { + const th = e.target.closest('th[data-sortable="true"]'); + if (!th) return; + + if (th.dataset.sortable === 'false') return; + + const colIndex = Number(th.dataset.sort); + if (!Number.isInteger(colIndex)) return; + + sortTable(colIndex, sortState); +} diff --git a/frontend/js/leases.js b/frontend/js/leases.js index 4e9adc9..f704d3d 100644 --- a/frontend/js/leases.js +++ b/frontend/js/leases.js @@ -574,9 +574,16 @@ const actionHandlers = { }; // ----------------------------- -// DOMContentLoaded: initialize everything +// DOMContentLoaded: bootstrap app // ----------------------------- document.addEventListener("DOMContentLoaded", async () => { + await initApp(); +}); + +// ----------------------------- +// APP INIT +// ----------------------------- +async function initApp() { // Load modals (Bootstrap 5 requires JS initialization for dynamic content) try { @@ -586,9 +593,6 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading modals", false); } - // Init UI sort (aria-sort, arrows) - initSortableTable(); - // Load data (leases) try { await loadLeases(); @@ -597,137 +601,163 @@ document.addEventListener("DOMContentLoaded", async () => { showToast(err?.message || "Error loading dhcp leases", false); } + initUI(); + initEvents(); +} + +// ----------------------------- +// UI INIT +// ----------------------------- +function initUI() { + // Init UI sort (aria-sort, arrows) + initSortableTable(); + initSearch(); + initModalLifecycle(); +} + +// ----------------------------- +// SEARCH +// ----------------------------- +function initSearch() { // search bar const input = document.getElementById("searchInput"); - if (input) { - // clean input on load - input.value = ""; - // live filter for each keystroke - input.addEventListener("input", filterLeases); - // Escape management when focus is in the input - input.addEventListener("keydown", (e) => { - if (e.key === "Escape") { - e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) - e.stopPropagation(); // evita che arrivi al listener globale - resetSorting(sortState); - clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) - filterLeases(''); // ripristina tabella - } - }); - } + if (!input) return; - // global ESC key listener to clear search and reset sorting - document.addEventListener("keydown", (e) => { - // Ignore if focus is in a typing field - const tag = (e.target.tagName || "").toLowerCase(); - const isTypingField = - tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; - - if (e.key === "Escape" && !isTypingField) { - // Prevent default form submission - e.preventDefault(); - resetSorting(sortState); - clearSearch(); - filterLeases(''); - } - }); + // clean input on load + input.value = ""; +} + +// ----------------------------- +// MODAL LIFECYCLE (ADD / EDIT) +// ----------------------------- +function initModalLifecycle() { // Modal show/hidden events to prepare/reset the form const modalEl = document.getElementById('addHostModal'); - if (modalEl) { - - // store who opened the modal - let lastTriggerEl = null; + if (!modalEl) return; - // When shown, determine Add or Edit mode - modalEl.addEventListener('show.bs.modal', async (ev) => { - lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) - const formEl = document.getElementById('addHostForm'); + // store who opened the modal + let lastTriggerEl = null; - // Security check - if (!formEl) return; + // When shown, determine Add or Edit mode + modalEl.addEventListener('show.bs.modal', async (ev) => { + lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit) - // check Add or Edit mode - const idAttr = lastTriggerEl?.getAttribute?.('data-lease-id'); - const id = idAttr ? Number(idAttr) : null; + // check Add or Edit mode based on presence of data-host-id in the trigger element + const id = Number(lastTriggerEl?.dataset?.leaseId); + if (Number.isFinite(id)) { try { await addHost(id); } catch (err) { showToast(err?.message || "Error loading host for edit", false); // Close modal - const closeOnShown = () => { + modalEl.addEventListener('shown.bs.modal', () => { closeAddHostModal(lastTriggerEl); - modalEl.removeEventListener('shown.bs.modal', closeOnShown); - }; - modalEl.addEventListener('shown.bs.modal', closeOnShown); + }, { once: true }); } - }); + } + }); - // When hiding, restore focus to the trigger element - modalEl.addEventListener('hide.bs.modal', () => { - const active = document.activeElement; - if (active && modalEl.contains(active)) { - if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') { - lastTriggerEl.focus({ preventScroll: true }); - } else { - active.blur(); - } - } - }); + // When hiding, restore focus to the trigger element + modalEl.addEventListener('hide.bs.modal', () => { + const active = document.activeElement; + if (active && modalEl.contains(active)) { + lastTriggerEl?.focus?.({ preventScroll: true }) || active.blur(); + } + }); - // When hidden, reset the form - modalEl.addEventListener('hidden.bs.modal', () => { - // reset form fields - clearAddHostForm(); - // pulizia ref del trigger - lastTriggerEl = null; - }); - } + // When hidden, reset the form + modalEl.addEventListener('hidden.bs.modal', () => { + // reset form fields + clearAddHostForm(); + // pulizia ref del trigger + lastTriggerEl = null; + }); +} - // Button event delegation (click) - document.addEventListener('click', async (e) => { - const el = e.target.closest('[data-action]'); - if (!el) return; +// ----------------------------- +// GLOBAL EVENTS INIT +// ----------------------------- +function initEvents() { + document.addEventListener('click', handleActionClick); + document.addEventListener('click', handleSortClick); + document.addEventListener('keydown', handleKeyboard); + document.addEventListener('submit', handleForms); +} - const action = el.dataset.action; - const handler = actionHandlers[action]; - if (!handler) return; +// ----------------------------- +// CLICK (DATA-ACTION) +// ----------------------------- +async function handleActionClick(e) { + const el = e.target.closest('[data-action]'); + if (!el) return; - // Execute handler - try { - await handler(e, el); - } catch (err) { - console.error(err?.message || 'Action error'); - showToast(err?.message || 'Action error', false); - } - }); + const action = el.dataset.action; + const handler = actionHandlers[action]; + if (!handler) return; + + // Execute handler + try { + await handler(e, el); + } catch (err) { + console.error(err?.message || 'Action error'); + showToast(err?.message || 'Action error', false); + } +} + +// ----------------------------- +// KEYBOARD (ESC + accessibility) +// ----------------------------- +function handleKeyboard(e) { + // Ignore if focus is in a typing field + const tag = (e.target.tagName || "").toLowerCase(); + const isTypingField = + tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable; + + // global ESC key listener to clear search and reset sorting + if (e.key === "Escape" && !isTypingField) { + // Prevent default form submission + e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) + resetSorting(sortState); + clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) + filterLeases(''); // ripristina tabella + } // Button event delegation (Enter, Space) - document.addEventListener('keydown', async (e) => { - const isEnter = e.key === 'Enter'; - const isSpace = e.key === ' ' || e.key === 'Spacebar'; - if (!isEnter && !isSpace) return; - - const el = e.target.closest('[data-action]'); - if (!el) return; - - // Space/Enter - if (el.tagName === 'BUTTON') return; - // Trigger click event - el.click(); - }); + const isEnter = e.key === 'Enter'; + const isSpace = e.key === ' ' || e.key === 'Spacebar'; + if (!isEnter && !isSpace) return; - // Submit Form - const form = document.getElementById('addHostForm'); - if (form) { - form.addEventListener('submit', handleAddHostSubmit); + const el = e.target.closest('[data-action]'); + if (!el) return; + + // Space/Enter + if (el.tagName === 'BUTTON') return; + // Trigger click event + el.click(); +} + +// ----------------------------- +// FORM SUBMIT (delegation) +// ----------------------------- +function handleForms(e) { + if (e.target.id === 'addHostForm') { + handleAddHostSubmit(e); } +} - // Submit Sort - const headers = document.querySelectorAll('thead th'); - headers.forEach((th) => { - if (th.dataset.sortable === 'false') return; - th.addEventListener('click', () => sortTable(th.cellIndex, sortState)); - }); -}); +// ----------------------------- +// SORT CLICK +// ----------------------------- +function handleSortClick(e) { + const th = e.target.closest('th[data-sortable="true"]'); + if (!th) return; + + if (th.dataset.sortable === 'false') return; + + const colIndex = Number(th.dataset.sort); + if (!Number.isInteger(colIndex)) return; + + sortTable(colIndex, sortState); +} diff --git a/frontend/leases.html b/frontend/leases.html index 54e648d..ad29feb 100644 --- a/frontend/leases.html +++ b/frontend/leases.html @@ -108,13 +108,13 @@
Hostname IP Address MAC AddressDescriptionOptions Actions Hostname IP Address MAC AddressDescriptionOptions Actions
- - - - - - - + + + + + + + -- 2.47.3
IP AddressMAC Hostname Start End State Actions IP AddressMAC Hostname Start End State Actions