/* * TweakDrag -- auto-scroll the board while dragging a card near a screen edge. * * Kanboard drags cards with jQuery UI sortable. The board does not scroll during a drag * (jQuery UI's autoscroll is unreliable, and on touch the bundled Touch Punch shim * suppresses native scrolling while a card is held), so you can only drop where already on * screen. While a card drag is active and the pointer nears an edge, this scrolls toward it * with a smooth continuous speed that grows toward the edge: * * - Horizontal: scrolls #board-container (near its left/right edge). * - Vertical: scrolls the window/page (near the viewport top/bottom). This matters when * the vertical-collapse view is OFF and the whole page scrolls; when collapse is ON the * page is not scrollable so window scrolling no-ops and the per-column scroll stays * native. * * The edge zone is a percentage of each axis (clamped), so it adapts to screen size and * orientation: a narrow phone width gets a small horizontal zone, a tall phone height a * larger vertical one, a wide desktop the reverse -- with no device detection. * * It only acts during an active card drag near an edge, so it is harmless when idle. Works * on desktop (mouse) and mobile (touch, via Touch Punch). No core changes. */ (function () { "use strict"; var EDGE_ZONE_PCT = 0.15; // edge zone = 15% of the axis length... var EDGE_ZONE_MIN = 30; // ...clamped to at least 30px... var EDGE_ZONE_MAX = 160; // ...and at most 160px. var MAX_SPEED = 22; // px per animation frame at the very edge var EASE = 1; // 1 = linear ramp, 2 = gentle-then-fast var SCROLL_SAFETY = 200; // px of page we may scroll past the bottom of the columns var jq = window.jQuery; if (!jq) { return; } var dragging = false; var rafId = 0; var lastX = 0; var lastY = 0; function onMove(e) { if (e.touches && e.touches.length) { lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; } else { lastX = e.clientX; lastY = e.clientY; } } // Edge zone for an axis of the given length (px), 15% clamped to [30, 160]. function zoneFor(length) { var z = length * EDGE_ZONE_PCT; if (z < EDGE_ZONE_MIN) { z = EDGE_ZONE_MIN; } if (z > EDGE_ZONE_MAX) { z = EDGE_ZONE_MAX; } return z; } // Distance is px from the edge; 0 at the zone border up to MAX_SPEED at the edge. function speedFor(distance, zone) { var p = (zone - distance) / zone; if (p <= 0) { return 0; } if (p > 1) { p = 1; } if (EASE !== 1) { p = Math.pow(p, EASE); } return MAX_SPEED * p; } function step() { if (!dragging) { return; } var moved = false; // Horizontal: scroll the board container near its left/right edge. var c = document.getElementById("board-container"); if (c) { var rect = c.getBoundingClientRect(); var zoneH = zoneFor(rect.width); var hspeed = 0; var dLeft = lastX - rect.left; var dRight = rect.right - lastX; if (dLeft < zoneH) { hspeed = -speedFor(dLeft, zoneH); } else if (dRight < zoneH) { hspeed = speedFor(dRight, zoneH); } if (hspeed !== 0) { var beforeX = c.scrollLeft; c.scrollLeft = beforeX + hspeed; if (c.scrollLeft !== beforeX) { moved = true; } } } // Vertical: scroll the window near the viewport top/bottom (collapse-off case; // a natural no-op when the page is not scrollable). var zoneV = zoneFor(window.innerHeight); var vspeed = 0; var dTop = lastY; var dBottom = window.innerHeight - lastY; if (dTop < zoneV) { vspeed = -speedFor(dTop, zoneV); } else if (dBottom < zoneV) { vspeed = speedFor(dBottom, zoneV); } // Cap downward scrolling to just past the bottom of the columns, so a drag held at // the bottom edge cannot scroll the page forever. #board is the columns table, so // its height is the tallest column; allow SCROLL_SAFETY px beyond it. if (vspeed > 0) { var boardEl = document.getElementById("board"); if (boardEl) { var boardBottomPage = boardEl.getBoundingClientRect().bottom + window.pageYOffset; var maxScrollY = boardBottomPage + SCROLL_SAFETY - window.innerHeight; if (window.pageYOffset >= maxScrollY) { vspeed = 0; } } } if (vspeed !== 0) { var beforeY = window.pageYOffset; window.scrollBy(0, vspeed); if (window.pageYOffset !== beforeY) { moved = true; } } // When the board/page actually moved, nudge jQuery UI to recompute drop positions // so the placeholder follows onto the newly revealed area (either axis). Dispatch // through jQuery with EXPLICIT page coordinates: a raw synthetic MouseEvent does not // carry a correct pageY on mobile browsers, which made jQuery UI drop the dragged // card to a wrong vertical position (X right, Y jumping up). if (moved) { jq(document).trigger(jq.Event("mousemove", { which: 1, clientX: lastX, clientY: lastY, pageX: lastX + window.pageXOffset, pageY: lastY + window.pageYOffset })); } rafId = window.requestAnimationFrame(step); } function startLoop() { if (!document.getElementById("board-container")) { return; } dragging = true; if (!rafId) { rafId = window.requestAnimationFrame(step); } } function stopLoop() { dragging = false; if (rafId) { window.cancelAnimationFrame(rafId); rafId = 0; } } // Track the pointer at all times (cheap; only used while dragging). document.addEventListener("mousemove", onMove, true); document.addEventListener("touchmove", onMove, true); document.addEventListener("pointermove", onMove, true); // A card drag started / stopped. jQuery UI sortable fires these (on touch too, via // Touch Punch); delegated on the document so board redraws need no rebinding. jq(document).on("sortstart", startLoop); jq(document).on("sortstop", stopLoop); })();