|
|
|
|
@@ -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"] });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|