15 Commits
v0.4 ... v1.4

9 changed files with 272 additions and 10 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
PLAN-*

View File

@@ -32,3 +32,42 @@
max-height: calc(100vh - var(--sv-shrink-offset, 240px)) !important;
min-height: 0 !important;
}
/*
* Optional custom top horizontal scrollbar (built by Asset/js/top-scrollbar.js when the
* "Add a top horizontal scrollbar" setting is enabled). It is a track + draggable thumb,
* not a native scrollbar, so it renders and can be sized identically in every browser
* (Chrome, Firefox, Safari) and on touch. Thickness follows the --sv-bar-size variable
* (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 {
position: relative;
width: 100%;
height: var(--sv-bar-size, 25px);
background: rgba(0, 0, 0, 0.06);
border-radius: 4px;
overflow: hidden;
touch-action: none;
}
.sv-top-scroll-thumb {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 100%;
background: rgba(0, 0, 0, 0.35);
border-radius: 4px;
cursor: grab;
touch-action: none;
}
.sv-top-scroll.sv-dragging .sv-top-scroll-thumb {
cursor: grabbing;
background: rgba(0, 0, 0, 0.5);
}
.sv-dragging {
user-select: none;
}

154
Asset/js/top-scrollbar.js Normal file
View File

@@ -0,0 +1,154 @@
/*
* ShrinkVertically -- optional custom horizontal scrollbar above the board.
*
* Kanboard's board scrolls horizontally inside #board-container (overflow-x: auto), so the
* only native horizontal scrollbar sits at the very bottom of the board. On a wide board
* you have to scroll all the way down to reach it. This adds a second scrollbar just above
* 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.
*/
(function () {
"use strict";
// Keep the thumb wide enough to grab, even on a very wide board.
var MIN_THUMB = 24;
function init() {
var container = document.getElementById("board-container");
// Only on the board, and never add it twice.
if (!container || document.querySelector(".sv-top-scroll")) {
return;
}
var track = document.createElement("div");
track.className = "sv-top-scroll";
var thumb = document.createElement("div");
thumb.className = "sv-top-scroll-thumb";
track.appendChild(thumb);
container.parentNode.insertBefore(track, container);
var dragging = false;
var dragStartX = 0;
var dragStartScrollLeft = 0;
function metrics() {
return {
viewport: container.clientWidth,
content: container.scrollWidth,
trackW: track.clientWidth
};
}
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)";
}
// Recompute thumb width/position; hide the whole bar when nothing to scroll.
function layout() {
var m = metrics();
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 () {
if (dragging) { return; }
var m = metrics();
if (m.content <= m.viewport) { return; }
positionThumb(m, thumb.offsetWidth);
});
// 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);
}
});
thumb.addEventListener("pointermove", function (e) {
if (!dragging) { return; }
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 */ }
}
}
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") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View File

