restore vertical axes of scroll bars
This commit is contained in:
@@ -1,41 +1,108 @@
|
||||
/*
|
||||
* OrganonTweaks -- keep the board's horizontal scroll position across refreshes AND full reloads.
|
||||
* OrganonTweaks -- keep the board's scroll position (horizontal AND vertical) across board AJAX
|
||||
* rebuilds and full page reloads.
|
||||
*
|
||||
* Two cases lose the horizontal scroll and snap the board back to the first column:
|
||||
* Two events reset the scroll and jump the board around:
|
||||
* 1. AJAX rebuild -- dropping a card / periodic polling replaces #board-container
|
||||
* (BoardDragAndDrop.refresh), and the new element starts at scrollLeft 0.
|
||||
* 2. Full page reload -- e.g. clicking the Done/Todo badge, which navigates and redirects back to
|
||||
* the board; a fresh page starts at scrollLeft 0.
|
||||
* (BoardDragAndDrop.refresh); the new element starts at scroll 0.
|
||||
* 2. Full page reload -- e.g. clicking the Todo/Done badge or the "Mark all" action, which
|
||||
* navigate and redirect back to the board; a fresh page starts at scroll 0.
|
||||
*
|
||||
* We remember the last position and restore it: in-memory for the AJAX rebuild (observing the STABLE
|
||||
* parent, since the container itself is replaced), and in sessionStorage (keyed per board) so it
|
||||
* survives a full reload as well.
|
||||
* Axes restored (all persisted per board in sessionStorage, so they survive a full reload):
|
||||
* - horizontal: #board-container.scrollLeft (key :hx)
|
||||
* - page vertical (expanded mode): window.scrollY (key :vy)
|
||||
* - per-column vertical (compact mode): each native .board-task-list-compact scrollTop, keyed by
|
||||
* its data-swimlane-id + data-column-id (key :vcol:<sw>:<col>)
|
||||
*
|
||||
* The per-column container is native (core board.css: .board-task-list-compact { overflow-y:auto });
|
||||
* ShrinkVertically only re-tunes its max-height, so this works with or without that plugin. Doing all
|
||||
* axes every time is safe: in expanded mode there are no compact lists (that loop is a no-op) and the
|
||||
* page owns the vertical scroll; in compact mode the columns own it and the page barely moves.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function boardKey() {
|
||||
function baseKey() {
|
||||
var m = location.href.match(/board\/(\d+)/) || location.href.match(/project_id=(\d+)/);
|
||||
return "organon-board-scroll-" + (m ? m[1] : location.pathname);
|
||||
}
|
||||
|
||||
function readStored() {
|
||||
function readNum(suffix) {
|
||||
try {
|
||||
var v = window.sessionStorage.getItem(boardKey());
|
||||
return v ? parseInt(v, 10) : 0;
|
||||
var v = window.sessionStorage.getItem(baseKey() + suffix);
|
||||
return v !== null ? parseInt(v, 10) : null;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeStored(value) {
|
||||
function writeNum(suffix, value) {
|
||||
try {
|
||||
window.sessionStorage.setItem(boardKey(), value);
|
||||
window.sessionStorage.setItem(baseKey() + suffix, value);
|
||||
} catch (e) {
|
||||
// sessionStorage unavailable (private mode / disabled) -- degrade to AJAX-only restore.
|
||||
}
|
||||
}
|
||||
|
||||
// While we restore programmatically, the browser fires scroll events; suppress the save briefly so
|
||||
// a not-yet-scrollable column (whose scrollTop stays 0) does not overwrite the stored position.
|
||||
var suppress = 0;
|
||||
function suppressBriefly() {
|
||||
suppress++;
|
||||
window.setTimeout(function () {
|
||||
if (suppress > 0) {
|
||||
suppress--;
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function colSuffix(el) {
|
||||
return ":vcol:" + (el.getAttribute("data-swimlane-id") || "0") + ":" + (el.getAttribute("data-column-id") || "0");
|
||||
}
|
||||
|
||||
// Restore the vertical axes. Called on load, again on the next frame (after ShrinkVertically has
|
||||
// settled the column heights), and on every board rebuild.
|
||||
function restoreVertical() {
|
||||
var y = readNum(":vy");
|
||||
if (y !== null && Math.round(window.scrollY) !== y) {
|
||||
suppressBriefly();
|
||||
window.scrollTo(window.scrollX, y);
|
||||
}
|
||||
|
||||
var lists = document.querySelectorAll("#board .board-task-list-compact");
|
||||
for (var i = 0; i < lists.length; i++) {
|
||||
var el = lists[i];
|
||||
var s = readNum(colSuffix(el));
|
||||
if (s !== null && el.scrollTop !== s) {
|
||||
suppressBriefly();
|
||||
el.scrollTop = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Core adds the .board-task-list-compact class via JS AFTER load (BoardVerticalScrolling.render),
|
||||
// and re-adds it on every board render -- so the per-column scroll containers often do not exist
|
||||
// yet when we first restore (this is why compact restore worked on F5, where the browser restores
|
||||
// element scroll natively, but not after a badge/redirect navigation). Re-run the vertical restore
|
||||
// whenever the board mutates or a class changes, rAF-coalesced. Idempotent (only sets scrollTop on
|
||||
// a mismatch), so it lands once the compact lists appear and never fights the user afterwards.
|
||||
var restoreScheduled = false;
|
||||
function scheduleRestore() {
|
||||
if (restoreScheduled) {
|
||||
return;
|
||||
}
|
||||
restoreScheduled = true;
|
||||
var run = function () {
|
||||
restoreScheduled = false;
|
||||
restoreVertical();
|
||||
};
|
||||
if (window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame(run);
|
||||
} else {
|
||||
window.setTimeout(run, 16);
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
var container = document.getElementById("board-container");
|
||||
if (!container) {
|
||||
@@ -43,37 +110,61 @@
|
||||
}
|
||||
|
||||
var parent = container.parentNode;
|
||||
var stored = readStored();
|
||||
var lastScrollLeft = stored || container.scrollLeft;
|
||||
var lastContainer = container;
|
||||
|
||||
// Restore across a full page reload (badge click, etc.), not only AJAX rebuilds.
|
||||
if (stored && container.scrollLeft !== stored) {
|
||||
container.scrollLeft = stored;
|
||||
// --- horizontal ---
|
||||
var storedX = readNum(":hx");
|
||||
var lastScrollLeft = storedX !== null ? storedX : container.scrollLeft;
|
||||
var lastContainer = container;
|
||||
if (storedX !== null && container.scrollLeft !== storedX) {
|
||||
suppressBriefly();
|
||||
container.scrollLeft = storedX;
|
||||
}
|
||||
|
||||
// Remember the position whenever the board is scrolled (capture: scroll does not bubble, and
|
||||
// this survives the container being replaced).
|
||||
// --- vertical (page + per-column), now and again after layout settles ---
|
||||
restoreVertical();
|
||||
if (window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame(restoreVertical);
|
||||
}
|
||||
|
||||
// Save on scroll. Capture phase: scroll does not bubble, but a capture listener on document
|
||||
// still receives it from #board-container and from any per-column list.
|
||||
document.addEventListener("scroll", function (e) {
|
||||
if (suppress > 0) {
|
||||
return;
|
||||
}
|
||||
var t = e.target;
|
||||
var c = document.getElementById("board-container");
|
||||
if (c && e.target === c) {
|
||||
if (t === c) {
|
||||
lastScrollLeft = c.scrollLeft;
|
||||
writeStored(lastScrollLeft);
|
||||
writeNum(":hx", c.scrollLeft);
|
||||
} else if (t && t.nodeType === 1 && t.classList && t.classList.contains("board-task-list-compact")) {
|
||||
writeNum(colSuffix(t), t.scrollTop);
|
||||
}
|
||||
}, true);
|
||||
|
||||
// When the board is rebuilt, #board-container becomes a new element at scrollLeft 0; restore
|
||||
// the remembered position before the browser paints it.
|
||||
// Page vertical scroll targets the document, so listen on window.
|
||||
window.addEventListener("scroll", function () {
|
||||
if (suppress === 0) {
|
||||
writeNum(":vy", Math.round(window.scrollY));
|
||||
}
|
||||
});
|
||||
|
||||
// Observe the STABLE parent for: (a) #board-container being replaced on an AJAX rebuild
|
||||
// (childList) -> restore horizontal; and (b) class changes anywhere below (attributes) -> the
|
||||
// moment core adds .board-task-list-compact, restore the per-column verticals. subtree covers
|
||||
// the deep .board-task-list elements. Both funnel into the rAF-coalesced scheduleRestore.
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(function () {
|
||||
var c = document.getElementById("board-container");
|
||||
if (c && c !== lastContainer) {
|
||||
lastContainer = c;
|
||||
if (c.scrollLeft !== lastScrollLeft) {
|
||||
suppressBriefly();
|
||||
c.scrollLeft = lastScrollLeft;
|
||||
}
|
||||
}
|
||||
}).observe(parent, { childList: true });
|
||||
scheduleRestore();
|
||||
}).observe(parent, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user