<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>
<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>
<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>
};
// -----------------------------
-// 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 {
showToast(err?.message || "Error loading modals", false);
}
- // Init UI sort (aria-sort, arrows)
- initSortableTable();
-
// Load data (aliases)
try {
await loadAliases();
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);
+}
};
// -----------------------------
-// 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 {
showToast(err?.message || "Error loading modals", false);
}
- // Init UI sort (aria-sort, arrows)
- initSortableTable();
-
// Load data (devices)
try {
await loadDevices();
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);
+}
};
// -----------------------------
-// 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 {
showToast(err?.message || "Error loading modals", false);
}
- // Init UI sort (aria-sort, arrows)
- initSortableTable();
-
// Load data (hosts)
try {
await loadHosts();
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);
+}
};
// -----------------------------
-// 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 {
showToast(err?.message || "Error loading modals", false);
}
- // Init UI sort (aria-sort, arrows)
- initSortableTable();
-
// Load data (leases)
try {
await loadLeases();
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);
+}
<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>