@@ -14,14 +14,22 @@ class ConfigController extends BaseController
const MIN_OFFSET = 0;
const MAX_OFFSET = 2000;
const DEFAULT_BAR_SIZE = 25;
const MIN_BAR_SIZE = 0;
const MAX_BAR_SIZE = 200;
public function show()
{
$offset = (int) $this->configModel->get('shrink_vertically_offset', self::DEFAULT_OFFSET);
$barSize = (int) $this->configModel->get('shrink_vertically_bar_size', self::DEFAULT_BAR_SIZE);
$topScrollbar = (int) $this->configModel->get('shrink_vertically_top_scrollbar', 1);
$this->response->html($this->helper->layout->config('shrinkVertically:config/show', array(
'title' => t('Settings').' &gt; '.t('Shrink Vertically'),
'values' => array(
'shrink_vertically_offset' => $offset,
'shrink_vertically_bar_size' => $barSize,
'shrink_vertically_top_scrollbar' => $topScrollbar,
),
'errors' => array(),
)));
@@ -34,7 +42,16 @@ class ConfigController extends BaseController
$offset = isset($values['shrink_vertically_offset']) ? (int) $values['shrink_vertically_offset'] : self::DEFAULT_OFFSET;
$offset = max(self::MIN_OFFSET, min(self::MAX_OFFSET, $offset));
if ($this->configModel->save(array('shrink_vertically_offset' => $offset))) {
$barSize = isset($values['shrink_vertically_bar_size']) ? (int) $values['shrink_vertically_bar_size'] : self::DEFAULT_BAR_SIZE;
$barSize = max(self::MIN_BAR_SIZE, min(self::MAX_BAR_SIZE, $barSize));
$topScrollbar = isset($values['shrink_vertically_top_scrollbar']) ? 1 : 0;
if ($this->configModel->save(array(
'shrink_vertically_offset' => $offset,
'shrink_vertically_bar_size' => $barSize,
'shrink_vertically_top_scrollbar' => $topScrollbar,
))) {
$this->flash->success(t('Settings saved successfully.'));
} else {
$this->flash->failure(t('Unable to save your settings.'));

View File

@@ -25,6 +25,13 @@ class Plugin extends Base
// Add a "Shrink Vertically" entry to the Settings sidebar.
$this->template->hook->attach('template:config:sidebar', 'shrinkVertically:config/sidebar');
// Optional: a second, mirrored horizontal scrollbar above the board columns.
if ((int) $this->configModel->get('shrink_vertically_top_scrollbar', 1) === 1) {
$this->hook->on('template:layout:js', array(
'template' => 'plugins/ShrinkVertically/Asset/js/top-scrollbar.js',
));
}
}
public function getPluginName()
@@ -44,7 +51,7 @@ class Plugin extends Base
public function getPluginVersion()
{
return '0.4.0';
return '1.4.0';
}
public function getPluginHomepage()

View File

@@ -1,7 +1,12 @@
# ShrinkVertically -- keep the board horizontal scrollbar visible
A tiny Kanboard plugin that fixes the vertically-collapsed board view so the bottom
horizontal scrollbar stays on screen.
A small Kanboard plugin focused on the vertically-collapsed board view. It fixes the
collapse so the bottom horizontal scrollbar stays on screen, and can optionally add a
second mirrored scrollbar above the columns. Configurable under
"Settings -> Shrink Vertically".
Board drag/touch and column-gap options that used to live here have moved to the separate
`TweakDrag` plugin.
## The problem
@@ -59,12 +64,32 @@ Copy this folder into your Kanboard installation as `plugins/ShrinkVertically/`.
directory name must be exactly `ShrinkVertically` (Kanboard derives the plugin namespace
from the folder name). Reload the board; no build step, no configuration.
## Top horizontal scrollbar (optional)
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
way down to move sideways. Enable "Add a top horizontal scrollbar" in the settings to get
a second scrollbar just above the columns.
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
browser (Chrome, Firefox, Safari) and on touch. Drag the thumb or click the track to move
the board; the thumb tracks the board when you scroll it by other means. Its thickness
follows the "Top Horizontal Scrollbar thickness" setting.
## Settings
Go to "Settings -> Shrink Vertically" to set the pixel offset. The value is stored in the
Kanboard configuration and injected 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 collapsed columns taller. Default: 240.
These settings are global (per Kanboard instance) and admin-only. Go to
"Settings -> Shrink Vertically":
- **Vertical offset in pixels** -- pixels reserved around the collapsed columns. Injected
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
collapsed columns taller. Default: 240.
- **Top Horizontal Scrollbar thickness in pixels** -- thickness of the custom top
scrollbar (all browsers). Injected as the `--sv-bar-size` CSS variable. Default: 25.
- **Add a top horizontal scrollbar to the board** -- toggles the mirrored top scrollbar
described above. Default: on.
## License

View File

@@ -13,6 +13,21 @@
</p>
</fieldset>
<fieldset>
<?= $this->form->label(t('Top Horizontal Scrollbar thickness in pixels'), 'shrink_vertically_bar_size') ?>
<?= $this->form->number('shrink_vertically_bar_size', $values, $errors) ?>
<p class="form-help">
<?= t('Thickness of the top horizontal scrollbar. Default: 25.') ?>
</p>
</fieldset>
<fieldset>
<?= $this->form->checkbox('shrink_vertically_top_scrollbar', t('Add a top horizontal scrollbar to the board'), 1, isset($values['shrink_vertically_top_scrollbar']) && $values['shrink_vertically_top_scrollbar'] == 1) ?>
<p class="form-help">
<?= t('Adds a second horizontal scrollbar above the columns, mirrored with the board scrollbar, so you do not have to scroll to the bottom to move sideways.') ?>
</p>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
</div>

View File

@@ -1,3 +1,6 @@
<style type="text/css">
:root { --sv-shrink-offset: <?= (int) $this->app->config('shrink_vertically_offset', 240) ?>px; }
:root {
--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;
}
</style>

View File

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