Wider than 12 columns, pack() took the first free spot for each widget's content-fitted height, and that height changes as each widget's content arrives and on every poll, so widgets traded columns and order as the page loaded (CI 9107: test_dash_layout fails at 16, 18 and 24 columns on the old pack()). Now columns and order are decided at the saved heights, from the saved layout and the column count alone; each widget then drops at its fitted height onto what is above it in its columns, so content only closes gaps. The dashboard moves widgets into place in two passes, below everything first and then up, so Gridstack never pushes a not-yet-moved widget out of its place on the way. The share view passes saved heights the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
5.3 KiB
JavaScript
122 lines
5.3 KiB
JavaScript
/* Dashboard layout (#5249): each widget as tall as its content, never taller
|
|
than the height saved for it, and extra grid columns on wide screens.
|
|
|
|
The layout is saved in 12 columns. A viewer on a wider screen gets more
|
|
columns (one per COL_PX of grid width, up to MAX_COLS), and the widgets are
|
|
placed again in reading order at their saved widths, so more of them sit
|
|
side by side; none is stretched. Placement follows the saved layout, not
|
|
the content, so widgets hold still as they load (#5713). Edit mode always shows the saved 12-column
|
|
layout, so what is saved never depends on the screen it was edited on.
|
|
|
|
Outside edit mode each saved row is split into ROW_SPLIT finer rows, so a
|
|
widget's height fits its content to within 10px rather than to a whole
|
|
70px row (#5214). viewed() gives the saved place in those rows.
|
|
|
|
Used by the dashboard (Gridstack) and the share view (CSS grid). */
|
|
(function () {
|
|
var COL_PX = 110, MIN_COLS = 12, MAX_COLS = 24, NARROW_PX = 700;
|
|
var ROW_SPLIT = 7, MIN_ROWS = ROW_SPLIT; // view rows: one saved row is the least
|
|
|
|
function columnsFor(width) {
|
|
if (width < NARROW_PX) return 1;
|
|
return Math.max(MIN_COLS, Math.min(MAX_COLS, Math.floor(width / COL_PX)));
|
|
}
|
|
|
|
/* items: [{id, x, y, w, h, full}] from the saved layout, in view rows: h
|
|
is the height to place (the content-fitted one), full the saved height
|
|
(h when left out). Returns {id: {x, y, w, h}}.
|
|
|
|
Where a widget sits depends only on the saved layout and the column
|
|
count, never on its content (#5713): content arrives widget by widget
|
|
and changes on every poll, and placing by it moved widgets between
|
|
columns as the page loaded. So columns are decided at the saved heights:
|
|
at 12 columns each widget keeps its saved column; narrower or wider, it
|
|
takes the first free spot in reading order, never above the one before
|
|
it. Then each widget, in that order, drops at its fitted height to just
|
|
below what is already above it in its columns: a short widget closes the
|
|
gap under it, and nothing changes column or passes another. */
|
|
function pack(items, cols) {
|
|
var taken = [];
|
|
function free(x, y, w, h) {
|
|
for (var r = y; r < y + h; r++) {
|
|
if (!taken[r]) continue;
|
|
for (var c = x; c < x + w; c++) if (taken[r][c]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
function take(x, y, w, h) {
|
|
for (var r = y; r < y + h; r++) {
|
|
taken[r] = taken[r] || [];
|
|
for (var c = x; c < x + w; c++) taken[r][c] = true;
|
|
}
|
|
}
|
|
// 1. Each widget's column and its place in the order, at saved heights.
|
|
var slots = [], bottom = [], minY = 0;
|
|
items.slice().sort(function (a, b) { return a.y - b.y || a.x - b.x; }).forEach(function (it) {
|
|
var w = Math.min(it.w, cols), full = it.full || it.h, x = 0, y;
|
|
if (cols === MIN_COLS) {
|
|
x = Math.min(it.x, cols - w);
|
|
y = 0;
|
|
for (var c = x; c < x + w; c++) y = Math.max(y, bottom[c] || 0);
|
|
for (c = x; c < x + w; c++) bottom[c] = y + full;
|
|
} else {
|
|
search:
|
|
for (y = minY; ; y++) {
|
|
for (x = 0; x + w <= cols; x++) if (free(x, y, w, full)) break search;
|
|
}
|
|
take(x, y, w, full);
|
|
minY = y;
|
|
}
|
|
slots.push({ it: it, x: x, y: y, w: w });
|
|
});
|
|
// 2. Each drops at its fitted height, in that order, onto what is above it.
|
|
var out = {}, floor = [];
|
|
slots.sort(function (a, b) { return a.y - b.y || a.x - b.x; }).forEach(function (s) {
|
|
var y = 0;
|
|
for (var c = s.x; c < s.x + s.w; c++) y = Math.max(y, floor[c] || 0);
|
|
for (c = s.x; c < s.x + s.w; c++) floor[c] = y + s.it.h;
|
|
out[s.it.id] = { x: s.x, y: y, w: s.w, h: s.it.h };
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/* The height a widget's box needs to show everything in it: its padding and
|
|
border, plus each child (header, body) at its natural height. The body
|
|
normally fills the box and scrolls, so it is measured unstretched. */
|
|
function naturalHeight(box) {
|
|
var body = box.querySelector('.dash-cell-body');
|
|
var cs = getComputedStyle(box);
|
|
var h = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom)
|
|
+ parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
|
|
if (body) { body.style.flex = '0 0 auto'; body.style.overflow = 'visible'; }
|
|
Array.prototype.forEach.call(box.children, function (child) {
|
|
var s = getComputedStyle(child);
|
|
h += child.offsetHeight + parseFloat(s.marginTop) + parseFloat(s.marginBottom);
|
|
});
|
|
if (body) { body.style.flex = ''; body.style.overflow = ''; }
|
|
return h;
|
|
}
|
|
|
|
/* View rows for that height: rowPx is one view row (with its share of the
|
|
gap), gapPx the space a cell loses to its margins. Capped at maxRows, the
|
|
saved height in view rows, where the body scrolls instead. */
|
|
function fitRows(box, rowPx, gapPx, maxRows) {
|
|
var rows = Math.ceil((naturalHeight(box) + gapPx) / rowPx);
|
|
return Math.max(MIN_ROWS, Math.min(maxRows, rows));
|
|
}
|
|
|
|
function saved(el) {
|
|
return { x: +el.dataset.x, y: +el.dataset.y, w: +el.dataset.w, h: +el.dataset.h };
|
|
}
|
|
|
|
function viewed(el) {
|
|
var s = saved(el);
|
|
return { x: s.x, y: s.y * ROW_SPLIT, w: s.w, h: s.h * ROW_SPLIT };
|
|
}
|
|
|
|
window.StewardDashLayout = {
|
|
MIN_COLS: MIN_COLS, ROW_SPLIT: ROW_SPLIT, columnsFor: columnsFor, pack: pack,
|
|
fitRows: fitRows, saved: saved, viewed: viewed,
|
|
};
|
|
})();
|