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/cache/
|
||||||
public/userarea/error_log.txt
|
public/userarea/error_log.txt
|
||||||
|
|
||||||
|
# PDF/QR generati dai template (userarea)
|
||||||
|
/public/userarea/pdftemplates/
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user