6 Commits
v1.2 ... v1.4

8 changed files with 160 additions and 192 deletions

View File

@@ -34,69 +34,40 @@
} }
/* /*
* Optional top horizontal scrollbar (added by Asset/js/top-scrollbar.js when the * Optional custom top horizontal scrollbar (built by Asset/js/top-scrollbar.js when the
* "Add a top horizontal scrollbar" setting is enabled). The inner element is given a * "Add a top horizontal scrollbar" setting is enabled). It is a track + draggable thumb,
* width equal to the board's scrollable width by the script; overflow-x: auto then * not a native scrollbar, so it renders and can be sized identically in every browser
* produces a scrollbar whose range mirrors the board's own bottom scrollbar. The 1px * (Chrome, Firefox, Safari) and on touch. Thickness follows the --sv-bar-size variable
* inner height keeps the bar to just the scrollbar itself. * (Settings -> Shrink Vertically), injected in the page head; falls back to 25px.
* touch-action: none lets a finger drag the thumb without scrolling the page.
*/ */
.sv-top-scroll { .sv-top-scroll {
overflow-x: auto; position: relative;
overflow-y: hidden; width: 100%;
}
.sv-top-scroll-inner {
height: 1px;
}
/*
* Make the top scrollbar thick enough to grab easily (touch / big fingers).
* WebKit/Blink (most desktop browsers and mobile Chrome/Safari) let us size the
* scrollbar; styling the thumb/track is needed for it to stay visible once customised.
* Thickness comes from the --sv-bar-size variable (Settings -> Shrink Vertically),
* injected in the page head; falls back to 25px. Chromium/WebKit only (Chrome/Edge/
* Safari, mobile Chrome/Safari); Firefox ignores ::-webkit-scrollbar and keeps its
* default thin bar -- accepted for now.
*/
.sv-top-scroll::-webkit-scrollbar {
height: var(--sv-bar-size, 25px); height: var(--sv-bar-size, 25px);
background: rgba(0, 0, 0, 0.06);
border-radius: 4px;
overflow: hidden;
touch-action: none;
} }
.sv-top-scroll::-webkit-scrollbar-track { .sv-top-scroll-thumb {
background: rgba(0, 0, 0, 0.05); position: absolute;
} top: 0;
left: 0;
.sv-top-scroll::-webkit-scrollbar-thumb { width: 0;
height: 100%;
background: rgba(0, 0, 0, 0.35); background: rgba(0, 0, 0, 0.35);
border-radius: 6px; border-radius: 4px;
}
/*
* Click-and-drag horizontal scrolling (added by Asset/js/drag-scroll.js when the
* "Drag the board background to scroll" setting is enabled). The .sv-drag-scroll class
* is set on #board-container by the script, so these rules are inert when disabled.
* A grab cursor advertises the empty background; cards keep the normal cursor since
* pressing them does a card drag, not a pan.
*/
#board-container.sv-drag-scroll {
cursor: grab; cursor: grab;
touch-action: none;
} }
#board-container.sv-drag-scroll .task-board { .sv-top-scroll.sv-dragging .sv-top-scroll-thumb {
cursor: default; cursor: grabbing;
background: rgba(0, 0, 0, 0.5);
} }
.sv-grabbing, .sv-dragging {
.sv-grabbing * { user-select: none;
cursor: grabbing !important;
user-select: none !important;
} }
/*
* Wider gutter between board columns is applied conditionally from
* Template/layout/variable.php (only when the "Widen the gap between columns" setting is
* on), using border-spacing with the --sv-bar-size value. border-spacing is the natural
* control for the gap between table columns: it spreads them without shrinking the cards
* and keeps each column header aligned with its task list. It is not in this static
* stylesheet so it can be toggled per the setting.
*/

View File

