From 1743f9b8680184cb03552124558923e716bca74d Mon Sep 17 00:00:00 2001 From: Ruben Carlo Benante Date: Mon, 6 Jul 2026 07:46:45 -0300 Subject: [PATCH] fix top scrollbar freezing after card drag: re-resolve #board-container on rebuild, sync on parent; v1.4.2 --- Asset/js/top-scrollbar.js | 69 ++++++++++++++++++++++++++++++--------- Plugin.php | 2 +- VERSION | 2 +- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/Asset/js/top-scrollbar.js b/Asset/js/top-scrollbar.js index 4a243b3..5a1dbbd 100644 --- a/Asset/js/top-scrollbar.js +++ b/Asset/js/top-scrollbar.js @@ -10,6 +10,14 @@ * track + thumb widget: it renders and behaves the same in every browser (Chrome, Firefox, * Safari) and on touch, and its thickness follows the --sv-bar-size setting. * + * Resilience note: after a card drag Kanboard rebuilds the board with + * `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh), so the element we + * started with is destroyed and replaced. We therefore never cache the container: we + * re-resolve it on every use, listen for its scroll on `document` in the capture phase (so + * the listener survives the swap), and observe the STABLE parent (which is not replaced) so + * the widget keeps working -- previously the observer and scroll listener died with the old + * element, which froze the bar and lost sync after each move. + * * Loaded only when the "Add a top horizontal scrollbar" setting is enabled. */ (function () { @@ -18,14 +26,21 @@ // Keep the thumb wide enough to grab, even on a very wide board. var MIN_THUMB = 24; + function getContainer() { + return document.getElementById("board-container"); + } + function init() { - var container = document.getElementById("board-container"); + var container = getContainer(); // Only on the board, and never add it twice. if (!container || document.querySelector(".sv-top-scroll")) { return; } + // The parent survives the board refresh (only #board-container is replaced). + var boardParent = container.parentNode; + var track = document.createElement("div"); track.className = "sv-top-scroll"; @@ -33,16 +48,21 @@ thumb.className = "sv-top-scroll-thumb"; track.appendChild(thumb); - container.parentNode.insertBefore(track, container); + boardParent.insertBefore(track, container); var dragging = false; var dragStartX = 0; var dragStartScrollLeft = 0; function metrics() { + var c = getContainer(); + if (!c) { + return null; + } return { - viewport: container.clientWidth, - content: container.scrollWidth, + c: c, + viewport: c.clientWidth, + content: c.scrollWidth, trackW: track.clientWidth }; } @@ -50,7 +70,7 @@ function positionThumb(m, thumbW) { var maxScroll = m.content - m.viewport; var maxThumb = m.trackW - thumbW; - var left = maxScroll > 0 ? (container.scrollLeft / maxScroll) * maxThumb : 0; + var left = maxScroll > 0 ? (m.c.scrollLeft / maxScroll) * maxThumb : 0; thumb.style.transform = "translateX(" + Math.round(left) + "px)"; } @@ -58,7 +78,7 @@ function layout() { var m = metrics(); - if (m.content <= m.viewport || m.trackW <= 0) { + if (!m || m.content <= m.viewport || m.trackW <= 0) { track.style.display = "none"; return; } @@ -73,21 +93,33 @@ positionThumb(m, thumbW); } - // Board scrolled by any other means -> keep the thumb in sync. - container.addEventListener("scroll", function () { + // Keep the track directly before the (current) container after a board refresh. + function reattachTrack() { + var c = getContainer(); + if (c && c.parentNode && track.nextSibling !== c) { + c.parentNode.insertBefore(track, c); + } + } + + // Board scrolled by any other means -> keep the thumb in sync. Capture-phase on + // document catches #board-container's (non-bubbling) scroll and survives the swap. + document.addEventListener("scroll", function (e) { if (dragging) { return; } + if (e.target !== getContainer()) { return; } var m = metrics(); - if (m.content <= m.viewport) { return; } + if (!m || m.content <= m.viewport) { return; } positionThumb(m, thumb.offsetWidth); - }); + }, true); // Drag the thumb (mouse + touch via Pointer Events). thumb.addEventListener("pointerdown", function (e) { + var c = getContainer(); + if (!c) { return; } e.preventDefault(); e.stopPropagation(); dragging = true; dragStartX = e.clientX; - dragStartScrollLeft = container.scrollLeft; + dragStartScrollLeft = c.scrollLeft; track.classList.add("sv-dragging"); if (thumb.setPointerCapture) { thumb.setPointerCapture(e.pointerId); @@ -97,10 +129,11 @@ thumb.addEventListener("pointermove", function (e) { if (!dragging) { return; } var m = metrics(); + if (!m) { return; } var maxThumb = m.trackW - thumb.offsetWidth; var maxScroll = m.content - m.viewport; if (maxThumb <= 0) { return; } - container.scrollLeft = dragStartScrollLeft + (e.clientX - dragStartX) * (maxScroll / maxThumb); + m.c.scrollLeft = dragStartScrollLeft + (e.clientX - dragStartX) * (maxScroll / maxThumb); positionThumb(m, thumb.offsetWidth); }); @@ -120,7 +153,7 @@ track.addEventListener("pointerdown", function (e) { if (e.target === thumb || dragging) { return; } var m = metrics(); - if (m.content <= m.viewport) { return; } + if (!m || m.content <= m.viewport) { return; } var thumbW = thumb.offsetWidth; var maxThumb = m.trackW - thumbW; @@ -132,15 +165,19 @@ if (targetLeft < 0) { targetLeft = 0; } if (targetLeft > maxThumb) { targetLeft = maxThumb; } - container.scrollLeft = (targetLeft / maxThumb) * maxScroll; + m.c.scrollLeft = (targetLeft / maxThumb) * maxScroll; positionThumb(m, thumbW); }); window.addEventListener("resize", layout); - // The board can be redrawn by AJAX polling / drag-and-drop; keep sizes current. + // Observe the STABLE parent so we keep working after #board-container is replaced by + // a card move / AJAX polling; re-seat the track and recompute sizes on any change. if (window.MutationObserver) { - new MutationObserver(layout).observe(container, { childList: true, subtree: true }); + new MutationObserver(function () { + reattachTrack(); + layout(); + }).observe(boardParent, { childList: true, subtree: true }); } layout(); diff --git a/Plugin.php b/Plugin.php index 0514f80..ca85fc7 100644 --- a/Plugin.php +++ b/Plugin.php @@ -51,7 +51,7 @@ class Plugin extends Base public function getPluginVersion() { - return '1.4.1'; + return '1.4.2'; } public function getPluginHomepage() diff --git a/VERSION b/VERSION index 69a11c9..b162917 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -ShrinkVertically v1.4.1 +ShrinkVertically v1.4.2