1// Row-aligned layout for WidgetGroup: keeps the 12-column widths (grid-column span) and gives every 2// widget in the same visual row a shared grid-row span, sized to the tallest widget in that row. This 3// lets each row's height adapt to its content while stopping a short widget's leftover space from 4// pulling the next widget up into it. A ResizeObserver re-runs when widgets load data or the viewport 5// changes. This is progressive enhancement â without it the grid falls back to a normal row layout. 6 7const registry = new WeakMap(); 8 9// Layout writes (setting grid-row spans) change element sizes, which the ResizeObserver picks up. Doing 10// that write synchronously inside the observer callback re-triggers the observer within the same 11// delivery cycle, producing the benign but noisy "ResizeObserver loop completed with undelivered 12// notifications" error. Deferring to requestAnimationFrame moves the writes out of that cycle and 13// coalesces a burst of notifications into a single pass. 14function scheduleLayout(container, baseRow, gap) { 15 const ctx = registry.get(container); 16 if (!ctx) { 17 doLayout(container, baseRow, gap); 18 return; 19 } 20 21 if (ctx.frame) return; 22 ctx.frame = requestAnimationFrame(() => { 23 ctx.frame = 0; 24 doLayout(container, baseRow, gap); 25 }); 26} 27 28export function observe(container, baseRow, gap) { 29 if (!container) return; 30 31 if (registry.has(container)) { 32 layout(container, baseRow, gap); 33 return; 34 } 35 36 const ctx = { ro: null, relayout: null, frame: 0, lastWidth: container.getBoundingClientRect().width }; 37 registry.set(container, ctx); 38 39 ctx.relayout = () => scheduleLayout(container, baseRow, gap); 40 41 // Setting grid-row spans changes the container's HEIGHT, which the observer would report and turn 42 // into another relayout â a self-feeding loop that surfaces as the benign "ResizeObserver loop 43 // completed with undelivered notifications" error. So on the container we react only to WIDTH 44 // changes (a possible column-count change); item content changes are always relevant (data loaded, 45 // text reflowed) and never caused by our own writes, since content stays height:auto. 46 ctx.ro = new ResizeObserver(entries => { 47 let relevant = false; 48 for (const entry of entries) { 49 if (entry.target === container) { 50 const width = entry.contentRect.width; 51 if (Math.abs(width - ctx.lastWidth) > 0.5) { 52 ctx.lastWidth = width; 53 relevant = true; 54 } 55 } else { 56 relevant = true; 57 } 58 } 59 if (relevant) scheduleLayout(container, baseRow, gap); 60 }); 61 62 ctx.ro.observe(container); 63 container.querySelectorAll('.widget-masonry-item').forEach(item => { 64 if (item.firstElementChild) ctx.ro.observe(item.firstElementChild); 65 }); 66 67 window.addEventListener('resize', ctx.relayout); 68 69 container.classList.add('masonry-ready'); 70 ctx.relayout(); 71} 72 73export function layout(container, baseRow, gap) { 74 if (!container) return; 75 76 // Observe any items added since the last layout (e.g. on re-render). 77 const ctx = registry.get(container); 78 if (ctx) { 79 container.querySelectorAll('.widget-masonry-item').forEach(item => { 80 if (item.firstElementChild) ctx.ro.observe(item.firstElementChild); 81 }); 82 } 83 84 container.classList.add('masonry-ready'); 85 scheduleLayout(container, baseRow, gap); 86} 87 88export function dispose(container) { 89 const ctx = registry.get(container); 90 if (!ctx) return; 91 92 if (ctx.frame) cancelAnimationFrame(ctx.frame); 93 ctx.ro.disconnect(); 94 window.removeEventListener('resize', ctx.relayout); 95 registry.delete(container); 96} 97 98function doLayout(container, baseRow, gap) { 99 if (!container) return; 100 101 const base = baseRow || 8; 102 const g = gap || 0; 103 104 // Number of grid columns (resolves to 1 on the narrow single-column breakpoint). 105 const cols = getComputedStyle(container).gridTemplateColumns.split(' ').length || 12; 106 107 const items = [...container.querySelectorAll('.widget-masonry-item')].filter(i => i.firstElementChild); 108 109 // Walk items in source order, grouping them into rows by accumulating their column spans. Each 110 // row's widgets all get the tallest span in that row so the next row starts below it, rather than 111 // a shorter widget's gap letting the following widget climb up beside a taller neighbour. 112 let row = []; 113 let used = 0; 114 115 const flushRow = () => { 116 if (!row.length) return; 117 const span = row.reduce((max, item) => { 118 const height = item.firstElementChild.getBoundingClientRect().height; 119 return Math.max(max, Math.ceil((height + g) / (base + g))); 120 }, 1); 121 // Only write when the span actually changes â redundant writes are wasted reflow that can 122 // feed the ResizeObserver loop. 123 const value = 'span ' + span; 124 row.forEach(item => { 125 if (item.style.gridRowEnd !== value) item.style.gridRowEnd = value; 126 }); 127 row = []; 128 used = 0; 129 }; 130
131 items.forEach(item => { 132 const match = /span\s+(\d+)/.exec(item.style.gridColumn || ''); 133 const colSpan = Math.min(match ? parseInt(match[1], 10) : 1, cols); 134 135 if (used + colSpan > cols) flushRow(); 136 row.push(item); 137 used += colSpan; 138 }); 139 140 flushRow(); 141}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.