/* 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, }; })();