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/error_log.txt
# PDF/QR generati dai template (userarea)
/public/userarea/pdftemplates/
+84 -3
View File
@@ -895,6 +895,76 @@
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}"]`,
@@ -1546,7 +1616,10 @@
row._dirty = true;
});
renderVisibleRows();
updateColumnCells(
"cliente_fornitore_id",
"cliente_fornitore_id",
);
updateDirtyIndicator();
return;
}
@@ -1565,7 +1638,7 @@
row._dirty = true;
});
renderVisibleRows();
updateColumnCells(fixedKey, "fixed");
updateDirtyIndicator();
return;
}
@@ -1595,7 +1668,15 @@
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();
return;
}
+65 -15
View File
@@ -1417,6 +1417,8 @@ $gridMeta = [
/* posizione verticale barra azioni: abbassa/alza questo */
--actions-height: 58px;
/* altezza reale del card-header azioni */
--scrollbar-height: 18px;
/* altezza della .top-scrollbar */
}
.card-header.sticky-actions {
@@ -1431,6 +1433,22 @@ $gridMeta = [
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 */
.sticky-toggle-btn {
border: 1px solid #ced4da;
@@ -1460,12 +1478,17 @@ $gridMeta = [
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 {
position: static !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) {
position: sticky !important;
left: 360px;
@@ -1594,8 +1617,11 @@ $gridMeta = [
<div class="top-scrollbar-inner" id="topScrollbarInner"></div>
</div>
<div class="grid-container" id="gridContainer">
<div class="grid-top-scroller" id="gridTopScroller">
<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 id="gridRowContainer"></div>
</div>
@@ -1666,21 +1692,43 @@ $gridMeta = [
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() {
if (syncingFromGrid) return;
syncingFromTop = true;
gridContainer.scrollLeft = topScrollbar.scrollLeft;
syncingFromTop = false;
syncScroll(topScrollbar);
});
gridContainer.addEventListener('scroll', function() {
if (syncingFromTop) return;
syncingFromGrid = true;
topScrollbar.scrollLeft = gridContainer.scrollLeft;
syncingFromGrid = false;
syncScroll(gridContainer);
});
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
setTimeout(updateTopScrollbarWidth, 100);
@@ -1689,13 +1737,13 @@ $gridMeta = [
setTimeout(updateTopScrollbarWidth, 1500);
// Recalculate automatically when rows/header/top controls are rendered or changed
const observer = new MutationObserver(updateTopScrollbarWidth);
const observer = new MutationObserver(scheduleUpdate);
if (gridContainer) {
observer.observe(gridContainer, {
childList: true,
subtree: true,
attributes: true
attributes: false
});
}
});
@@ -1872,7 +1920,7 @@ $gridMeta = [
<script>
(function() {
// ── 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 LOGIN_URL = '../login'; // adatta al tuo path reale
@@ -1939,9 +1987,11 @@ $gridMeta = [
const cardHeader = document.querySelector('.card-header.sticky-actions');
if (!btn || !cardHeader) return;
const gridTopScroller = document.getElementById('gridTopScroller');
btn.addEventListener('click', function() {
const pinned = btn.classList.toggle('active');
cardHeader.classList.toggle('actions-unpinned', !pinned);
if (gridTopScroller) gridTopScroller.classList.toggle('actions-unpinned', !pinned);
btn.title = pinned ?
'Sblocca barra azioni' :
'Blocca barra azioni';