5 Commits
v1.0 ... v1.2.3

6 changed files with 165 additions and 57 deletions

View File

@@ -6,16 +6,17 @@
* --td-column-gap value, so it is not in this static stylesheet. * --td-column-gap value, so it is not in this static stylesheet.
* *
* Below: click-and-drag horizontal scrolling (added by Asset/js/drag-scroll.js when the * Below: click-and-drag horizontal scrolling (added by Asset/js/drag-scroll.js when the
* "Drag the board background to scroll" setting is enabled). The .td-drag-scroll class is * "Drag the board background to scroll" setting is enabled). The .td-drag-scroll marker is
* set on #board-container by the script, so these rules are inert when disabled. A grab * set on <body> by the script (not on #board-container, which Kanboard replaces on every
* card move), so these rules are inert when disabled and survive board rebuilds. A grab
* cursor advertises the empty background; cards keep the normal cursor since pressing them * cursor advertises the empty background; cards keep the normal cursor since pressing them
* does a card drag, not a pan. * does a card drag, not a pan.
*/ */
#board-container.td-drag-scroll { body.td-drag-scroll #board-container {
cursor: grab; cursor: grab;
} }
#board-container.td-drag-scroll .task-board { body.td-drag-scroll #board-container .task-board {
cursor: default; cursor: default;
} }

View File

