/** * gridRenderer.js — Data-driven grid renderer for imported.php * * Reads window.gridData (array of row objects) and window.gridMeta (column defs, config). * Renders only visible rows into DOM. Propagate/edit operates on gridData array, * then re-renders visible rows. Save reads from gridData, not DOM. */ (function () { "use strict"; const PAGE_SIZE = 20; let revealedCount = PAGE_SIZE; let dropdownOptionsCache = {}; // fieldId -> [{id, text}] — used only for small lists const dropdownNameCache = {}; // "fieldId_valueId" -> label let clientData = []; // loaded from get_clienti.php let fixedFieldCache = window.fixedFieldDataCache || {}; window.fixedFieldDataCache = fixedFieldCache; const data = window.gridData || []; const meta = window.gridMeta || {}; const columns = meta.columns || []; let totalRows = data.length; // ── DOM refs ──────────────────────────────────────────────────────────── let rowContainer = null; let headerContainer = null; let topContainer = null; let statusEl = null; // ── Helpers ───────────────────────────────────────────────────────────── function esc(str) { if (str === null || str === undefined) return ""; const d = document.createElement("div"); d.textContent = String(str); return d.innerHTML; } function escAttr(str) { if (str === null || str === undefined) return ""; return String(str) .replace(/&/g, "&") .replace(/"/g, """) .replace(/'/g, "'") .replace(//g, ">"); } function getDetailValue(rowIndex, mappingId) { return data[rowIndex].details[String(mappingId)] ?? ""; } function setDetailValue(rowIndex, mappingId, value) { data[rowIndex].details[String(mappingId)] = value; data[rowIndex]._dirty = true; } function getFixedValue(rowIndex, key) { return data[rowIndex].fixedFields[key] ?? ""; } function setFixedValue(rowIndex, key, value) { data[rowIndex].fixedFields[key] = value; data[rowIndex]._dirty = true; } // ── Client data (AJAX Select2, no bulk loading) ────────────────────── function formatClientLabel(client) { const nome = ( client.Nominativo || client.text || "Nome non disponibile" ).trim(); const id = client.IdCliente || client.id || ""; const codiceCliente = ( client.CodiceCliente ?? client.codiceCliente ?? "" ) .toString() .trim(); // Lettera estratta (parte dopo "_", altrimenti prima lettera del codice) const suffix = (codiceCliente.split("_")[1] || "").trim(); const shortCode = suffix || (codiceCliente ? codiceCliente.charAt(0) : "--"); // Nome - I - 123 return `${nome} - ${shortCode} - ${id}`; } // Cache of resolved client names: id → name const clientNameCache = {}; // Build select with only the selected option function buildClientOptionsHTML(selectedId) { let html = ''; if (selectedId) { const label = clientNameCache[selectedId] || selectedId; html += ``; } return html; } // Pre-resolve all unique client IDs used in data rows, then re-render async function resolveClientNames() { const ids = new Set(); data.forEach((row) => { if (row.idclient) ids.add(String(row.idclient)); if (row.cliente_fornitore_id) ids.add(String(row.cliente_fornitore_id)); // Fixed fields that are client-sourced if (row.fixedFields) { for (const [key, val] of Object.entries(row.fixedFields)) { const cfg = fixedFieldApiConfig[key]; if (cfg && cfg.source === "clients" && val) ids.add(String(val)); } } if (row.fixedFields && row.fixedFields.ClienteFatturazione) { ids.add(String(row.fixedFields.ClienteFatturazione)); } }); // Batch resolve via single request per ID const unresolvedIds = [...ids].filter((id) => !clientNameCache[id]); if (unresolvedIds.length === 0) return; await Promise.all( unresolvedIds.map(async (id) => { try { const resp = await fetch( "search_clienti.php?id=" + encodeURIComponent(id), ); const json = await resp.json(); const item = (json.results || [])[0]; if (item) { clientNameCache[id] = formatClientLabel(item); } } catch (e) { /* ignore */ } }), ); } // Ranking inizio/centro/fine per select con dati già in memoria. // Restituisce { matcher, sorter } da passare a select2(). function rankedLocalSelect2() { return { matcher: function (params, data) { const term = $.trim(params.term || "").toLowerCase(); if (term === "") return data; if (typeof data.text === "undefined") return null; // AND multi-termine, posizione indipendente const text = data.text.toLowerCase(); const terms = term.split(/\s+/).filter(Boolean); const all = terms.every((t) => text.indexOf(t) !== -1); return all ? data : null; }, sorter: function (results) { const term = $( ".select2-container--open .select2-search__field", ).val(); const search = (term || "").toLowerCase().trim(); if (!search) return results; const first = search.split(/\s+/)[0]; function score(text) { const t = (text || "").toLowerCase(); if (t === search) return 0; // match esatto if (t.indexOf(first) === 0) return 1; // inizia col termine if (new RegExp("(^|\\s)" + escapeRegExp(first)).test(t)) return 2; // inizio parola return 3; // in mezzo } return results.slice().sort(function (a, b) { const sa = score(a.text); const sb = score(b.text); if (sa !== sb) return sa - sb; return (a.text || "").localeCompare(b.text || "", "it", { sensitivity: "base", }); }); }, }; } function escapeRegExp(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function sortSelect2ResultsByStart(data) { const term = $(".select2-container--open .select2-search__field").val(); if (!term) { return data; } const search = term.toLowerCase().trim(); return data.sort(function (a, b) { const textA = (a.text || "").toLowerCase().trim(); const textB = (b.text || "").toLowerCase().trim(); const aStarts = textA.startsWith(search); const bStarts = textB.startsWith(search); if (aStarts && !bStarts) return -1; if (!aStarts && bStarts) return 1; return textA.localeCompare(textB, "it", { sensitivity: "base" }); }); } // Mappa: quale filtro Inattivo applicare per ogni campo cliente const clientFieldTipoMap = { ClienteFornitore: "attivo", ClienteAnalisi: "inattivo", ClienteFatturazione: "attivo", }; // Select2 AJAX config factory for client selects (parametrizzata su "tipo") function clientSelect2ConfigFor(tipo) { return { placeholder: "Search client...", allowClear: true, width: "100%", minimumInputLength: 0, sorter: sortSelect2ResultsByStart, dropdownCssClass: "select2-dropdown-smaller", ajax: { url: "search_clienti.php", dataType: "json", delay: 150, data: function (params) { const d = { q: params.term || "", limit: 20 }; if (tipo) d.tipo = tipo; return d; }, processResults: function (data) { const results = (data.results || []).map((item) => ({ ...item, text: formatClientLabel(item), })); return { results: results }; }, cache: true, }, }; } // Helper: inizializza Select2 su un elemento leggendo il tipo da data-client-tipo function initClientSelect2(el) { const tipo = el.dataset.clientTipo || ""; $(el).select2(clientSelect2ConfigFor(tipo)); } async function loadClientData() { // No longer loads all clients — AJAX Select2 handles search // Just resolve names for pre-selected values await resolveClientNames(); } // ── Fixed field data loading ─────────────────────────────────────────── const fixedFieldApiConfig = { MoltiplicatorePrezzo: { endpoint: "MoltiplicatorePrezzo", idKey: "IdMoltiplicatorePrezzo", textKey: "Descrizione", }, AnagraficaCertestObject: { endpoint: "AnagraficaCertestObject", idKey: "IdAnagrafica", textKey: "NomeAnagrafica", }, AnagraficaCertestService: { endpoint: "AnagraficaCertestService", idKey: "IdAnagrafica", textKey: "NomeAnagrafica", }, ClienteResponsabile: { endpoint: "ClienteResponsabile", idKey: "IdClienteResponsabile", textKey: "Nominativo", dependsOn: "idclient", getParams: (cid) => ({ id_cliente: cid }), }, ClienteFornitore: { source: "clients" }, ClienteFatturazione: { source: "clients" }, ClienteAnalisi: { source: "clients" }, }; let _pendingFixed = {}; async function loadFixedFieldOptions(fieldKey, clientId) { const config = fixedFieldApiConfig[fieldKey]; if (!config) return []; // Client-sourced fields — handled by AJAX Select2, skip preloading if (config.source === "clients") { fixedFieldCache[fieldKey] = []; return []; } const cacheKey = fieldKey + (clientId ? "_" + clientId : ""); if (fixedFieldCache[cacheKey]) return fixedFieldCache[cacheKey]; if (_pendingFixed[cacheKey]) return _pendingFixed[cacheKey]; const params = { field: config.endpoint }; if (config.dependsOn && clientId) { Object.assign(params, config.getParams(clientId)); } _pendingFixed[cacheKey] = (async () => { try { const resp = await fetch( "get_fixed_field_data.php?" + new URLSearchParams(params), ); const json = await resp.json(); let items = fieldKey === "ClienteResponsabile" ? json.Responsabili || [] : json.value || json.d?.results || json || []; const results = items .map((item) => ({ id: item[config.idKey], text: (item.Codice ? item.Codice + " - " : "") + (item[config.textKey] || ""), })) .sort((a, b) => String(a.text).localeCompare(String(b.text), "it", { sensitivity: "base", }), ); fixedFieldCache[cacheKey] = results; delete _pendingFixed[cacheKey]; return results; } catch (e) { delete _pendingFixed[cacheKey]; console.error("Failed to load fixed field " + fieldKey, e); return []; } })(); return _pendingFixed[cacheKey]; } async function refreshDependentFixedFieldsForRow(rowIndex) { const row = data[rowIndex]; if (!row) return; const clientId = row.idclient || ""; // Find fixed fields that depend on idclient const dependentFields = Object.keys(fixedFieldApiConfig).filter( (key) => { return fixedFieldApiConfig[key].dependsOn === "idclient"; }, ); if (dependentFields.length === 0) return; for (const fieldKey of dependentFields) { // When client changes, the old responsible is no longer reliable if ( row.fixedFields && Object.prototype.hasOwnProperty.call(row.fixedFields, fieldKey) ) { row.fixedFields[fieldKey] = ""; row._dirty = true; } // Reload options for the new client if (clientId) { await loadFixedFieldOptions(fieldKey, clientId); } } // Re-render only this row so ClienteResponsabile select is rebuilt with the new options renderSingleRow(rowIndex); // If the first row client changes, update the top propagation select too if (rowIndex === 0) { await refreshTopDependentFixedSelect( "ClienteResponsabile", clientId, ); } updateDirtyIndicator(); } // ── Custom field dropdown data loading ───────────────────────────────── async function refreshTopDependentFixedSelect(fieldKey, clientId) { if (!topContainer || !fieldKey) return; const sel = topContainer.querySelector( `.api-fixed-select[data-fixed-key="${fieldKey}"]`, ); if (!sel) return; // Destroy Select2 if already initialized if ($(sel).hasClass("select2-hidden-accessible")) { $(sel).select2("destroy"); } sel.innerHTML = ''; if (!clientId) { $(sel).select2({ placeholder: "Seleziona...", allowClear: true, width: "100%", }); return; } const items = await loadFixedFieldOptions(fieldKey, clientId); items.forEach((item) => { sel.add(new Option(item.text, item.id)); }); $(sel).select2({ placeholder: "Seleziona...", allowClear: true, width: "100%", }); } // Select2 AJAX config factory for SceltaMultipla function sceltaSelect2Config(fieldId) { return { placeholder: "Search...", allowClear: true, width: "100%", minimumInputLength: 0, ajax: { url: "search_customfield_values.php", dataType: "json", delay: 150, data: function (params) { return { field_id: fieldId, q: params.term || "", limit: 0, // 0 = no limit for custom field values }; }, processResults: function (data) { return { results: data.results || [] }; }, cache: true, }, }; } // ── Preload all data ─────────────────────────────────────────────────── async function preloadAllData() { // 1. Clients await loadClientData(); // 2. Non-dependent fixed fields const nonDependent = Object.keys(fixedFieldApiConfig).filter( (k) => !fixedFieldApiConfig[k].dependsOn && !fixedFieldApiConfig[k].source, ); await Promise.all(nonDependent.map((k) => loadFixedFieldOptions(k))); // Client-sourced fixed fields const clientSourced = Object.keys(fixedFieldApiConfig).filter( (k) => fixedFieldApiConfig[k].source === "clients", ); await Promise.all(clientSourced.map((k) => loadFixedFieldOptions(k))); // 3. Dependent fixed fields — collect unique clientIds const clientIds = new Set(); data.forEach((row) => { if (row.idclient) clientIds.add(String(row.idclient)); }); const dependent = Object.keys(fixedFieldApiConfig).filter( (k) => fixedFieldApiConfig[k].dependsOn, ); for (const fieldKey of dependent) { for (const cid of clientIds) { await loadFixedFieldOptions(fieldKey, cid); } } // 4. Warm server cache + resolve SceltaMultipla names in one request const allFieldIds = [ ...new Set( columns .filter( (c) => (c.type === "detail" || c.type === "main_field") && c.dataType === "SceltaMultipla" && c.fieldId, ) .map((c) => String(c.fieldId)), ), ]; if (allFieldIds.length > 0) { try { const resp = await fetch( "get_customfield_values.php?field_ids=" + allFieldIds.join(","), ); const json = await resp.json(); const entries = json.data ? json.data : json; for (const [fid, values] of Object.entries(entries)) { if (Array.isArray(values)) { values.forEach((v) => { dropdownNameCache[ fid + "_" + v.IdCustomFieldsValue ] = v.Valore || ""; }); } } } catch (e) { console.error("Failed to preload dropdown data:", e); } } console.log("[gridRenderer] preload done:", { clients: clientData.length, fixedFieldCache: Object.keys(fixedFieldCache), dropdownOptionsCache: Object.keys(dropdownOptionsCache), }); } // ── Cell rendering ───────────────────────────────────────────────────── function createCell(col, rowIndex, cellIndex) { const div = document.createElement("div"); div.className = "grid-cell editable-cell"; // Field color classification // Schema/customfield fields are green. // Fixed and standard fields stay white. if (col.type === "detail" || col.type === "main_field") { div.classList.add("schema-field"); } else if (col.type === "fixed") { div.classList.add("fixed-field"); } else { div.classList.add("standard-field"); } // Required field classification. // This comes from template_mapping.is_required or template_fixed_mapping.is_required. if ( col.isRequired === true || col.isRequired === 1 || col.isRequired === "1" ) { div.classList.add("required-field"); } div.dataset.col = col.key; div.dataset.colType = col.type; div.dataset.row = rowIndex; div.dataset.index = cellIndex; div.style.flex = `0 0 ${col.width}px`; const row = data[rowIndex]; switch (col.type) { case "main_field": { const val = getDetailValue(rowIndex, col.key); div.innerHTML = createInputHTML(col, val || "", rowIndex); break; } case "status": { const st = row.status || "i"; const label = st === "i" ? "Imported" : st === "P" ? "In Progress" : "To LIMS"; let html = `${label}`; if (row.commessaweb) { html += `${esc(row.commessaweb)}`; } div.innerHTML = html; div.classList.remove("editable-cell"); break; } case "idclient": { const sel = document.createElement("select"); sel.className = "cell-input dropdown-select client-select searchable-client"; sel.dataset.currentValue = row.idclient || ""; sel.innerHTML = buildClientOptionsHTML(row.idclient); div.appendChild(sel); break; } case "cliente_fornitore_id": { const sel = document.createElement("select"); sel.className = "cell-input dropdown-select client-select searchable-client fornitore-select"; sel.dataset.currentValue = row.cliente_fornitore_id || ""; sel.dataset.clientTipo = "attivo"; sel.innerHTML = buildClientOptionsHTML( row.cliente_fornitore_id, ); div.appendChild(sel); break; } case "detail": { const val = getDetailValue(rowIndex, col.key); div.innerHTML = createInputHTML(col, val, rowIndex); break; } case "tested_component": div.style.overflow = "visible"; div.innerHTML = `
`; break; case "awb": div.innerHTML = `` + `` + ``; break; case "tracking": div.className = "grid-cell tracking-info"; div.dataset.row = rowIndex; div.innerHTML = 'Shipment Info'; break; case "fixed": { const val = getFixedValue(rowIndex, col.key); div.innerHTML = createFixedFieldHTML(col, val, rowIndex); break; } case "static": { const val = row[col.key] || ""; div.classList.remove("editable-cell"); if (col.key === "filename_import" && val) { div.innerHTML = `File`; } else { div.innerHTML = `${esc(val)}`; } break; } } return div; } function createInputHTML(col, value, rowIndex) { const cls = col.isManual ? "manual-input" : "auto-input"; const reqCls = col.isRequired ? " required-input" : ""; const req = col.isRequired ? " required" : ""; const v = escAttr(value); if (col.dataType === "SceltaMultipla") { const options = buildDropdownOptionsHTML(col.fieldId, value); return ``; } if (col.dataType === "Data") { return ``; } if (col.dataType === "INT") { return ``; } if ( col.autoValue === "import_time" || (meta.timeLabels || []).includes(col.label) ) { return ``; } return ``; } function createFixedFieldHTML(col, value, rowIndex) { if (col.dataType === "DATE") { const reqCls = col.isRequired ? " required-input" : ""; const req = col.isRequired ? " required" : ""; return ``; } // Client-sourced fields → AJAX Select2 (like idclient) const config = fixedFieldApiConfig[col.key]; if (config && config.source === "clients") { const reqCls = col.isRequired ? " required-input" : ""; const req = col.isRequired ? " required" : ""; const tipo = clientFieldTipoMap[col.key] || ""; let opts = ''; if (value) { const label = clientNameCache[value] || value; opts += ``; } return ``; } // Select — build from cache const isApiField = !!config; const selectClass = isApiField ? "api-fixed-select" : ""; let options = ''; const cacheKey = config?.dependsOn ? col.key + "_" + (data[rowIndex].idclient || "") : col.key; const items = fixedFieldCache[cacheKey] || []; items.forEach((item) => { const sel = String(item.id) === String(value) ? " selected" : ""; options += ``; }); const reqCls = col.isRequired ? " required-input" : ""; const req = col.isRequired ? " required" : ""; return ``; } function buildDropdownOptionsHTML(fieldId, selectedValue) { let html = ''; if (selectedValue) { const label = dropdownNameCache[fieldId + "_" + selectedValue] || selectedValue; html += ``; } return html; } // ── Row rendering ────────────────────────────────────────────────────── function renderActionButtons(rowIndex) { const row = data[rowIndex]; const isExported = row.status === "l"; const div = document.createElement("div"); div.className = "grid-cell button-cell"; div.style.flex = "0 0 auto"; div.style.position = "relative"; let html = ""; html += ``; html += ``; html += ``; html += ``; html += ``; div.innerHTML = html; return div; } function renderRow(rowIndex) { const row = data[rowIndex]; const rowDiv = document.createElement("div"); rowDiv.className = "grid-row"; rowDiv.dataset.id = row.iddatadb; if (row._dirty) rowDiv.classList.add("row-dirty"); // Action buttons rowDiv.appendChild(renderActionButtons(rowIndex)); // All columns let cellIndex = 1; columns.forEach((col) => { rowDiv.appendChild(createCell(col, rowIndex, cellIndex)); cellIndex++; }); // Restore validation errors if present if (row._validationErrors && row._validationErrors.length > 0) { rowDiv.classList.add("validation-row-error"); const messages = []; row._validationErrors.forEach((err) => { messages.push(err.message); if (!err.field) return; let cell = null; if (err.field.startsWith("field_label:")) { const label = err.field.substring("field_label:".length); const headers = document.querySelectorAll(".grid-header"); let targetIndex = null; headers.forEach((h) => { if (h.textContent.trim() === label) targetIndex = h.getAttribute("data-index"); }); if (targetIndex) cell = rowDiv.querySelector( `.grid-cell[data-index="${targetIndex}"]`, ); } else { cell = rowDiv.querySelector( `.grid-cell[data-col="${err.field}"]`, ); } if (cell) { cell.classList.add("validation-error"); cell.querySelectorAll("input, select").forEach((el) => el.classList.add("input-validation-error"), ); let tooltip = cell.querySelector(".validation-tooltip"); if (!tooltip) { tooltip = document.createElement("div"); tooltip.className = "validation-tooltip"; cell.appendChild(tooltip); } tooltip.textContent = err.message; } }); // Show error msg on button cell const btnCell2 = rowDiv.querySelector(".button-cell"); if (btnCell2) { const errorEl = document.createElement("div"); errorEl.className = "batch-error-msg"; errorEl.textContent = "Warning — click for details"; errorEl.addEventListener("click", () => { document.getElementById("exportResponseMessage").innerHTML = messages.join("
"); document.getElementById( "exportResponseModalLabel", ).textContent = "Validation Error (id: " + row.iddatadb + ")"; new bootstrap.Modal( document.getElementById("exportResponseModal"), { keyboard: false }, ).show(); }); btnCell2.appendChild(errorEl); } } // Restore export error indicator if present if (row._exportError) { rowDiv.classList.add("batch-row-error"); const btnCell = rowDiv.querySelector(".button-cell"); if (btnCell) { const errorEl = document.createElement("div"); errorEl.className = "batch-error-msg"; errorEl.textContent = "Warning — click for details"; errorEl.addEventListener("click", () => { document.getElementById("exportResponseMessage").innerHTML = row._exportError.replace(/\n/g, "
"); document.getElementById( "exportResponseModalLabel", ).textContent = "Error (id: " + row.iddatadb + ")"; new bootstrap.Modal( document.getElementById("exportResponseModal"), { keyboard: false }, ).show(); }); btnCell.appendChild(errorEl); } } return rowDiv; } // ── Render visible rows ──────────────────────────────────────────────── function renderVisibleRows() { if (!rowContainer) return; // Refresh totalRows in case data was modified externally totalRows = data.length; if (revealedCount < totalRows) revealedCount = totalRows; // Destroy Select2 on existing rows $(rowContainer).find(".select2-hidden-accessible").select2("destroy"); rowContainer.innerHTML = ""; const end = Math.min(revealedCount, totalRows); for (let i = 0; i < end; i++) { rowContainer.appendChild(renderRow(i)); } // Init flatpickr on visible date pickers initFlatpickr(); updateStatus(); updateDirtyIndicator(); } // Aggiorna in-place SOLO le celle di una colonna in tutte le righe visibili, // senza distruggere/ricostruire l'intera griglia (evita il freeze in propagazione). function updateColumnCells(colKey, colType) { if (!rowContainer) return; const cells = rowContainer.querySelectorAll( `.grid-cell[data-col="${CSS.escape(String(colKey))}"][data-col-type="${colType}"]`, ); cells.forEach((cell) => { const rowIndex = parseInt(cell.dataset.row, 10); if (Number.isNaN(rowIndex) || !data[rowIndex]) return; // Ricava il valore corrente dal modello dati let value = ""; if (colType === "detail" || colType === "main_field") { value = data[rowIndex].details?.[String(colKey)] ?? ""; } else if (colType === "fixed") { value = data[rowIndex].fixedFields?.[colKey] ?? ""; } else if (colType === "cliente_fornitore_id") { value = data[rowIndex].cliente_fornitore_id ?? ""; } const input = cell.querySelector(".cell-input"); if (!input) return; if ($(input).hasClass("select2-hidden-accessible")) { // Assicura che l'opzione selezionata esista, poi imposta il valore if ( value && !input.querySelector( `option[value="${CSS.escape(String(value))}"]`, ) ) { const label = clientNameCache[value] || (input.dataset?.fieldId ? dropdownNameCache[ input.dataset.fieldId + "_" + value ] : null) || value; input.add(new Option(label, value, true, true)); } $(input) .val(value ? String(value) : "") .trigger("change.select2"); } else if (input.tagName === "SELECT") { // Select nativo: aggiungi opzione se manca if ( value && !input.querySelector( `option[value="${CSS.escape(String(value))}"]`, ) ) { const label = (input.dataset?.fieldId ? dropdownNameCache[ input.dataset.fieldId + "_" + value ] : null) || value; input.add(new Option(label, value, true, true)); } input.value = value; } else { input.value = value; } cell.classList.add("cell-changed"); }); } function renderSingleRow(rowIndex) { const existing = rowContainer.querySelector( `.grid-row[data-id="${data[rowIndex].iddatadb}"]`, ); if (!existing) return; // Destroy Select2 before removing $(existing).find(".select2-hidden-accessible").select2("destroy"); const newRow = renderRow(rowIndex); existing.replaceWith(newRow); // Init flatpickr $(newRow) .find(".date-picker") .each(function () { flatpickr(this, { dateFormat: "Y-m-d", allowInput: true }); }); } function revealNextBatch() { if (revealedCount >= totalRows) return; const start = revealedCount; revealedCount = Math.min(revealedCount + PAGE_SIZE, totalRows); for (let i = start; i < revealedCount; i++) { rowContainer.appendChild(renderRow(i)); } initFlatpickr(); updateStatus(); } function initFlatpickr() { $(rowContainer) .find(".date-picker:not(.flatpickr-input)") .each(function () { flatpickr(this, { dateFormat: "Y-m-d", allowInput: true }); }); } function getInputTextWidth(input) { const span = document.createElement("span"); const style = window.getComputedStyle(input); span.style.position = "absolute"; span.style.visibility = "hidden"; span.style.whiteSpace = "pre"; span.style.font = style.font; span.style.fontSize = style.fontSize; span.style.fontFamily = style.fontFamily; span.style.fontWeight = style.fontWeight; span.textContent = input.value || input.placeholder || ""; document.body.appendChild(span); const width = span.offsetWidth + 60; document.body.removeChild(span); return width; } function autoExpandColumnFromInput(input) { if (!input) return; const cell = input.closest(".grid-cell"); if (!cell || !cell.dataset.index) return; const columnIndex = parseInt(cell.dataset.index, 10); if (!columnIndex) return; const wantedWidth = Math.max(120, getInputTextWidth(input)); const currentWidth = cell.offsetWidth || 0; // Only expand, do not shrink automatically if (wantedWidth <= currentWidth) return; const newWidth = Math.min(wantedWidth, 900); const header = document.querySelector( `.grid-header[data-index="${columnIndex}"]`, ); if (header) { header.style.flex = `0 0 ${newWidth}px`; } const topCell = document.querySelector( `.grid-top .grid-cell:nth-child(${columnIndex + 1})`, ); if (topCell) { topCell.style.flex = `0 0 ${newWidth}px`; } const cells = document.querySelectorAll( `.grid-row .grid-cell[data-index="${columnIndex}"]`, ); cells.forEach((c) => { c.style.flex = `0 0 ${newWidth}px`; }); const colPos = columnIndex - 1; if (columns[colPos]) { columns[colPos].width = newWidth; } } function syncVisibleRowsToGridData() { if (!rowContainer) return; rowContainer .querySelectorAll(".grid-cell[data-row]") .forEach((cell) => { const rowIndex = parseInt(cell.dataset.row, 10); const row = data[rowIndex]; if (!row) return; // Salta le righe nascoste (filtrate / non nella selezione ristretta): // non devono essere sincronizzate né marcate dirty. const rowEl = cell.closest(".grid-row"); if (rowEl && rowEl.style.display === "none") return; const colType = cell.dataset.colType; const colKey = cell.dataset.col; const input = cell.querySelector(".cell-input"); if (!input) return; const value = $(input).hasClass("select2-hidden-accessible") ? $(input).val() || "" : input.value || ""; if (colType === "detail" || colType === "main_field") { if (!row.details) row.details = {}; if ( String(row.details[String(colKey)] ?? "") !== String(value) ) { row.details[String(colKey)] = value; if (colType === "main_field") { row.mainFieldValue = value; } row._dirty = true; } } else if (colType === "fixed") { if (!row.fixedFields) row.fixedFields = {}; if ( String(row.fixedFields[colKey] ?? "") !== String(value) ) { row.fixedFields[colKey] = value; row._dirty = true; } } else if (colType === "idclient") { if (String(row.idclient ?? "") !== String(value)) { row.idclient = value; row._dirty = true; } } else if (colType === "cliente_fornitore_id") { if ( String(row.cliente_fornitore_id ?? "") !== String(value) ) { row.cliente_fornitore_id = value; row._dirty = true; } } else if (colType === "tested_component") { if (String(row.tested_component ?? "") !== String(value)) { row.tested_component = value; row._dirty = true; } } }); updateDirtyIndicator(); } // ── Headers & Propagate row ──────────────────────────────────────────── function renderHeaders() { if (!headerContainer) return; headerContainer.innerHTML = ""; // Actions header const actH = document.createElement("div"); actH.className = "grid-header button-header"; actH.style.flex = "0 0 380px"; actH.textContent = "Actions"; headerContainer.appendChild(actH); let idx = 1; columns.forEach((col) => { const h = document.createElement("div"); h.className = "grid-header"; h.dataset.index = idx; h.style.flex = `0 0 ${col.width}px`; h.style.position = "relative"; h.innerHTML = esc(col.label) + '
'; headerContainer.appendChild(h); idx++; }); initColumnResizers(); } function renderTopRow() { if (!topContainer) return; topContainer.innerHTML = ""; // Empty cell for actions column const empty = document.createElement("div"); empty.className = "grid-cell save-all-cell"; topContainer.appendChild(empty); columns.forEach((col, colIdx) => { const cell = document.createElement("div"); cell.className = "grid-cell grid-top-cell"; // Field color classification for top propagation row // Schema/customfield fields are green. // Fixed and standard fields stay white. if (col.type === "detail" || col.type === "main_field") { cell.classList.add("schema-field"); } else if (col.type === "fixed") { cell.classList.add("fixed-field"); } else { cell.classList.add("standard-field"); } // Required field classification also for the top propagation row. if ( col.isRequired === true || col.isRequired === 1 || col.isRequired === "1" ) { cell.classList.add("required-field"); } cell.style.flex = `0 0 ${col.width}px`; if ( col.editable === false || col.type === "static" || col.type === "tracking" ) { // Empty top cell } else if (col.type === "idclient") { cell.innerHTML = `` + ``; } else if (col.type === "cliente_fornitore_id") { cell.innerHTML = `` + ``; } else if (col.type === "fixed" && col.dataType === "DATE") { cell.innerHTML = `` + ``; } else if (col.type === "fixed") { const isApiField = !!fixedFieldApiConfig[col.key]; if (isApiField) { const tipo = clientFieldTipoMap[col.key] || ""; const isClientSourced = fixedFieldApiConfig[col.key]?.source === "clients"; const clientCls = isClientSourced ? " client-select searchable-client" : ""; cell.innerHTML = `` + ``; } else { cell.innerHTML = `` + ``; } } else if (col.type === "detail" || col.type === "main_field") { if (col.dataType === "SceltaMultipla") { cell.innerHTML = `` + ``; } else if (col.dataType === "Data") { cell.innerHTML = `` + ``; } else { cell.innerHTML = `` + ``; } } topContainer.appendChild(cell); }); // Populate top row selects populateTopRowSelects(); } async function populateTopRowSelects() { // Client selects in top row — AJAX mode const clientSel = document.getElementById("clientSelect"); if (clientSel) { clientSel.innerHTML = buildClientOptionsHTML(meta.defaultIdclient); initClientSelect2(clientSel); } const fornitSel = document.getElementById("clienteFornitoreSelect"); if (fornitSel) { fornitSel.innerHTML = buildClientOptionsHTML(""); initClientSelect2(fornitSel); } // Fixed field selects in top row topContainer.querySelectorAll(".api-fixed-select").forEach((sel) => { const fieldKey = sel.dataset.fixedKey; const config = fixedFieldApiConfig[fieldKey]; // Client-sourced → init as AJAX Select2 if (config && config.source === "clients") { initClientSelect2(sel); return; } if (config && config.dependsOn) { // Dependent fixed fields, for example ClienteResponsabile: // use the first row client, not all cached clients. const firstClientId = data[0]?.idclient || meta.defaultIdclient || ""; sel.innerHTML = ''; if (firstClientId) { const items = fixedFieldCache[fieldKey + "_" + firstClientId] || []; items.forEach((item) => { sel.add(new Option(item.text, item.id)); }); } $(sel).select2({ placeholder: "Seleziona...", allowClear: true, width: "100%", sorter: sortSelect2ResultsByStart, }); } else { const items = fixedFieldCache[fieldKey] || []; sel.innerHTML = ''; items.forEach((item) => sel.add(new Option(item.text, item.id)), ); $(sel).select2({ placeholder: "Seleziona...", allowClear: true, width: "100%", ...rankedLocalSelect2(), }); } }); // Custom field dropdowns in top row — AJAX Select2 topContainer .querySelectorAll(".dropdown-select[data-field-id]") .forEach((sel) => { const fieldId = sel.dataset.fieldId; if (fieldId) $(sel).select2(sceltaSelect2Config(fieldId)); }); // Flatpickr in top row topContainer.querySelectorAll(".date-picker").forEach((el) => { flatpickr(el, { dateFormat: "Y-m-d", allowInput: true }); }); } // ── Event delegation ─────────────────────────────────────────────────── function attachEvents() { if (!rowContainer) return; // ── Auto-copy: Sample Arrival date/time → Unlock date/time ────────── // Coppie basate sui fieldId dei campi (source → target) const AUTO_COPY_FIELD_PAIRS = [ { from: 259, to: 260 }, // Sample Arrival date → Unlock Date { from: 348, to: 349 }, // Sample Arrival time → Unlock time ]; // Risolve fieldId → col.key (mapping id) usando le definizioni colonne function colKeyByFieldId(fieldId) { const col = columns.find( (c) => (c.type === "detail" || c.type === "main_field") && String(c.fieldId) === String(fieldId), ); return col ? String(col.key) : null; } // Precalcola le coppie come col.key (sourceKey → targetKey) var AUTO_COPY_KEY_PAIRS = AUTO_COPY_FIELD_PAIRS.map((p) => ({ fromKey: colKeyByFieldId(p.from), toKey: colKeyByFieldId(p.to), })).filter((p) => p.fromKey && p.toKey); function applyAutoCopy(rowIndex, changedColKey, value) { const pair = AUTO_COPY_KEY_PAIRS.find( (p) => p.fromKey === String(changedColKey), ); if (!pair) return; const row = data[rowIndex]; if (!row) return; if (!row.details) row.details = {}; row.details[pair.toKey] = value; row._dirty = true; // Aggiorna la cella target visibile, se presente const targetCell = rowContainer.querySelector( `.grid-cell[data-row="${rowIndex}"][data-col="${pair.toKey}"]`, ); if (targetCell) { const input = targetCell.querySelector(".cell-input"); if (input) { if ($(input).hasClass("select2-hidden-accessible")) { $(input).val(value).trigger("change.select2"); } else { input.value = value; } } targetCell.classList.add("cell-changed"); } } // Cell value changes → write to gridData rowContainer.addEventListener("change", function (e) { const cell = e.target.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const colType = cell.dataset.colType; const colKey = cell.dataset.col; const value = e.target.value; if (colType === "detail" || colType === "main_field") { if (colType === "main_field") { data[rowIndex].mainFieldValue = value; } setDetailValue(rowIndex, colKey, value); applyAutoCopy(rowIndex, colKey, value); } else if (colType === "fixed") { setFixedValue(rowIndex, colKey, value); } else if (colType === "idclient") { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; refreshDependentFixedFieldsForRow(rowIndex); } else if (colType === "cliente_fornitore_id") { data[rowIndex].cliente_fornitore_id = value; data[rowIndex]._dirty = true; console.log( "[gridRenderer] cliente_fornitore_id changed:", rowIndex, value, ); } else if (colType === "tested_component") { data[rowIndex].tested_component = value; data[rowIndex]._dirty = true; } // Visual feedback cell.classList.add("cell-changed"); updateDirtyIndicator(); }); rowContainer.addEventListener("input", function (e) { const cell = e.target.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; if (e.target.classList.contains("cell-input")) { autoExpandColumnFromInput(e.target); } const rowIndex = parseInt(cell.dataset.row, 10); const colType = cell.dataset.colType; if (colType === "tested_component") { data[rowIndex].tested_component = e.target.value; data[rowIndex]._dirty = true; cell.classList.add("cell-changed"); updateDirtyIndicator(); } }); rowContainer.addEventListener("focusin", function (e) { if (!e.target.classList.contains("cell-input")) return; autoExpandColumnFromInput(e.target); setTimeout(() => { autoExpandColumnFromInput(e.target); }, 50); }); // Persist tested_component before clicking + document.addEventListener("mousedown", function (e) { const btn = e.target.closest(".add-part-btn"); if (!btn) return; const rowIndex = parseInt(btn.dataset.row, 10); if (isNaN(rowIndex) || !data[rowIndex]) return; const rowEl = btn.closest(".grid-row"); if (!rowEl) return; const testedInput = rowEl.querySelector(".tested-component-input"); if (!testedInput) return; data[rowIndex].tested_component = testedInput.value || ""; data[rowIndex]._dirty = true; const cell = testedInput.closest(".grid-cell"); if (cell) cell.classList.add("cell-changed"); updateDirtyIndicator(); }); // Propagate buttons document.addEventListener("click", function (e) { const btn = e.target.closest(".propagate-btn"); if (!btn) return; // Before propagating and re-rendering, persist current visible row values into gridData. syncVisibleRowsToGridData(); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const column = btn.dataset.column || ""; const colIndex = Number.isNaN(parseInt(btn.dataset.colIndex, 10)) ? null : parseInt(btn.dataset.colIndex, 10); if (!column && colIndex === null) return; // IMPORTANT: // Read ONLY the input/select inside the same top propagation cell. // Do not scan other top fields. const cell = btn.closest(".grid-top-cell"); if (!cell) return; const input = cell.querySelector(".custom-field"); if (!input) return; const value = $(input).hasClass("select2-hidden-accessible") ? $(input).val() || "" : input.value || ""; // Do not propagate empty dropdown values. // This prevents wiping rows when a top Select2 is empty/not fully initialized. if (input.tagName === "SELECT" && value === "") { console.warn( "[gridRenderer] Empty select propagation blocked:", column, ); return; } // Cache selected label so re-render can show text instead of only ID. if (value && $(input).hasClass("select2-hidden-accessible")) { const label = $(input).find("option:selected").text(); if (label && label !== value) { if ( column === "idclient" || column === "cliente_fornitore_id" || input.classList.contains("searchable-client") ) { clientNameCache[value] = label; } const fieldId = input.dataset?.fieldId; if (fieldId) { dropdownNameCache[fieldId + "_" + value] = label; } } } const col = colIndex !== null ? columns[colIndex] : null; console.log("[gridRenderer] Propagating ONLY:", { column: column, colIndex: colIndex, value: value, label: input.tagName === "SELECT" ? $(input).find("option:selected").text() : value, }); if (column === "idclient") { data.forEach((row) => { if ( window.isTargetGridId && !window.isTargetGridId(row.iddatadb) ) return; const oldClientId = row.idclient || ""; row.idclient = value; // Clear ClienteResponsabile only if client really changed. if ( String(oldClientId) !== String(value) && row.fixedFields && Object.prototype.hasOwnProperty.call( row.fixedFields, "ClienteResponsabile", ) ) { row.fixedFields["ClienteResponsabile"] = ""; } row._dirty = true; }); loadFixedFieldOptions("ClienteResponsabile", value).then(() => { refreshTopDependentFixedSelect( "ClienteResponsabile", value, ); renderVisibleRows(); updateDirtyIndicator(); }); return; } if (column === "cliente_fornitore_id") { data.forEach((row) => { if ( window.isTargetGridId && !window.isTargetGridId(row.iddatadb) ) return; row.cliente_fornitore_id = value; row._dirty = true; }); updateColumnCells( "cliente_fornitore_id", "cliente_fornitore_id", ); updateDirtyIndicator(); return; } if (column && column.startsWith("fixed_")) { const fixedKey = column.replace("fixed_", ""); data.forEach((row) => { if ( window.isTargetGridId && !window.isTargetGridId(row.iddatadb) ) return; if (!row.fixedFields) row.fixedFields = {}; row.fixedFields[fixedKey] = value; row._dirty = true; }); updateColumnCells(fixedKey, "fixed"); updateDirtyIndicator(); return; } if (col && (col.type === "detail" || col.type === "main_field")) { const autoPair = AUTO_COPY_KEY_PAIRS.find( (p) => p.fromKey === String(col.key), ); data.forEach((row) => { if ( window.isTargetGridId && !window.isTargetGridId(row.iddatadb) ) return; if (!row.details) row.details = {}; row.details[col.key] = value; if (autoPair) { row.details[autoPair.toKey] = value; } if (col.type === "main_field") { row.mainFieldValue = value; } row._dirty = true; }); updateColumnCells(col.key, col.type); // Se esiste una colonna auto-copiata (es. Arrival→Unlock), aggiorna anche quella if (autoPair) { const pairCol = columns.find( (c) => String(c.key) === String(autoPair.toKey), ); if (pairCol) updateColumnCells(pairCol.key, pairCol.type); } updateDirtyIndicator(); return; } }); // Select2 change events (don't bubble via native addEventListener) $(rowContainer).on("change", ".searchable-client", function () { const cell = this.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const colType = cell.dataset.colType; const value = $(this).val() || ""; // Cache selected label if (value) { const label = $(this).find("option:selected").text(); if (label && label !== value) clientNameCache[value] = label; } if (colType === "idclient") { data[rowIndex].idclient = value; data[rowIndex]._dirty = true; refreshDependentFixedFieldsForRow(rowIndex); } else if (colType === "cliente_fornitore_id") { data[rowIndex].cliente_fornitore_id = value; data[rowIndex]._dirty = true; } cell.classList.add("cell-changed"); updateDirtyIndicator(); }); // Handle SceltaMultipla changes and persist them into gridData. // Without this, a later renderVisibleRows() can rebuild the row with the old empty value. $(rowContainer).on("change", ".searchable-dropdown", function () { const cell = this.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row, 10); const colType = cell.dataset.colType; const colKey = cell.dataset.col; const val = $(this).val() || ""; const fieldId = this.dataset.fieldId; // Cache label so re-render shows the text instead of only the ID. if (val && fieldId) { const label = $(this).find("option:selected").text(); if (label && label !== val) { dropdownNameCache[fieldId + "_" + val] = label; } } // Persist value into gridData. if (colType === "detail" || colType === "main_field") { if (!data[rowIndex].details) data[rowIndex].details = {}; data[rowIndex].details[String(colKey)] = val; if (colType === "main_field") { data[rowIndex].mainFieldValue = val; } data[rowIndex]._dirty = true; cell.classList.add("cell-changed"); updateDirtyIndicator(); } }); // Select2 change on fixed field selects $(rowContainer).on("change", ".api-fixed-select", function () { const cell = this.closest(".grid-cell"); if (!cell || !cell.dataset.row) return; const rowIndex = parseInt(cell.dataset.row); const key = this.dataset.fixedKey || cell.dataset.col; const value = $(this).val() || ""; if (key) { data[rowIndex].fixedFields[key] = value; data[rowIndex]._dirty = true; cell.classList.add("cell-changed"); updateDirtyIndicator(); } }); // Scroll function onScroll() { if (revealedCount >= totalRows) return; const docEl = document.documentElement; const scrollBottom = Math.max( docEl.scrollTop + docEl.clientHeight, document.body.scrollTop + window.innerHeight, ); if (scrollBottom >= docEl.scrollHeight - 300) { revealNextBatch(); } } window.addEventListener("scroll", onScroll, { passive: true }); document .querySelector(".page-wrapper") ?.addEventListener("scroll", onScroll, { passive: true }); } // ── Save ─────────────────────────────────────────────────────────────── window.buildSavePayload = function (rowIndex) { const row = data[rowIndex]; const formData = new FormData(); formData.append("iddatadb", row.iddatadb); // Details for (const [mappingId, value] of Object.entries(row.details)) { formData.append(`details${mappingId}field_value`, value); } // Client if (row.idclient) formData.append("idclient", row.idclient); formData.append("cliente_fornitore_id", row.cliente_fornitore_id || ""); formData.append("tested_component", row.tested_component || ""); // Fixed fields → real column names const aliasMap = meta.fixedAliasMap || {}; for (const [logicalKey, value] of Object.entries(row.fixedFields)) { const realKey = aliasMap[logicalKey] || logicalKey; formData.append(realKey, value); } return formData; }; // ── Dirty indicator ──────────────────────────────────────────────────── function updateDirtyIndicator() { const dirtyCount = data.filter((r) => r._dirty).length; const indicator = document.getElementById("unsavedChanges"); const changedEl = document.getElementById("changedRows"); if (indicator) { indicator.style.display = dirtyCount > 0 ? "" : "none"; } if (changedEl) { changedEl.textContent = dirtyCount > 0 ? `(${dirtyCount} rows)` : ""; } } function updateStatus() { if (!statusEl) return; const shown = Math.min(revealedCount, totalRows); if (shown >= totalRows) { statusEl.textContent = `All ${totalRows} rows loaded`; setTimeout(() => (statusEl.style.display = "none"), 2000); } else { statusEl.textContent = `Showing ${shown} of ${totalRows} rows — scroll down for more`; } } // ── Lazy Select2 for row selects ─────────────────────────────────────── function initLazySelect2() { $(document).on("mouseenter", ".grid-row .grid-cell", function () { $(this) .find(".searchable-client:not(.select2-hidden-accessible)") .each(function () { initClientSelect2(this); }); $(this) .find(".searchable-dropdown:not(.select2-hidden-accessible)") .each(function () { const fieldId = this.dataset.fieldId; if (fieldId) $(this).select2(sceltaSelect2Config(fieldId)); }); $(this) .find(".searchable-fixed:not(.select2-hidden-accessible)") .each(function () { $(this).select2({ placeholder: "Seleziona...", allowClear: true, width: "100%", ...rankedLocalSelect2(), }); }); }); } // ── Column resize ────────────────────────────────────────────────────── function initColumnResizers() { const resizers = document.querySelectorAll(".resizer"); let currentResizer = null; let startX = 0; let startWidth = 0; let columnIndex = null; function resize(e) { if (!currentResizer || !columnIndex) return; const deltaX = e.pageX - startX; const newWidth = Math.max(80, startWidth + deltaX); const header = document.querySelector( `.grid-header[data-index="${columnIndex}"]`, ); if (header) { header.style.flex = `0 0 ${newWidth}px`; } const topCell = document.querySelector( `.grid-top .grid-cell:nth-child(${parseInt(columnIndex, 10) + 1})`, ); if (topCell) { topCell.style.flex = `0 0 ${newWidth}px`; } const cells = document.querySelectorAll( `.grid-row .grid-cell[data-index="${columnIndex}"]`, ); cells.forEach((cell) => { cell.style.flex = `0 0 ${newWidth}px`; }); // aggiorna anche la width nel meta così i rerender la mantengono const colPos = parseInt(columnIndex, 10) - 1; if (columns[colPos]) { columns[colPos].width = newWidth; } } function stopResize() { if (currentResizer) { document.removeEventListener("mousemove", resize); document.removeEventListener("mouseup", stopResize); currentResizer = null; columnIndex = null; } } resizers.forEach((resizer) => { resizer.addEventListener("mousedown", function (e) { e.preventDefault(); e.stopPropagation(); currentResizer = this; const header = this.closest(".grid-header"); if (!header) return; columnIndex = header.getAttribute("data-index"); startX = e.pageX; startWidth = header.offsetWidth; document.addEventListener("mousemove", resize); document.addEventListener("mouseup", stopResize); }); }); } // ── Init ─────────────────────────────────────────────────────────────── async function init() { rowContainer = document.getElementById("gridRowContainer"); headerContainer = document.getElementById("gridHeaderContainer"); topContainer = document.getElementById("gridTopContainer"); if (!rowContainer) { console.error("gridRenderer: #gridRowContainer not found"); return; } // Status bar statusEl = document.createElement("div"); statusEl.style.cssText = "text-align:center; padding:8px; color:#666; font-size:12px;"; if (totalRows > PAGE_SIZE) { statusEl.textContent = `Loading data...`; rowContainer.parentElement.appendChild(statusEl); } // Preload all dropdown/field data await preloadAllData(); // Render renderHeaders(); renderTopRow(); renderVisibleRows(); initColumnResizers(); // Events attachEvents(); initLazySelect2(); } // Start when DOM ready if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } // ── Public API ───────────────────────────────────────────────────────── window.gridRenderer = { renderVisibleRows, renderSingleRow, revealNextBatch, buildSavePayload: window.buildSavePayload, getData: () => data, getMeta: () => meta, getClientData: () => clientData, getDirtyRows: () => data.filter((r) => r._dirty).map((r, i) => i), clearDirty: (rowIndex) => { data[rowIndex]._dirty = false; updateDirtyIndicator(); }, }; })(); // =================== // NAVIGAZIONE GRIGLIA CON FRECCE TASTIERA // =================== (function () { // Naviga solo tra input di testo/numero/time e date-picker. // I Select2 (client, fornitore, SceltaMultipla) sono esclusi: // lì le frecce servono al dropdown. function isNavigable(el) { if (!el || !el.classList) return false; if (!el.classList.contains("cell-input")) return false; // Escludi i select (nativi o Select2) if (el.tagName === "SELECT") return false; if (el.classList.contains("select2-hidden-accessible")) return false; return true; } // Tutte le celle navigabili della riga, ordinate per data-index function navCellsInRow($row) { return $row.find(".grid-cell[data-index]").filter(function () { const input = this.querySelector(".cell-input"); return isNavigable(input); }); } function focusCellInput($cell) { if (!$cell || !$cell.length) return; const input = $cell.get(0).querySelector(".cell-input"); if (!input) return; input.focus(); if (input.type !== "date" && typeof input.select === "function") { try { input.select(); } catch (e) {} } } function atStart(el) { if (el.type === "date" || el.type === "number" || el.type === "time") return true; return el.selectionStart === 0 && el.selectionEnd === 0; } function atEnd(el) { if (el.type === "date" || el.type === "number" || el.type === "time") return true; const len = (el.value || "").length; return el.selectionStart === len && el.selectionEnd === len; } $(document).on("keydown", "#gridRowContainer .cell-input", function (e) { const el = this; if (!isNavigable(el)) return; // Ignora se un dropdown Select2 è aperto if ($(".select2-container--open").length) return; const key = e.key; if ( key !== "ArrowUp" && key !== "ArrowDown" && key !== "ArrowLeft" && key !== "ArrowRight" ) { return; } const $cell = $(el).closest(".grid-cell"); const $row = $cell.closest(".grid-row"); if (!$cell.length || !$row.length) return; const colIndex = $cell.attr("data-index"); // --- SU / GIÙ: stessa colonna (data-index), riga adiacente --- if (key === "ArrowUp" || key === "ArrowDown") { const $targetRow = key === "ArrowUp" ? $row.prev(".grid-row") : $row.next(".grid-row"); if (!$targetRow.length) return; e.preventDefault(); // Cella stessa colonna nella riga di destinazione let $target = $targetRow.find( `.grid-cell[data-index="${colIndex}"]`, ); // Se quella colonna non è navigabile (es. è un select), // cerca la più vicina navigabile nella riga if ( !$target.length || !isNavigable($target.get(0).querySelector(".cell-input")) ) { const $cells = navCellsInRow($targetRow); if (!$cells.length) return; $target = $cells.first(); } focusCellInput($target); return; } // --- SINISTRA / DESTRA: cella navigabile precedente/successiva nella riga --- const $cells = navCellsInRow($row); const curPos = $cells.index($cell); if (curPos === -1) return; if (key === "ArrowLeft") { if (!atStart(el)) return; if (curPos <= 0) return; e.preventDefault(); focusCellInput($cells.eq(curPos - 1)); return; } if (key === "ArrowRight") { if (!atEnd(el)) return; if (curPos >= $cells.length - 1) return; e.preventDefault(); focusCellInput($cells.eq(curPos + 1)); return; } }); })();