3 Commits

Author SHA1 Message Date
7f9235dbdc autoscroll variable speed v1.0 2026-07-01 10:46:10 -03:00
bb3ac1c946 auto-scroll on drag 2026-07-01 10:45:07 -03:00
924e287aee drag for mobile - handle sizeable 2026-07-01 10:30:07 -03:00
8 changed files with 179 additions and 4 deletions

View File

@@ -24,3 +24,17 @@
cursor: grabbing !important; cursor: grabbing !important;
user-select: none !important; user-select: none !important;
} }
/*
* Enlarge the mobile card drag handle so it is a usable touch target.
* Kanboard reveals the .task-board-sort-handle (a FontAwesome fa-arrows-alt glyph) only
* on mobile (isMobile.any) and restricts card dragging to it; by default the glyph is
* tiny and hard to grab with a finger. FontAwesome icons are sized by font-size, so this
* is a pure font-size bump; it is inert on desktop where the handle stays display:none.
* Size comes from the --td-handle-size variable (Settings -> Tweak Drag), injected in the
* page head; falls back to 20px.
*/
.task-board-sort-handle,
.task-board-sort-handle i {
font-size: var(--td-handle-size, 20px);
}

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

@@ -0,0 +1,119 @@
/*
* 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 horizontally, and on touch the bundled Touch Punch
* shim suppresses native scrolling while a card is held), so you can only drop onto columns
* already on screen. This scrolls #board-container while a card drag is active and the
* pointer nears the left/right edge, with a smooth continuous speed that grows toward the
* edge -- so you can drag a card to the edge and roll the board to a far column.
*
* 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 = 50; // px from the board edge where auto-scroll begins
var MAX_SPEED = 22; // px per animation frame at the very edge
var EASE = 1; // 1 = linear ramp, 2 = gentle-then-fast
var jq = window.jQuery;
if (!jq) {
return;
}
var container = null;
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;
}
}
// Distance is px from the edge; returns 0 at the zone border up to MAX_SPEED at the edge.
function speedFor(distance) {
var p = (EDGE_ZONE - distance) / EDGE_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 || !container) {
return;
}
var rect = container.getBoundingClientRect();
var dLeft = lastX - rect.left;
var dRight = rect.right - lastX;
var speed = 0;
if (dLeft < EDGE_ZONE) {
speed = -speedFor(dLeft);
} else if (dRight < EDGE_ZONE) {
speed = speedFor(dRight);
}
if (speed !== 0) {
var before = container.scrollLeft;
container.scrollLeft = before + speed;
// Only when the board actually moved: nudge jQuery UI to recompute drop
// positions so the placeholder follows onto the newly revealed column.
if (container.scrollLeft !== before) {
document.dispatchEvent(new MouseEvent("mousemove", {
clientX: lastX,
clientY: lastY,
bubbles: true
}));
}
}
rafId = window.requestAnimationFrame(step);
}
function startLoop() {
container = document.getElementById("board-container");
if (!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

@@ -13,11 +13,16 @@ class ConfigController extends BaseController
const MIN_GAP_SIZE = 0; const MIN_GAP_SIZE = 0;
const MAX_GAP_SIZE = 200; const MAX_GAP_SIZE = 200;
const DEFAULT_HANDLE_SIZE = 20;
const MIN_HANDLE_SIZE = 8;
const MAX_HANDLE_SIZE = 200;
public function show() public function show()
{ {
$gapSize = (int) $this->configModel->get('tweakdrag_column_gap_size', self::DEFAULT_GAP_SIZE); $gapSize = (int) $this->configModel->get('tweakdrag_column_gap_size', self::DEFAULT_GAP_SIZE);
$columnGap = (int) $this->configModel->get('tweakdrag_column_gap', 1); $columnGap = (int) $this->configModel->get('tweakdrag_column_gap', 1);
$dragScroll = (int) $this->configModel->get('tweakdrag_drag_scroll', 1); $dragScroll = (int) $this->configModel->get('tweakdrag_drag_scroll', 1);
$handleSize = (int) $this->configModel->get('tweakdrag_handle_size', self::DEFAULT_HANDLE_SIZE);
$this->response->html($this->helper->layout->config('tweakDrag:config/show', array( $this->response->html($this->helper->layout->config('tweakDrag:config/show', array(
'title' => t('Settings').' &gt; '.t('Tweak Drag'), 'title' => t('Settings').' &gt; '.t('Tweak Drag'),
@@ -25,6 +30,7 @@ class ConfigController extends BaseController
'tweakdrag_column_gap_size' => $gapSize, 'tweakdrag_column_gap_size' => $gapSize,
'tweakdrag_column_gap' => $columnGap, 'tweakdrag_column_gap' => $columnGap,
'tweakdrag_drag_scroll' => $dragScroll, 'tweakdrag_drag_scroll' => $dragScroll,
'tweakdrag_handle_size' => $handleSize,
), ),
'errors' => array(), 'errors' => array(),
))); )));
@@ -40,10 +46,14 @@ class ConfigController extends BaseController
$columnGap = isset($values['tweakdrag_column_gap']) ? 1 : 0; $columnGap = isset($values['tweakdrag_column_gap']) ? 1 : 0;
$dragScroll = isset($values['tweakdrag_drag_scroll']) ? 1 : 0; $dragScroll = isset($values['tweakdrag_drag_scroll']) ? 1 : 0;
$handleSize = isset($values['tweakdrag_handle_size']) ? (int) $values['tweakdrag_handle_size'] : self::DEFAULT_HANDLE_SIZE;
$handleSize = max(self::MIN_HANDLE_SIZE, min(self::MAX_HANDLE_SIZE, $handleSize));
if ($this->configModel->save(array( if ($this->configModel->save(array(
'tweakdrag_column_gap_size' => $gapSize, 'tweakdrag_column_gap_size' => $gapSize,
'tweakdrag_column_gap' => $columnGap, 'tweakdrag_column_gap' => $columnGap,
'tweakdrag_drag_scroll' => $dragScroll, 'tweakdrag_drag_scroll' => $dragScroll,
'tweakdrag_handle_size' => $handleSize,
))) { ))) {
$this->flash->success(t('Settings saved successfully.')); $this->flash->success(t('Settings saved successfully.'));
} else { } else {

View File

@@ -25,6 +25,12 @@ class Plugin extends Base
'template' => 'plugins/TweakDrag/Asset/js/drag-scroll.js', '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() public function getPluginName()
@@ -44,7 +50,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '0.2.0'; return '1.0.0';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -1,12 +1,16 @@
# TweakDrag -- board drag, touch and column-gap tweaks # TweakDrag -- board drag, touch and column-gap tweaks
A Kanboard plugin that groups board dragging and spacing tweaks. This first version A Kanboard plugin that groups board dragging and spacing tweaks, configurable under
provides two options, both configurable under "Settings -> Tweak Drag": "Settings -> Tweak Drag":
- **Drag the board background to scroll** (desktop) -- click and drag on the empty board - **Drag the board background to scroll** (desktop) -- click and drag on the empty board
background to scroll the columns sideways, like the Trello board canvas. background to scroll the columns sideways, like the Trello board canvas.
- **Widen the gap between columns** -- spread the columns further apart, easier to tell - **Widen the gap between columns** -- spread the columns further apart, easier to tell
apart and to touch. 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 that is off screen (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
@@ -23,6 +27,17 @@ movement threshold so ending a drag does not open the task) without bloating tha
by the `--td-column-gap` CSS variable. This spreads the columns without shrinking the by the `--td-column-gap` CSS variable. This spreads the columns without shrinking the
cards and keeps each column header aligned with its list. It also adds some spacing at cards and keeps each column header aligned with its list. It also adds some spacing at
the left and right edges of the board. the left and right edges of the board.
- **Mobile drag handle size**: Kanboard already supports dragging cards on mobile, but only
via a small handle (a FontAwesome `fa-arrows-alt` glyph it reveals on touch devices) that
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 `#board-container` when the pointer nears
its left/right edge. Speed ramps up smoothly toward the edge. After each scroll step it
dispatches a synthetic `mousemove` so jQuery UI moves the drop placeholder onto the newly
revealed column. It only acts during an active drag, so it is idle otherwise.
## Requirements ## Requirements
@@ -43,6 +58,8 @@ These settings are global (per Kanboard instance) and admin-only. Go to
- **Widen the gap between columns** -- toggles the wider gap. Default: on. - **Widen the gap between columns** -- toggles the wider gap. Default: on.
- **Drag the board background to scroll horizontally** -- toggles the Trello-style - **Drag the board background to scroll horizontally** -- toggles the Trello-style
click-and-drag panning. Desktop (mouse) for now. Default: on. click-and-drag panning. Desktop (mouse) for now. Default: on.
- **Mobile drag handle size in pixels** -- size of the mobile card drag handle
(`--td-handle-size`). No effect on desktop. Default: 20.
## License ## License

View File

@@ -27,6 +27,14 @@
</p> </p>
</fieldset> </fieldset>
<fieldset>
<?= $this->form->label(t('Mobile drag handle size in pixels'), 'tweakdrag_handle_size') ?>
<?= $this->form->number('tweakdrag_handle_size', $values, $errors) ?>
<p class="form-help">
<?= t('Size of the card drag handle that Kanboard shows on mobile, so it is easy to grab with a finger. No effect on desktop. Default: 20.') ?>
</p>
</fieldset>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button> <button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
</div> </div>

View File

@@ -1,6 +1,7 @@
<style type="text/css"> <style type="text/css">
:root { :root {
--td-column-gap: <?= (int) $this->app->config('tweakdrag_column_gap_size', 25) ?>px; --td-column-gap: <?= (int) $this->app->config('tweakdrag_column_gap_size', 25) ?>px;
--td-handle-size: <?= (int) $this->app->config('tweakdrag_handle_size', 20) ?>px;
} }
<?php if ((int) $this->app->config('tweakdrag_column_gap', 1) === 1): ?> <?php if ((int) $this->app->config('tweakdrag_column_gap', 1) === 1): ?>
#board { border-collapse: separate !important; border-spacing: var(--td-column-gap, 25px) 0 !important; } #board { border-collapse: separate !important; border-spacing: var(--td-column-gap, 25px) 0 !important; }

View File

@@ -1 +1 @@
TweakDrag v0.2 TweakDrag v1.0