2026-07-01 10:45:07 -03:00
|
|
|
/*
|
|
|
|
|
* 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
|
2026-07-06 20:25:02 -03:00
|
|
|
* (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.
|
2026-07-01 10:45:07 -03:00
|
|
|
*
|
|
|
|
|
* 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";
|
|
|
|
|
|
2026-07-06 20:25:02 -03:00
|
|
|
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
|
2026-07-06 20:47:01 -03:00
|
|
|
var SCROLL_SAFETY = 200; // px of page we may scroll past the bottom of the columns
|
2026-07-01 10:45:07 -03:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 20:25:02 -03:00
|
|
|
// 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;
|
2026-07-01 10:45:07 -03:00
|
|
|
if (p <= 0) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
if (p > 1) {
|
|
|
|
|
p = 1;
|
|
|
|
|
}
|
|
|
|
|
if (EASE !== 1) {
|
|
|
|
|
p = Math.pow(p, EASE);
|
|
|
|
|
}
|
|
|
|
|
return MAX_SPEED * p;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function step() {
|
2026-07-06 20:25:02 -03:00
|
|
|
if (!dragging) {
|
2026-07-01 10:45:07 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 20:25:02 -03:00
|
|
|
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);
|
|
|
|
|
}
|
2026-07-01 10:45:07 -03:00
|
|
|
|
2026-07-06 20:25:02 -03:00
|
|
|
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);
|
2026-07-01 10:45:07 -03:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 20:47:01 -03:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 20:25:02 -03:00
|
|
|
if (vspeed !== 0) {
|
|
|
|
|
var beforeY = window.pageYOffset;
|
|
|
|
|
window.scrollBy(0, vspeed);
|
|
|
|
|
if (window.pageYOffset !== beforeY) {
|
|
|
|
|
moved = true;
|
2026-07-01 10:45:07 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 20:25:02 -03:00
|
|
|
// When the board/page actually moved, nudge jQuery UI to recompute drop positions
|
2026-07-06 20:47:01 -03:00
|
|
|
// 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).
|
2026-07-06 20:25:02 -03:00
|
|
|
if (moved) {
|
2026-07-06 20:47:01 -03:00
|
|
|
jq(document).trigger(jq.Event("mousemove", {
|
|
|
|
|
which: 1,
|
2026-07-06 20:25:02 -03:00
|
|
|
clientX: lastX,
|
|
|
|
|
clientY: lastY,
|
2026-07-06 20:47:01 -03:00
|
|
|
pageX: lastX + window.pageXOffset,
|
|
|
|
|
pageY: lastY + window.pageYOffset
|
2026-07-06 20:25:02 -03:00
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-01 10:45:07 -03:00
|
|
|
rafId = window.requestAnimationFrame(step);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startLoop() {
|
2026-07-06 20:25:02 -03:00
|
|
|
if (!document.getElementById("board-container")) {
|
2026-07-01 10:45:07 -03:00
|
|
|
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);
|
|
|
|
|
})();
|