diff --git a/steward/static/dash-layout.js b/steward/static/dash-layout.js index 78b5787..2167535 100644 --- a/steward/static/dash-layout.js +++ b/steward/static/dash-layout.js @@ -4,7 +4,8 @@ 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. Edit mode always shows the saved 12-column + 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 @@ -21,13 +22,19 @@ return Math.max(MIN_COLS, Math.min(MAX_COLS, Math.floor(width / COL_PX))); } - /* items: [{id, x, y, w, h}] from the saved layout; h is the height to place - (the content-fitted one). Returns {id: {x, y, w, h}}. + /* 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}}. - Reading order (y, then x) is kept: nothing is placed above the item before - it. At 12 columns an item keeps its saved column and floats up, which is the - saved layout minus the space short widgets no longer need. Narrower or wider, - it takes the first free spot. */ + 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) { @@ -43,21 +50,32 @@ for (var c = x; c < x + w; c++) taken[r][c] = true; } } - var out = {}, minY = 0; + // 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), h = it.h, x = 0, y; + 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); - for (y = 0; !free(x, y, w, h); y++) { /* float up as far as it fits */ } + 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, h)) break search; + for (x = 0; x + w <= cols; x++) if (free(x, y, w, full)) break search; } + take(x, y, w, full); + minY = y; } - take(x, y, w, h); - minY = y; - out[it.id] = { x: x, y: y, w: w, h: h }; + 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; } diff --git a/steward/templates/dashboard/index.html b/steward/templates/dashboard/index.html index 762fd01..1a06674 100644 --- a/steward/templates/dashboard/index.html +++ b/steward/templates/dashboard/index.html @@ -87,11 +87,25 @@ var pos = L.pack(els.map(function (el) { var s = L.viewed(el); s.id = el.getAttribute('gs-id'); + s.full = s.h; s.h = el._fitH || s.h; return s; }), cols); grid.batchUpdate(); if (grid.getColumn() !== cols) grid.column(cols, 'none'); + // Two passes (#5713): moved into place one by one, a widget could land on + // one not moved yet, and Gridstack would push that one down out of its + // place. So everything first goes below the lowest widget, keeping its + // new shape, then up into place, where nothing is in the way. + var below = 0; + els.forEach(function (el) { + var n = el.gridstackNode, p = pos[el.getAttribute('gs-id')]; + below = Math.max(below, p.y + p.h, n ? n.y + n.h : 0); + }); + els.forEach(function (el) { + var p = pos[el.getAttribute('gs-id')]; + grid.update(el, { x: p.x, y: below + p.y, w: p.w, h: p.h }); + }); els.forEach(function (el) { grid.update(el, pos[el.getAttribute('gs-id')]); }); grid.batchUpdate(false); } diff --git a/steward/templates/dashboard/share_view.html b/steward/templates/dashboard/share_view.html index 277caea..11f9034 100644 --- a/steward/templates/dashboard/share_view.html +++ b/steward/templates/dashboard/share_view.html @@ -76,7 +76,7 @@ document.addEventListener('DOMContentLoaded', function () { function relayout() { var cols = L.columnsFor(grid.clientWidth); var pos = L.pack(cells().map(function (el) { - var s = L.viewed(el); s.id = el.dataset.id; s.h = el._fitH || s.h; return s; + var s = L.viewed(el); s.id = el.dataset.id; s.full = s.h; s.h = el._fitH || s.h; return s; }), cols); grid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)'; cells().forEach(function (el) {