fix(tableaux): thumb custom toujours visible, drag souris et tactile #82
Remplace le scrollbar natif (invisible en mode overlay sous Linux) par un thumb div orange toujours visible. Track gris (#e0e0e0) 16px. Drag souris + toucher + clic sur le track supportés. Supprime la dépendance aux pseudo-éléments webkit et scrollbar-color. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
-20
@@ -389,34 +389,32 @@ table.cell-table th {
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 11; /* au-dessus des en-têtes sticky */
|
z-index: 11; /* au-dessus des en-têtes sticky */
|
||||||
overflow-x: scroll; /* scroll (pas auto) force le thumb toujours visible */
|
|
||||||
overflow-y: hidden;
|
|
||||||
height: 16px;
|
height: 16px;
|
||||||
scrollbar-color: orange #e0e0e0;
|
|
||||||
scrollbar-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marches_table .dt-hscroll::-webkit-scrollbar {
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marches_table .dt-hscroll::-webkit-scrollbar-thumb {
|
|
||||||
background-color: orange;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marches_table .dt-hscroll::-webkit-scrollbar-track {
|
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
}
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
.marches_table .dt-hscroll-inner {
|
|
||||||
height: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.marches_table .dt-hscroll.is-hidden {
|
.marches_table .dt-hscroll.is-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Thumb custom — toujours visible, draggable */
|
||||||
|
.marches_table .dt-hscroll-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
height: calc(100% - 4px);
|
||||||
|
min-width: 40px;
|
||||||
|
background-color: orange;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marches_table .dt-hscroll-thumb:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
/* Column Visibility Menu */
|
/* Column Visibility Menu */
|
||||||
.column-actions {
|
.column-actions {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|||||||
+91
-29
@@ -14,39 +14,101 @@
|
|||||||
|
|
||||||
const bar = document.createElement("div");
|
const bar = document.createElement("div");
|
||||||
bar.className = "dt-hscroll is-hidden";
|
bar.className = "dt-hscroll is-hidden";
|
||||||
const inner = document.createElement("div");
|
const thumb = document.createElement("div");
|
||||||
inner.className = "dt-hscroll-inner";
|
thumb.className = "dt-hscroll-thumb";
|
||||||
bar.appendChild(inner);
|
bar.appendChild(thumb);
|
||||||
wrapper.insertBefore(bar, wrapper.firstChild);
|
wrapper.insertBefore(bar, wrapper.firstChild);
|
||||||
|
|
||||||
// La barre synchronise le scroll horizontal de la page (overflow: visible sur les
|
// --- Métriques communes ---
|
||||||
// conteneurs Dash laisse le scroll se faire au niveau de la page, ce qui permet
|
const metrics = () => {
|
||||||
// aux en-têtes position:sticky de rester calés sur la fenêtre).
|
|
||||||
// syncing évite les boucles dans la même pile d'exécution.
|
|
||||||
let syncing = false;
|
|
||||||
const onBar = () => {
|
|
||||||
if (syncing) return;
|
|
||||||
syncing = true;
|
|
||||||
window.scrollTo(bar.scrollLeft, window.scrollY);
|
|
||||||
syncing = false;
|
|
||||||
};
|
|
||||||
const onPage = () => {
|
|
||||||
if (syncing) return;
|
|
||||||
syncing = true;
|
|
||||||
bar.scrollLeft = window.scrollX;
|
|
||||||
syncing = false;
|
|
||||||
};
|
|
||||||
bar.addEventListener("scroll", onBar);
|
|
||||||
// SPA : ce listener est intentionnellement conservé pour toute la durée de vie de la page.
|
|
||||||
window.addEventListener("scroll", onPage);
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
const total = document.documentElement.scrollWidth;
|
const total = document.documentElement.scrollWidth;
|
||||||
const visible = window.innerWidth;
|
const visible = window.innerWidth;
|
||||||
inner.style.width = total + "px";
|
const trackW = bar.clientWidth;
|
||||||
// +1 absorbe les erreurs d'arrondi sous-pixel
|
const thumbW = Math.max(40, (visible / total) * trackW);
|
||||||
bar.classList.toggle("is-hidden", total <= visible + 1);
|
const scrollRange = total - visible;
|
||||||
bar.scrollLeft = window.scrollX;
|
const thumbRange = trackW - thumbW;
|
||||||
|
return { total, visible, trackW, thumbW, scrollRange, thumbRange };
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Mise à jour de la position du thumb ---
|
||||||
|
const syncThumb = () => {
|
||||||
|
const { total, visible, thumbW, scrollRange, thumbRange } = metrics();
|
||||||
|
if (total <= visible + 1 || thumbRange <= 0) return;
|
||||||
|
thumb.style.width = thumbW + "px";
|
||||||
|
const fraction = scrollRange > 0 ? window.scrollX / scrollRange : 0;
|
||||||
|
thumb.style.left = Math.round(fraction * thumbRange) + "px";
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Drag souris + tactile ---
|
||||||
|
let dragStartX = null;
|
||||||
|
let dragScrollStart = null;
|
||||||
|
|
||||||
|
const startDrag = (clientX) => {
|
||||||
|
dragStartX = clientX;
|
||||||
|
dragScrollStart = window.scrollX;
|
||||||
|
};
|
||||||
|
const moveDrag = (clientX) => {
|
||||||
|
if (dragStartX === null) return;
|
||||||
|
const dx = clientX - dragStartX;
|
||||||
|
const { scrollRange, thumbRange } = metrics();
|
||||||
|
if (thumbRange <= 0) return;
|
||||||
|
window.scrollTo(
|
||||||
|
dragScrollStart + (dx / thumbRange) * scrollRange,
|
||||||
|
window.scrollY
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const endDrag = () => {
|
||||||
|
dragStartX = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
thumb.addEventListener("mousedown", (e) => {
|
||||||
|
startDrag(e.clientX);
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
document.addEventListener("mousemove", (e) => moveDrag(e.clientX));
|
||||||
|
document.addEventListener("mouseup", endDrag);
|
||||||
|
|
||||||
|
thumb.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
(e) => {
|
||||||
|
startDrag(e.touches[0].clientX);
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
document.addEventListener(
|
||||||
|
"touchmove",
|
||||||
|
(e) => {
|
||||||
|
if (dragStartX !== null) {
|
||||||
|
moveDrag(e.touches[0].clientX);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
document.addEventListener("touchend", endDrag);
|
||||||
|
|
||||||
|
// Clic sur le track (hors thumb) : saute à la position cliquée.
|
||||||
|
bar.addEventListener("click", (e) => {
|
||||||
|
if (e.target === thumb) return;
|
||||||
|
const rect = bar.getBoundingClientRect();
|
||||||
|
const { scrollRange, thumbW, thumbRange } = metrics();
|
||||||
|
const fraction = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(1, (e.clientX - rect.left - thumbW / 2) / thumbRange)
|
||||||
|
);
|
||||||
|
window.scrollTo(fraction * scrollRange, window.scrollY);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Synchronise le thumb quand la page défile (via clavier, molette, etc.).
|
||||||
|
// SPA : ce listener est intentionnellement conservé pour toute la durée de vie de la page.
|
||||||
|
window.addEventListener("scroll", syncThumb);
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
const { total, visible } = metrics();
|
||||||
|
const hasOverflow = total > visible + 1;
|
||||||
|
bar.classList.toggle("is-hidden", !hasOverflow);
|
||||||
|
if (hasOverflow) syncThumb();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Recalcule quand le tableau change (pagination, tri, filtre, données).
|
// Recalcule quand le tableau change (pagination, tri, filtre, données).
|
||||||
|
|||||||
Reference in New Issue
Block a user