/* * ShrinkVertically -- optional custom horizontal scrollbar above the board. * * Kanboard's board scrolls horizontally inside #board-container (overflow-x: auto), so the * only native horizontal scrollbar sits at the very bottom of the board. On a wide board * you have to scroll all the way down to reach it. This adds a second scrollbar just above * the columns and keeps it in sync with the board. * * Unlike a native scrollbar (whose thickness cannot be sized in Firefox), this is a custom * 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 () { "use strict"; // 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 = 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"; var thumb = document.createElement("div"); thumb.className = "sv-top-scroll-thumb"; track.appendChild(thumb); boardParent.insertBefore(track, container); var dragging = false; var dragStartX = 0; var dragStartScrollLeft = 0; function metrics() { var c = getContainer(); if (!c) { return null; } return { c: c, viewport: c.clientWidth, content: c.scrollWidth, trackW: track.clientWidth }; } function positionThumb(m, thumbW) { var maxScroll = m.content - m.viewport; var maxThumb = m.trackW - thumbW; var left = maxScroll > 0 ? (m.c.scrollLeft / maxScroll) * maxThumb : 0; thumb.style.transform = "translateX(" + Math.round(left) + "px)"; } // Recompute thumb width/position; hide the whole bar when nothing to scroll. function layout() { var m = metrics(); if (!m || m.content <= m.viewport || m.trackW <= 0) { track.style.display = "none"; return; } track.style.display = ""; var thumbW = Math.round(m.trackW * m.viewport / m.content); if (thumbW < MIN_THUMB) { thumbW = MIN_THUMB; } if (thumbW > m.trackW) { thumbW = m.trackW; } thumb.style.width = thumbW + "px"; positionThumb(m, thumbW); } // 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 || 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 = c.scrollLeft; track.classList.add("sv-dragging"); if (thumb.setPointerCapture) { thumb.setPointerCapture(e.pointerId); } }); 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; } m.c.scrollLeft = dragStartScrollLeft + (e.clientX - dragStartX) * (maxScroll / maxThumb); positionThumb(m, thumb.offsetWidth); }); function endDrag(e) { if (!dragging) { return; } dragging = false; track.classList.remove("sv-dragging"); if (thumb.releasePointerCapture && e && e.pointerId != null) { try { thumb.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ } } } thumb.addEventListener("pointerup", endDrag); thumb.addEventListener("pointercancel", endDrag); // Click on the empty track -> jump so the thumb centers under the pointer. track.addEventListener("pointerdown", function (e) { if (e.target === thumb || dragging) { return; } var m = metrics(); if (!m || m.content <= m.viewport) { return; } var thumbW = thumb.offsetWidth; var maxThumb = m.trackW - thumbW; var maxScroll = m.content - m.viewport; if (maxThumb <= 0) { return; } var clickX = e.clientX - track.getBoundingClientRect().left; var targetLeft = clickX - thumbW / 2; if (targetLeft < 0) { targetLeft = 0; } if (targetLeft > maxThumb) { targetLeft = maxThumb; } m.c.scrollLeft = (targetLeft / maxThumb) * maxScroll; positionThumb(m, thumbW); }); window.addEventListener("resize", layout); // 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(function () { reattachTrack(); layout(); }).observe(boardParent, { childList: true, subtree: true }); } layout(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();