]> git.giorgioravera.it Git - network-manager.git/commitdiff
review of DOMContentLoaded
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Tue, 26 May 2026 15:13:03 +0000 (17:13 +0200)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Tue, 26 May 2026 15:13:03 +0000 (17:13 +0200)
frontend/aliases.html
frontend/devices.html
frontend/hosts.html
frontend/js/aliases.js
frontend/js/devices.js
frontend/js/hosts.js
frontend/js/leases.js
frontend/leases.html

index 4d587f1a0d3be8516fdf65ad958cd5143db0bacb..43210029f6ae81d6a939b845f4d3fa8ffe7bcc1c 100644 (file)
     <table id="dataTable" class="table table-bordered table-hover align-middle d-none">
         <thead class="table-light">
             <tr>
-                <th data-type="string" data-sortable="true">Alias      <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Target     <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Description<span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Options    <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="false">Actions   <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="0">Alias      <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="1">Target     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="2">Description<span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="3">Options    <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="false" data-sort="4">Actions   <span class="sort-arrow" aria-hidden="true"></span></th>
             </tr>
         </thead>
         <tbody></tbody>
index f908c9dbc9bcafc686adf540535e8a02321bd73c..3b9d8575f6aceb6ec9a37128f52fef4581118d55 100644 (file)
     <table id="dataTable" class="table table-bordered table-hover align-middle d-none">
         <thead class="table-light">
             <tr>
-                <th data-type="ipv4"   data-sortable="true">IP Address <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="mac"    data-sortable="true">MAC Address<span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Hostname   <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Description<span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">State      <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Active     <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="false">Actions   <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="ipv4"   data-sortable="true" data-sort="0">IP Address <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="mac"    data-sortable="true" data-sort="1">MAC Address<span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="2">Hostname   <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="3">Description<span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="4">State      <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="5">Active     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="false" data-sort="6">Actions   <span class="sort-arrow" aria-hidden="true"></span></th>
             </tr>
         </thead>
         <tbody></tbody>
index 69dd03208c4d1943e9138b8fd3d2eb257ddae02f..00bd53b49c1b880cd837e7e93d4b4556b4fb9e33 100644 (file)
     <table id="dataTable" class="table table-bordered table-hover align-middle d-none">
         <thead class="table-light">
             <tr>
-                <th data-type="string" data-sortable="true">Hostname   <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="ipv4"   data-sortable="true">IP Address <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="mac"    data-sortable="true">MAC Address<span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Description<span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Options    <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="false">Actions   <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="0">Hostname   <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="ipv4"   data-sortable="true" data-sort="1">IP Address <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="mac"    data-sortable="true" data-sort="2">MAC Address<span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="3">Description<span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="4">Options    <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="false" data-sort="5">Actions    <span class="sort-arrow" aria-hidden="true"></span></th>
             </tr>
         </thead>
         <tbody></tbody>
index 2d88a9f06a040ecb89991ae492e0f2301d00ff04..a04b343bbd462d331c6d68cbadb31383b3e97a7b 100644 (file)
@@ -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);
+}
index af886f702b13d7b3e1433d2074c89d88608e0860..610290636e5ae1e0904131f87f918e2eae84bcbf 100644 (file)
@@ -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);
+}
index 9b7795c71054777b7906838b0263a4dc04744a7b..f65e32817067f1337c90ba5139c0c7e8d4b1a96e 100644 (file)
@@ -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);
+}
index 4e9adc9680ce0553f27660325e5270b312387986..f704d3dca3a64b2acb5112a57bac34173d62cd46 100644 (file)
@@ -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);
+}
index 54e648d9a36a4f7f160f4dca139e1719ed8b4bf5..ad29feb891aa150aac1c808a0d8713494d4f3c20 100644 (file)
     <table id="dataTable" class="table table-bordered table-hover align-middle d-none">
         <thead class="table-light">
             <tr>
-                <th data-type="ipv4"   data-sortable="true">IP Address<span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="mac"    data-sortable="true">MAC       <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Hostname  <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">Start     <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">End       <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="true">State     <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" data-sortable="false">Actions  <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="ipv4"   data-sortable="true" data-sort="0">IP Address<span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="mac"    data-sortable="true" data-sort="1">MAC       <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="2">Hostname  <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="3">Start     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="4">End       <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true" data-sort="5">State     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="false" data-sort="6">Actions  <span class="sort-arrow" aria-hidden="true"></span></th>
             </tr>
         </thead>
         <tbody></tbody>