7 Commits
v0.3 ... v1.2.3

6 changed files with 259 additions and 18 deletions

View File

@@ -6,16 +6,17 @@
* --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
* "Drag the board background to scroll" setting is enabled). The .td-drag-scroll class is
* set on #board-container by the script, so these rules are inert when disabled. A grab
* "Drag the board background to scroll" setting is enabled). The .td-drag-scroll marker is
* 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
* does a card drag, not a pan.
*/
#board-container.td-drag-scroll {
body.td-drag-scroll #board-container {
cursor: grab;
}
#board-container.td-drag-scroll .task-board {
body.td-drag-scroll #board-container .task-board {
cursor: default;
}

187
Asset/js/drag-autoscroll.js Normal file
View File

@@ -0,0 +1,187 @@
/*
* 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);
})();

View File

@@ -6,6 +6,13 @@
* 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.
*
* 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.
*/
(function () {
@@ -21,34 +28,40 @@
}
function init() {
var container = document.getElementById("board-container");
if (!container || container.getAttribute("data-td-drag") === "1") {
if (!document.getElementById("board-container") || document.body.getAttribute("data-td-drag") === "1") {
return;
}
container.setAttribute("data-td-drag", "1");
container.classList.add("td-drag-scroll");
// Marker on <body> (stable) enables the grab cursor via CSS, surviving board rebuilds.
document.body.setAttribute("data-td-drag", "1");
document.body.classList.add("td-drag-scroll");
var dragging = false;
var container = null;
var startX = 0;
var startScrollLeft = 0;
container.addEventListener("mousedown", function (e) {
// Left button only, and only on empty board background.
if (e.button !== 0 || isIgnored(e.target)) {
document.addEventListener("mousedown", function (e) {
if (e.button !== 0) {
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;
startX = e.clientX;
startScrollLeft = container.scrollLeft;
container.classList.add("td-grabbing");
startScrollLeft = c.scrollLeft;
document.body.classList.add("td-grabbing");
e.preventDefault();
});
document.addEventListener("mousemove", function (e) {
if (!dragging) {
if (!dragging || !container) {
return;
}
container.scrollLeft = startScrollLeft - (e.clientX - startX);
@@ -60,7 +73,8 @@
return;
}
dragging = false;
container.classList.remove("td-grabbing");
container = null;
document.body.classList.remove("td-grabbing");
}
document.addEventListener("mouseup", stop);

View File

@@ -25,6 +25,12 @@ class Plugin extends Base
'template' => 'plugins/TweakDrag/Asset/js/drag-scroll.js',
));
}
// Auto-scroll the board while dragging a card near a screen edge. Always loaded;
// it only acts during an active card drag.
$this->hook->on('template:layout:js', array(
'template' => 'plugins/TweakDrag/Asset/js/drag-autoscroll.js',
));
}
public function getPluginName()
@@ -44,7 +50,7 @@ class Plugin extends Base
public function getPluginVersion()
{
return '0.3.0';
return '1.2.3';
}
public function getPluginHomepage()

View File

@@ -9,6 +9,10 @@ A Kanboard plugin that groups board dragging and spacing tweaks, configurable un
apart and to touch.
- **Mobile drag handle size** -- enlarge the card drag handle Kanboard shows on mobile so
it is easy to grab with a finger.
- **Drag auto-scroll** -- while dragging a card, the board scrolls when the pointer nears a
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
drag/touch behaviour can grow on its own (planned: mobile drag-to-move cards, and a drag
@@ -30,6 +34,15 @@ movement threshold so ending a drag does not open the task) without bloating tha
is hard to grab. FontAwesome icons are sized by `font-size`, so a single rule scales the
handle from the `--td-handle-size` CSS variable. It is inert on desktop, where Kanboard
keeps the handle hidden.
- **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
Punch) and, while a card drag is active, scrolls toward whichever edge the pointer nears:
the board (`#board-container`) horizontally, and the window/page vertically (for the
collapse-off view; a no-op when the page does not scroll). Speed ramps up smoothly toward
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
@@ -56,3 +69,23 @@ These settings are global (per Kanboard instance) and admin-only. Go to
## 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 v0.3
TweakDrag v1.2.3