fixed sticky bars with toggle
This commit is contained in:
@@ -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/
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user