From 9c27b51eedbb2447eccb46ea7e5b21ac91d9de5f Mon Sep 17 00:00:00 2001
From: Ruben Carlo Benante
Date: Mon, 6 Jul 2026 21:12:26 -0300
Subject: [PATCH] keep board horizontal scroll across refresh: restore
scrollLeft after #board-container rebuild; v1.0
---
Asset/js/keep-scroll.js | 55 +++++++++++++++++++++++++++++++++
Controller/ConfigController.php | 7 ++++-
Plugin.php | 10 +++++-
README.md | 13 ++++++++
Template/config/show.php | 7 +++++
VERSION | 2 +-
6 files changed, 91 insertions(+), 3 deletions(-)
create mode 100644 Asset/js/keep-scroll.js
diff --git a/Asset/js/keep-scroll.js b/Asset/js/keep-scroll.js
new file mode 100644
index 0000000..1cb4eac
--- /dev/null
+++ b/Asset/js/keep-scroll.js
@@ -0,0 +1,55 @@
+/*
+ * OrganonTweaks -- keep the board's horizontal scroll position across refreshes.
+ *
+ * When you drop a card (and on Kanboard's periodic AJAX polling) the board is rebuilt with
+ * `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh). The brand-new
+ * element starts at scrollLeft 0, so the board snaps back to the first column -- a jarring
+ * jump. This remembers the last scroll position and restores it the moment a replacement
+ * container appears, so the view stays put.
+ *
+ * We observe the STABLE parent (the container itself is replaced) and read the scroll from
+ * whichever #board-container is current, so it keeps working after every rebuild.
+ */
+(function () {
+ "use strict";
+
+ function init() {
+ var container = document.getElementById("board-container");
+ if (!container) {
+ return;
+ }
+
+ var parent = container.parentNode;
+ var lastScrollLeft = container.scrollLeft;
+ var lastContainer = container;
+
+ // Remember the position whenever the board is scrolled (capture: scroll does not
+ // bubble, and this survives the container being replaced).
+ document.addEventListener("scroll", function (e) {
+ var c = document.getElementById("board-container");
+ if (c && e.target === c) {
+ lastScrollLeft = c.scrollLeft;
+ }
+ }, true);
+
+ // When the board is rebuilt, #board-container becomes a new element at scrollLeft 0;
+ // restore the remembered position before the browser paints it.
+ if (window.MutationObserver) {
+ new MutationObserver(function () {
+ var c = document.getElementById("board-container");
+ if (c && c !== lastContainer) {
+ lastContainer = c;
+ if (c.scrollLeft !== lastScrollLeft) {
+ c.scrollLeft = lastScrollLeft;
+ }
+ }
+ }).observe(parent, { childList: true });
+ }
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+ } else {
+ init();
+ }
+})();
diff --git a/Controller/ConfigController.php b/Controller/ConfigController.php
index 85841f1..42d5e20 100644
--- a/Controller/ConfigController.php
+++ b/Controller/ConfigController.php
@@ -15,6 +15,7 @@ class ConfigController extends BaseController
'title' => t('Settings').' > '.t('Organon Tweaks'),
'values' => array(
'organon_tweaks_always_comment_icon' => (int) $this->configModel->get('organon_tweaks_always_comment_icon', 1),
+ 'organon_tweaks_keep_scroll' => (int) $this->configModel->get('organon_tweaks_keep_scroll', 1),
),
'errors' => array(),
)));
@@ -25,8 +26,12 @@ class ConfigController extends BaseController
$values = $this->request->getValues();
$alwaysCommentIcon = isset($values['organon_tweaks_always_comment_icon']) ? 1 : 0;
+ $keepScroll = isset($values['organon_tweaks_keep_scroll']) ? 1 : 0;
- if ($this->configModel->save(array('organon_tweaks_always_comment_icon' => $alwaysCommentIcon))) {
+ if ($this->configModel->save(array(
+ 'organon_tweaks_always_comment_icon' => $alwaysCommentIcon,
+ 'organon_tweaks_keep_scroll' => $keepScroll,
+ ))) {
$this->flash->success(t('Settings saved successfully.'));
} else {
$this->flash->failure(t('Unable to save your settings.'));
diff --git a/Plugin.php b/Plugin.php
index d259299..6b95185 100644
--- a/Plugin.php
+++ b/Plugin.php
@@ -26,6 +26,14 @@ class Plugin extends Base
if ((int) $this->configModel->get('organon_tweaks_always_comment_icon', 1) === 1) {
$this->template->hook->attach('template:board:task:icons', 'organonTweaks:board/task_comment_icon');
}
+
+ // Tweak: keep the board horizontal scroll position across refreshes (Kanboard
+ // rebuilds #board-container on card drop / polling, resetting scrollLeft to 0).
+ if ((int) $this->configModel->get('organon_tweaks_keep_scroll', 1) === 1) {
+ $this->hook->on('template:layout:js', array(
+ 'template' => 'plugins/OrganonTweaks/Asset/js/keep-scroll.js',
+ ));
+ }
}
public function getHelpers()
@@ -52,7 +60,7 @@ class Plugin extends Base
public function getPluginVersion()
{
- return '0.3.1';
+ return '1.0.0';
}
public function getPluginHomepage()
diff --git a/README.md b/README.md
index a904b38..18a636b 100644
--- a/README.md
+++ b/README.md
@@ -35,11 +35,24 @@ have comments keep showing the count as before.
read-only viewers).
- **On by default.** Toggle it under "Settings -> Organon Tweaks".
+### Keep the board scroll position across refreshes
+
+When you drop a card (or Kanboard auto-refreshes the board via polling), it rebuilds the
+board with `$("#board-container").replaceWith(...)`, and the new element starts scrolled to
+the far left -- so the view jumps back to the first column. This tweak remembers the
+horizontal scroll position and restores it as soon as the rebuilt board appears, so the
+board stays where you were.
+
+- Implemented in `Asset/js/keep-scroll.js`: it observes the stable parent (the container
+ itself is replaced), reads/writes `scrollLeft` on whichever `#board-container` is current.
+- **On by default.** Toggle it under "Settings -> Organon Tweaks".
+
## Settings
Global (per Kanboard instance) and admin-only, under "Settings -> Organon Tweaks":
- **Always show the comment icon on board cards** -- default on.
+- **Keep the board scroll position across refreshes** -- default on.
## Requirements
diff --git a/Template/config/show.php b/Template/config/show.php
index 84f995a..781e219 100644
--- a/Template/config/show.php
+++ b/Template/config/show.php
@@ -12,6 +12,13 @@