@@ -1,75 +0,0 @@
/*
* ShrinkVertically -- optional click-and-drag horizontal scrolling of the board.
*
* Like the Trello board canvas: press the empty board background and drag left/right
* to scroll #board-container horizontally. The pan only starts on empty background --
* 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.
*
* Loaded only when the "Drag the board background to scroll" setting is enabled.
*/
(function () {
"use strict";
// Pressing any of these (or their descendants) must NOT start a background pan.
var IGNORE = "a, button, input, textarea, select, label, " +
".task-board, [data-task-id], .task-board-sort-handle, " +
".draggable-row-handle, .dropdown, .ui-sortable-handle";
function isIgnored(el) {
return !!(el && el.closest && el.closest(IGNORE));
}
function init() {
var container = document.getElementById("board-container");
if (!container || container.getAttribute("data-sv-drag") === "1") {
return;
}
container.setAttribute("data-sv-drag", "1");
container.classList.add("sv-drag-scroll");
var dragging = false;
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)) {
return;
}
dragging = true;
startX = e.clientX;
startScrollLeft = container.scrollLeft;
container.classList.add("sv-grabbing");
e.preventDefault();
});
document.addEventListener("mousemove", function (e) {
if (!dragging) {
return;
}
container.scrollLeft = startScrollLeft - (e.clientX - startX);
e.preventDefault();
});
function stop() {
if (!dragging) {
return;
}
dragging = false;
container.classList.remove("sv-grabbing");
}
document.addEventListener("mouseup", stop);
document.addEventListener("mouseleave", stop);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View File

