PageSourceSearch

https://assessment.thetacore.com/_content/ThetaCore.Common.Blazor/js/widgetMasonry.rlups1skq9.js

js thetacore.com collected 2026-09-24 14:54:56 UTC 5,474 bytes, 141 lines download raw bytes

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.