fixed sticky bars with toggle

This commit is contained in:
2026-09-08 09:18:17 +02:00
parent 9453e7a01a
commit def7377447
3 changed files with 152 additions and 18 deletions
+3
View File
@@ -62,3 +62,6 @@ public/userarea/schemi_base_response.json
public/userarea/cache/ public/userarea/cache/
public/userarea/error_log.txt public/userarea/error_log.txt
# PDF/QR generati dai template (userarea)
/public/userarea/pdftemplates/
+84 -3
View File
@@ -895,6 +895,76 @@
updateDirtyIndicator(); 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) { function renderSingleRow(rowIndex) {
const existing = rowContainer.querySelector( const existing = rowContainer.querySelector(
`.grid-row[data-id="${data[rowIndex].iddatadb}"]`, `.grid-row[data-id="${data[rowIndex].iddatadb}"]`,
@@ -1546,7 +1616,10 @@
row._dirty = true; row._dirty = true;
}); });
renderVisibleRows(); updateColumnCells(
"cliente_fornitore_id",
"cliente_fornitore_id",
);
updateDirtyIndicator(); updateDirtyIndicator();
return; return;
} }
@@ -1565,7 +1638,7 @@
row._dirty = true; row._dirty = true;
}); });
renderVisibleRows(); updateColumnCells(fixedKey, "fixed");
updateDirtyIndicator(); updateDirtyIndicator();
return; return;
} }
@@ -1595,7 +1668,15 @@
row._dirty = true; row._dirty = true;
}); });
renderVisibleRows(); 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(); updateDirtyIndicator();
return; return;
} }
+65 -15
View File
@@ -1417,6 +1417,8 @@ $gridMeta = [
/* posizione verticale barra azioni: abbassa/alza questo */ /* posizione verticale barra azioni: abbassa/alza questo */
--actions-height: 58px; --actions-height: 58px;
/* altezza reale del card-header azioni */ /* altezza reale del card-header azioni */
--scrollbar-height: 18px;
/* altezza della .top-scrollbar */
} }
.card-header.sticky-actions { .card-header.sticky-actions {
@@ -1431,6 +1433,22 @@ $gridMeta = [
top: calc(var(--actions-top) + var(--actions-height)) !important; top: calc(var(--actions-top) + var(--actions-height)) !important;
} }
/* Wrapper del grid-top: sticky verticale + scroll orizzontale proprio */
.grid-top-scroller {
position: sticky;
top: calc(var(--actions-top) + var(--actions-height) + var(--scrollbar-height));
z-index: 25;
background: #fff;
overflow-x: auto;
overflow-y: hidden;
/* nasconde la barra di scroll di questo wrapper: usiamo la top-scrollbar e la griglia */
scrollbar-width: none;
}
.grid-top-scroller::-webkit-scrollbar {
display: none;
}
/* Tastino toggle sticky */ /* Tastino toggle sticky */
.sticky-toggle-btn { .sticky-toggle-btn {
border: 1px solid #ced4da; border: 1px solid #ced4da;
@@ -1460,12 +1478,17 @@ $gridMeta = [
transform: rotate(45deg); transform: rotate(45deg);
} }
/* Quando la classe è tolta dal card-header, non è più sticky */ /* Quando sbloccato, card-header e grid-top non sono più sticky verticalmente */
.card-header.actions-unpinned { .card-header.actions-unpinned {
position: static !important; position: static !important;
top: auto !important; top: auto !important;
} }
.grid-top-scroller.actions-unpinned {
position: static !important;
top: auto !important;
}
<?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-top .grid-cell:nth-child(3) { <?php if (isset($mainFieldMappings) && count($mainFieldMappings) >= 2): ?>.grid-top .grid-cell:nth-child(3) {
position: sticky !important; position: sticky !important;
left: 360px; left: 360px;
@@ -1594,8 +1617,11 @@ $gridMeta = [
<div class="top-scrollbar-inner" id="topScrollbarInner"></div> <div class="top-scrollbar-inner" id="topScrollbarInner"></div>
</div> </div>
<div class="grid-container" id="gridContainer"> <div class="grid-top-scroller" id="gridTopScroller">
<div class="grid-top" id="gridTopContainer"></div> <div class="grid-top" id="gridTopContainer"></div>
</div>
<div class="grid-container" id="gridContainer">
<div class="grid-row" id="gridHeaderContainer" style="font-weight:600;"></div> <div class="grid-row" id="gridHeaderContainer" style="font-weight:600;"></div>
<div id="gridRowContainer"></div> <div id="gridRowContainer"></div>
</div> </div>
@@ -1666,21 +1692,43 @@ $gridMeta = [
topScrollbar.scrollLeft = gridContainer.scrollLeft; topScrollbar.scrollLeft = gridContainer.scrollLeft;
} }
const gridTopScroller = document.getElementById('gridTopScroller');
let syncing = false;
function syncScroll(source) {
if (syncing) return;
syncing = true;
const x = source.scrollLeft;
if (source !== topScrollbar) topScrollbar.scrollLeft = x;
if (source !== gridContainer) gridContainer.scrollLeft = x;
if (gridTopScroller && source !== gridTopScroller) gridTopScroller.scrollLeft = x;
syncing = false;
}
topScrollbar.addEventListener('scroll', function() { topScrollbar.addEventListener('scroll', function() {
if (syncingFromGrid) return; syncScroll(topScrollbar);
syncingFromTop = true;
gridContainer.scrollLeft = topScrollbar.scrollLeft;
syncingFromTop = false;
}); });
gridContainer.addEventListener('scroll', function() { gridContainer.addEventListener('scroll', function() {
if (syncingFromTop) return; syncScroll(gridContainer);
syncingFromGrid = true;
topScrollbar.scrollLeft = gridContainer.scrollLeft;
syncingFromGrid = false;
}); });
if (gridTopScroller) {
gridTopScroller.addEventListener('scroll', function() {
syncScroll(gridTopScroller);
});
}
window.addEventListener('resize', updateTopScrollbarWidth); let rafPending = false;
function scheduleUpdate() {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(function() {
rafPending = false;
updateTopScrollbarWidth();
});
}
window.addEventListener('resize', scheduleUpdate);
// Recalculate after JS grid rendering // Recalculate after JS grid rendering
setTimeout(updateTopScrollbarWidth, 100); setTimeout(updateTopScrollbarWidth, 100);
@@ -1689,13 +1737,13 @@ $gridMeta = [
setTimeout(updateTopScrollbarWidth, 1500); setTimeout(updateTopScrollbarWidth, 1500);
// Recalculate automatically when rows/header/top controls are rendered or changed // Recalculate automatically when rows/header/top controls are rendered or changed
const observer = new MutationObserver(updateTopScrollbarWidth); const observer = new MutationObserver(scheduleUpdate);
if (gridContainer) { if (gridContainer) {
observer.observe(gridContainer, { observer.observe(gridContainer, {
childList: true, childList: true,
subtree: true, subtree: true,
attributes: true attributes: false
}); });
} }
}); });
@@ -1872,7 +1920,7 @@ $gridMeta = [
<script> <script>
(function() { (function() {
// ── TEST: tempi compressi. In produzione: SESSION_MINUTES=120, WARN_SECONDS=120 // ── TEST: tempi compressi. In produzione: SESSION_MINUTES=120, WARN_SECONDS=120
const SESSION_MINUTES = 2; // durata simulata const SESSION_MINUTES = 110; // durata simulata
const WARN_SECONDS = 30; // preavviso prima della scadenza const WARN_SECONDS = 30; // preavviso prima della scadenza
const LOGIN_URL = '../login'; // adatta al tuo path reale const LOGIN_URL = '../login'; // adatta al tuo path reale
@@ -1939,9 +1987,11 @@ $gridMeta = [
const cardHeader = document.querySelector('.card-header.sticky-actions'); const cardHeader = document.querySelector('.card-header.sticky-actions');
if (!btn || !cardHeader) return; if (!btn || !cardHeader) return;
const gridTopScroller = document.getElementById('gridTopScroller');
btn.addEventListener('click', function() { btn.addEventListener('click', function() {
const pinned = btn.classList.toggle('active'); const pinned = btn.classList.toggle('active');
cardHeader.classList.toggle('actions-unpinned', !pinned); cardHeader.classList.toggle('actions-unpinned', !pinned);
if (gridTopScroller) gridTopScroller.classList.toggle('actions-unpinned', !pinned);
btn.title = pinned ? btn.title = pinned ?
'Sblocca barra azioni' : 'Sblocca barra azioni' :
'Blocca barra azioni'; 'Blocca barra azioni';