@@ -1,16 +1,23 @@
/* /*
* ShrinkVertically -- optional horizontal scrollbar above the board. * ShrinkVertically -- optional custom horizontal scrollbar above the board.
* *
* Kanboard's board scrolls horizontally inside #board-container (overflow-x: auto), * Kanboard's board scrolls horizontally inside #board-container (overflow-x: auto), so the
* so the only horizontal scrollbar sits at the very bottom of the board. On a wide * only native horizontal scrollbar sits at the very bottom of the board. On a wide board
* board you have to scroll all the way down to reach it. This adds a second, mirrored * you have to scroll all the way down to reach it. This adds a second scrollbar just above
* scrollbar just above the columns and keeps the two scroll positions in sync. * the columns and keeps it in sync with the board.
*
* Unlike a native scrollbar (whose thickness cannot be sized in Firefox), this is a custom
* track + thumb widget: it renders and behaves the same in every browser (Chrome, Firefox,
* Safari) and on touch, and its thickness follows the --sv-bar-size setting.
* *
* Loaded only when the "Add a top horizontal scrollbar" setting is enabled. * Loaded only when the "Add a top horizontal scrollbar" setting is enabled.
*/ */
(function () { (function () {
"use strict"; "use strict";
// Keep the thumb wide enough to grab, even on a very wide board.
var MIN_THUMB = 24;
function init() { function init() {
var container = document.getElementById("board-container"); var container = document.getElementById("board-container");
@@ -19,45 +26,124 @@
return; return;
} }
var bar = document.createElement("div"); var track = document.createElement("div");
bar.className = "sv-top-scroll"; track.className = "sv-top-scroll";
var inner = document.createElement("div"); var thumb = document.createElement("div");
inner.className = "sv-top-scroll-inner"; thumb.className = "sv-top-scroll-thumb";
bar.appendChild(inner); track.appendChild(thumb);
container.parentNode.insertBefore(bar, container); container.parentNode.insertBefore(track, container);
function sync() { var dragging = false;
// Match the dummy bar's content width to the board's scrollable width var dragStartX = 0;
// so its scrollbar has the same range as the board's own scrollbar. var dragStartScrollLeft = 0;
inner.style.width = container.scrollWidth + "px";
function metrics() {
return {
viewport: container.clientWidth,
content: container.scrollWidth,
trackW: track.clientWidth
};
} }
var lock = false; function positionThumb(m, thumbW) {
var maxScroll = m.content - m.viewport;
var maxThumb = m.trackW - thumbW;
var left = maxScroll > 0 ? (container.scrollLeft / maxScroll) * maxThumb : 0;
thumb.style.transform = "translateX(" + Math.round(left) + "px)";
}
bar.addEventListener("scroll", function () { // Recompute thumb width/position; hide the whole bar when nothing to scroll.
if (lock) { return; } function layout() {
lock = true; var m = metrics();
container.scrollLeft = bar.scrollLeft;
lock = false;
});
if (m.content <= m.viewport || m.trackW <= 0) {
track.style.display = "none";
return;
}
track.style.display = "";
var thumbW = Math.round(m.trackW * m.viewport / m.content);
if (thumbW < MIN_THUMB) { thumbW = MIN_THUMB; }
if (thumbW > m.trackW) { thumbW = m.trackW; }
thumb.style.width = thumbW + "px";
positionThumb(m, thumbW);
}
// Board scrolled by any other means -> keep the thumb in sync.
container.addEventListener("scroll", function () { container.addEventListener("scroll", function () {
if (lock) { return; } if (dragging) { return; }
lock = true; var m = metrics();
bar.scrollLeft = container.scrollLeft; if (m.content <= m.viewport) { return; }
lock = false; positionThumb(m, thumb.offsetWidth);
}); });
window.addEventListener("resize", sync); // Drag the thumb (mouse + touch via Pointer Events).
thumb.addEventListener("pointerdown", function (e) {
e.preventDefault();
e.stopPropagation();
dragging = true;
dragStartX = e.clientX;
dragStartScrollLeft = container.scrollLeft;
track.classList.add("sv-dragging");
if (thumb.setPointerCapture) {
thumb.setPointerCapture(e.pointerId);
}
});
// The board can be redrawn by the AJAX polling/refresh; keep the width current. thumb.addEventListener("pointermove", function (e) {
if (window.MutationObserver) { if (!dragging) { return; }
new MutationObserver(sync).observe(container, { childList: true, subtree: true }); var m = metrics();
var maxThumb = m.trackW - thumb.offsetWidth;
var maxScroll = m.content - m.viewport;
if (maxThumb <= 0) { return; }
container.scrollLeft = dragStartScrollLeft + (e.clientX - dragStartX) * (maxScroll / maxThumb);
positionThumb(m, thumb.offsetWidth);
});
function endDrag(e) {
if (!dragging) { return; }
dragging = false;
track.classList.remove("sv-dragging");
if (thumb.releasePointerCapture && e && e.pointerId != null) {
try { thumb.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
}
} }
sync(); thumb.addEventListener("pointerup", endDrag);
thumb.addEventListener("pointercancel", endDrag);
// Click on the empty track -> jump so the thumb centers under the pointer.
track.addEventListener("pointerdown", function (e) {
if (e.target === thumb || dragging) { return; }
var m = metrics();
if (m.content <= m.viewport) { return; }
var thumbW = thumb.offsetWidth;
var maxThumb = m.trackW - thumbW;
var maxScroll = m.content - m.viewport;
if (maxThumb <= 0) { return; }
var clickX = e.clientX - track.getBoundingClientRect().left;
var targetLeft = clickX - thumbW / 2;
if (targetLeft < 0) { targetLeft = 0; }
if (targetLeft > maxThumb) { targetLeft = maxThumb; }
container.scrollLeft = (targetLeft / maxThumb) * maxScroll;
positionThumb(m, thumbW);
});
window.addEventListener("resize", layout);
// The board can be redrawn by AJAX polling / drag-and-drop; keep sizes current.
if (window.MutationObserver) {
new MutationObserver(layout).observe(container, { childList: true, subtree: true });
}
layout();
} }
if (document.readyState === "loading") { if (document.readyState === "loading") {

View File

@@ -51,7 +51,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '1.3.0'; return '1.4.0';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -1,11 +1,13 @@
# ShrinkVertically -- keep the board horizontal scrollbar visible # ShrinkVertically -- keep the board horizontal scrollbar visible
A small Kanboard plugin focused on getting around the board horizontally. It fixes the A small Kanboard plugin focused on the vertically-collapsed board view. It fixes the
vertically-collapsed board view so the bottom scrollbar stays on screen, can optionally collapse so the bottom horizontal scrollbar stays on screen, and can optionally add a
add a second mirrored scrollbar above the columns, and can optionally let you drag the second mirrored scrollbar above the columns. Configurable under
board background to scroll (like Trello). Everything is configurable under
"Settings -> Shrink Vertically". "Settings -> Shrink Vertically".
Board drag/touch and column-gap options that used to live here have moved to the separate
`TweakDrag` plugin.
## The problem ## The problem
Kanboard's board gear menu has an "expand vertically / collapse vertically" toggle. When Kanboard's board gear menu has an "expand vertically / collapse vertically" toggle. When
@@ -67,20 +69,13 @@ from the folder name). Reload the board; no build step, no configuration.
Kanboard's board scrolls horizontally inside `#board-container`, so its only horizontal Kanboard's board scrolls horizontally inside `#board-container`, so its only horizontal
scrollbar is at the very bottom of the board -- on a wide board you must scroll all the scrollbar is at the very bottom of the board -- on a wide board you must scroll all the
way down to move sideways. Enable "Add a top horizontal scrollbar" in the settings to get way down to move sideways. Enable "Add a top horizontal scrollbar" in the settings to get
a second, mirrored scrollbar just above the columns. A small script a second scrollbar just above the columns.
(`Asset/js/top-scrollbar.js`, loaded only when the option is on) inserts a dummy scroll
strip above `#board-container` and keeps its scroll position in sync with the board both
ways.
## Drag to scroll on Desktop (optional) It is a custom track + draggable thumb (`Asset/js/top-scrollbar.js`, loaded only when the
option is on), not a native scrollbar, so it renders and can be sized the same in every
Kanboard has no way to pan the board by grabbing it; you must use the scrollbar. Enable browser (Chrome, Firefox, Safari) and on touch. Drag the thumb or click the track to move
"Drag the board background to scroll" to click and drag on the empty board background to the board; the thumb tracks the board when you scroll it by other means. Its thickness
scroll the columns sideways, like the Trello board canvas. A small script follows the "Top Horizontal Scrollbar thickness" setting.
(`Asset/js/drag-scroll.js`, loaded only when the option is on) starts a horizontal pan on
`#board-container` only when the press lands on empty background -- pressing a task card,
a drag handle or any control is ignored, so Kanboard's own card and column drag-and-drop
keep working unchanged.
## Settings ## Settings
@@ -91,16 +86,10 @@ These settings are global (per Kanboard instance) and admin-only. Go to
in the page head as the `--sv-shrink-offset` CSS variable. Increase it if the scrollbar in the page head as the `--sv-shrink-offset` CSS variable. Increase it if the scrollbar
is still clipped (for example with a taller theme header); decrease it to make the is still clipped (for example with a taller theme header); decrease it to make the
collapsed columns taller. Default: 240. collapsed columns taller. Default: 240.
- **Scrollbar thickness and column gap in pixels** -- a single value that sets both the - **Top Horizontal Scrollbar thickness in pixels** -- thickness of the custom top
thickness of the top scrollbar (Chromium/WebKit only) and the width of the column gap. scrollbar (all browsers). Injected as the `--sv-bar-size` CSS variable. Default: 25.
Injected as the `--sv-bar-size` CSS variable. Default: 25.
- **Add a top horizontal scrollbar to the board** -- toggles the mirrored top scrollbar - **Add a top horizontal scrollbar to the board** -- toggles the mirrored top scrollbar
described above. Default: on. described above. Default: on.
- **Widen the gap between columns** -- toggles the wider gap between board columns (sized
by the value above). Also adds some spacing at the left and right edges of the board.
Default: on.
- **Drag the board background to scroll horizontally** -- toggles the Trello-style
click-and-drag panning described above. Default: on.
## License ## License

View File

@@ -14,10 +14,10 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<?= $this->form->label(t('Scrollbar thickness in pixels'), 'shrink_vertically_bar_size') ?> <?= $this->form->label(t('Top Horizontal Scrollbar thickness in pixels'), 'shrink_vertically_bar_size') ?>
<?= $this->form->number('shrink_vertically_bar_size', $values, $errors) ?> <?= $this->form->number('shrink_vertically_bar_size', $values, $errors) ?>
<p class="form-help"> <p class="form-help">
<?= t('Thickness of the top horizontal scrollbar (Chromium/WebKit browsers only). Default: 25.') ?> <?= t('Thickness of the top horizontal scrollbar. Default: 25.') ?>
</p> </p>
</fieldset> </fieldset>

View File

@@ -3,7 +3,4 @@
--sv-shrink-offset: <?= (int) $this->app->config('shrink_vertically_offset', 240) ?>px; --sv-shrink-offset: <?= (int) $this->app->config('shrink_vertically_offset', 240) ?>px;
--sv-bar-size: <?= (int) $this->app->config('shrink_vertically_bar_size', 25) ?>px; --sv-bar-size: <?= (int) $this->app->config('shrink_vertically_bar_size', 25) ?>px;
} }
<?php if ((int) $this->app->config('shrink_vertically_column_gap', 1) === 1): ?>
#board { border-collapse: separate !important; border-spacing: var(--sv-bar-size, 25px) 0 !important; }
<?php endif ?>
</style> </style>

View File

@@ -1 +1 @@
ShrinkVertically v1.2 ShrinkVertically v1.4