@@ -2,11 +2,20 @@
* TweakDrag -- auto-scroll the board while dragging a card near a screen edge. * 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 * Kanboard drags cards with jQuery UI sortable. The board does not scroll during a drag
* (jQuery UI's autoscroll is unreliable horizontally, and on touch the bundled Touch Punch * (jQuery UI's autoscroll is unreliable, and on touch the bundled Touch Punch shim
* shim suppresses native scrolling while a card is held), so you can only drop onto columns * suppresses native scrolling while a card is held), so you can only drop where already on
* already on screen. This scrolls #board-container while a card drag is active and the * screen. While a card drag is active and the pointer nears an edge, this scrolls toward it
* pointer nears the left/right edge, with a smooth continuous speed that grows toward the * with a smooth continuous speed that grows toward the edge:
* edge -- so you can drag a card to the edge and roll the board to a far column. *
* - 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 * 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. * on desktop (mouse) and mobile (touch, via Touch Punch). No core changes.
@@ -14,16 +23,18 @@
(function () { (function () {
"use strict"; "use strict";
var EDGE_ZONE = 50; // px from the board edge where auto-scroll begins 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 MAX_SPEED = 22; // px per animation frame at the very edge
var EASE = 1; // 1 = linear ramp, 2 = gentle-then-fast 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; var jq = window.jQuery;
if (!jq) { if (!jq) {
return; return;
} }
var container = null;
var dragging = false; var dragging = false;
var rafId = 0; var rafId = 0;
var lastX = 0; var lastX = 0;
@@ -39,9 +50,17 @@
} }
} }
// Distance is px from the edge; returns 0 at the zone border up to MAX_SPEED at the edge. // Edge zone for an axis of the given length (px), 15% clamped to [30, 160].
function speedFor(distance) { function zoneFor(length) {
var p = (EDGE_ZONE - distance) / EDGE_ZONE; 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) { if (p <= 0) {
return 0; return 0;
} }
@@ -55,42 +74,91 @@
} }
function step() { function step() {
if (!dragging || !container) { if (!dragging) {
return; return;
} }
var rect = container.getBoundingClientRect(); 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 dLeft = lastX - rect.left;
var dRight = rect.right - lastX; var dRight = rect.right - lastX;
var speed = 0;
if (dLeft < EDGE_ZONE) { if (dLeft < zoneH) {
speed = -speedFor(dLeft); hspeed = -speedFor(dLeft, zoneH);
} else if (dRight < EDGE_ZONE) { } else if (dRight < zoneH) {
speed = speedFor(dRight); hspeed = speedFor(dRight, zoneH);
} }
if (speed !== 0) { if (hspeed !== 0) {
var before = container.scrollLeft; var beforeX = c.scrollLeft;
container.scrollLeft = before + speed; c.scrollLeft = beforeX + hspeed;
if (c.scrollLeft !== beforeX) {
moved = true;
}
}
}
// Only when the board actually moved: nudge jQuery UI to recompute drop // Vertical: scroll the window near the viewport top/bottom (collapse-off case;
// positions so the placeholder follows onto the newly revealed column. // a natural no-op when the page is not scrollable).
if (container.scrollLeft !== before) { var zoneV = zoneFor(window.innerHeight);
document.dispatchEvent(new MouseEvent("mousemove", { 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, clientX: lastX,
clientY: lastY, clientY: lastY,
bubbles: true pageX: lastX + window.pageXOffset,
pageY: lastY + window.pageYOffset
})); }));
} }
}
rafId = window.requestAnimationFrame(step); rafId = window.requestAnimationFrame(step);
} }
function startLoop() { function startLoop() {
container = document.getElementById("board-container"); if (!document.getElementById("board-container")) {
if (!container) {
return; return;
} }
dragging = true; dragging = true;

View File

@@ -6,6 +6,13 @@
* pressing a task card, a drag handle, or any interactive control is ignored, so this * pressing a task card, a drag handle, or any interactive control is ignored, so this
* never interferes with Kanboard's own card and column drag-and-drop. * never interferes with Kanboard's own card and column drag-and-drop.
* *
* Resilience note: after a card drag Kanboard rebuilds the board with
* `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh), destroying the
* element. So we must NOT bind to #board-container directly: we listen on `document` and
* re-resolve the container on each press, and the grab-cursor marker lives on <body>
* (which is never replaced). Previously the mousedown listener and the cursor class were
* on the container, so background dragging stopped working after every card move.
*
* Loaded only when the "Drag the board background to scroll" setting is enabled. * Loaded only when the "Drag the board background to scroll" setting is enabled.
*/ */
(function () { (function () {
@@ -21,34 +28,40 @@
} }
function init() { function init() {
var container = document.getElementById("board-container"); if (!document.getElementById("board-container") || document.body.getAttribute("data-td-drag") === "1") {
if (!container || container.getAttribute("data-td-drag") === "1") {
return; return;
} }
container.setAttribute("data-td-drag", "1"); // Marker on <body> (stable) enables the grab cursor via CSS, surviving board rebuilds.
container.classList.add("td-drag-scroll"); document.body.setAttribute("data-td-drag", "1");
document.body.classList.add("td-drag-scroll");
var dragging = false; var dragging = false;
var container = null;
var startX = 0; var startX = 0;
var startScrollLeft = 0; var startScrollLeft = 0;
container.addEventListener("mousedown", function (e) { document.addEventListener("mousedown", function (e) {
// Left button only, and only on empty board background. if (e.button !== 0) {
if (e.button !== 0 || isIgnored(e.target)) {
return; return;
} }
// Re-resolve the current container and only act on its empty background.
var c = document.getElementById("board-container");
if (!c || !c.contains(e.target) || isIgnored(e.target)) {
return;
}
container = c;
dragging = true; dragging = true;
startX = e.clientX; startX = e.clientX;
startScrollLeft = container.scrollLeft; startScrollLeft = c.scrollLeft;
container.classList.add("td-grabbing"); document.body.classList.add("td-grabbing");
e.preventDefault(); e.preventDefault();
}); });
document.addEventListener("mousemove", function (e) { document.addEventListener("mousemove", function (e) {
if (!dragging) { if (!dragging || !container) {
return; return;
} }
container.scrollLeft = startScrollLeft - (e.clientX - startX); container.scrollLeft = startScrollLeft - (e.clientX - startX);
@@ -60,7 +73,8 @@
return; return;
} }
dragging = false; dragging = false;
container.classList.remove("td-grabbing"); container = null;
document.body.classList.remove("td-grabbing");
} }
document.addEventListener("mouseup", stop); document.addEventListener("mouseup", stop);

View File

@@ -50,7 +50,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '1.0.0'; return '1.2.3';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -10,7 +10,9 @@ A Kanboard plugin that groups board dragging and spacing tweaks, configurable un
- **Mobile drag handle size** -- enlarge the card drag handle Kanboard shows on mobile so - **Mobile drag handle size** -- enlarge the card drag handle Kanboard shows on mobile so
it is easy to grab with a finger. it is easy to grab with a finger.
- **Drag auto-scroll** -- while dragging a card, the board scrolls when the pointer nears a - **Drag auto-scroll** -- while dragging a card, the board scrolls when the pointer nears a
screen edge, so you can reach a column that is off screen (desktop and mobile). screen edge, so you can reach a column or row that is off screen. Horizontal scrolls the
board; vertical scrolls the page (when the vertical-collapse view is off). Works on
desktop and mobile.
These options were originally part of the ShrinkVertically plugin and moved here so that These options were originally part of the ShrinkVertically plugin and moved here so that
drag/touch behaviour can grow on its own (planned: mobile drag-to-move cards, and a drag drag/touch behaviour can grow on its own (planned: mobile drag-to-move cards, and a drag
@@ -34,10 +36,13 @@ movement threshold so ending a drag does not open the task) without bloating tha
keeps the handle hidden. keeps the handle hidden.
- **Drag auto-scroll**: a script (`Asset/js/drag-autoscroll.js`) listens for jQuery UI - **Drag auto-scroll**: a script (`Asset/js/drag-autoscroll.js`) listens for jQuery UI
sortable's `sortstart`/`sortstop` (which also fire on touch via Kanboard's bundled Touch sortable's `sortstart`/`sortstop` (which also fire on touch via Kanboard's bundled Touch
Punch) and, while a card drag is active, scrolls `#board-container` when the pointer nears Punch) and, while a card drag is active, scrolls toward whichever edge the pointer nears:
its left/right edge. Speed ramps up smoothly toward the edge. After each scroll step it the board (`#board-container`) horizontally, and the window/page vertically (for the
dispatches a synthetic `mousemove` so jQuery UI moves the drop placeholder onto the newly collapse-off view; a no-op when the page does not scroll). Speed ramps up smoothly toward
revealed column. It only acts during an active drag, so it is idle otherwise. the edge. The edge zone is 15% of each axis, clamped to 30-160px, so it adapts to screen
size and orientation. After each scroll step it dispatches a synthetic `mousemove` so
jQuery UI moves the drop placeholder onto the newly revealed area. It only acts during an
active drag, so it is idle otherwise.
## Requirements ## Requirements
@@ -64,3 +69,23 @@ These settings are global (per Kanboard instance) and admin-only. Go to
## License ## License
AGPL-3.0. See LICENSE. AGPL-3.0. See LICENSE.
## More Kanboard plugins by Dr. Bèco
All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco):
- **[RecoReco](https://code.beco.cc/beco/RecoReco)** -- calendar-scheduled recurring cards (yearly,
monthly, weekly, daily), driven by the card due date.
- **[FinanceBuddy](https://code.beco.cc/beco/FinanceBuddy)** -- attach a money value (debit/credit
and installments) to cards, shown on the card and totalled per column.
- **[QualKard](https://code.beco.cc/beco/QualKard)** -- flashcard spaced repetition: grade a card by
dragging it to a column, and it comes back when due (needs RecoReco).
- **[WorkspaceOrg](https://code.beco.cc/beco/WorkspaceOrg)** -- group your projects into personal
per-user workspaces, shown on a "My workspaces" dashboard page with a badge on each project.
- **[OrganonTweaks](https://code.beco.cc/beco/OrganonTweaks)** -- an umbrella of small
quality-of-life board tweaks: remove an empty column, always show the comment icon, emphasize due
dates, extra search keywords and shared board filters, and more.
- **[BulkMoveTasks](https://code.beco.cc/beco/BulkMoveTasks)** -- move every task from one board
column to another in a single action.
- **[ShrinkVertically](https://code.beco.cc/beco/ShrinkVertically)** -- shrink vertically-collapsed
board columns so the horizontal scrollbar stays within reach.

View File

@@ -1 +1 @@
TweakDrag v1.0 TweakDrag v1.2.3