@unofficialbox/box-open-elements 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Row windowing for lists whose rows are *not* all the same height.
3
+ *
4
+ * `resolveRowWindow` (./virtualize.ts) multiplies a row count by one row
5
+ * height. That is exact and O(1), and it is the right answer for a flat table
6
+ * of uniform rows — but it cannot describe a surface where the items differ:
7
+ * a grouped audit log alternates short headings with taller event rows, a
8
+ * collapsed group contributes only its heading, and an expanded detail row adds
9
+ * height the count knows nothing about. Feeding those to the fixed-height
10
+ * engine does not degrade gracefully; the spacers under-report the real height,
11
+ * the scroll range stops matching the scrollbar, and `scrollTop` maps to the
12
+ * wrong item. That is why `box-table` refuses to window expandable rows rather
13
+ * than approximating them.
14
+ *
15
+ * The general answer is a cumulative offset index: the running pixel offset of
16
+ * every item, built once, then binary-searched per scroll. Building is O(n) and
17
+ * searching is O(log n), which is the right split — heights change when the
18
+ * data or the collapse state changes, not on every scroll event.
19
+ *
20
+ * Heights are still *estimates* until something measures them. This engine
21
+ * takes whatever heights it is given and is exact with respect to them; keeping
22
+ * them honest is the caller's job, the same bargain `row-height` strikes.
23
+ */
24
+ import type { RowWindow } from "./virtualize.js";
25
+ export interface OffsetIndex {
26
+ /**
27
+ * Running offsets, length `count + 1`. `offsets[i]` is the top of item `i`;
28
+ * `offsets[count]` is the total height, so the height of item `i` is
29
+ * `offsets[i + 1] - offsets[i]` with no special case for the last one.
30
+ */
31
+ readonly offsets: readonly number[];
32
+ readonly count: number;
33
+ readonly totalHeight: number;
34
+ }
35
+ export interface OffsetWindowMetrics {
36
+ /** Height of the scrolling viewport, in pixels. */
37
+ viewportHeight: number;
38
+ /** Current scroll offset of the viewport, in pixels. */
39
+ scrollTop: number;
40
+ /** Extra items rendered above and below the visible band. Default 4. */
41
+ overscan?: number;
42
+ }
43
+ /**
44
+ * The cumulative offset index for a list of item heights.
45
+ *
46
+ * A non-finite or negative height contributes zero rather than poisoning every
47
+ * subsequent offset with `NaN`. Heights arrive from measurement and from host
48
+ * estimates; one bad value should cost that item its space, not break the
49
+ * scrollbar for the whole collection.
50
+ */
51
+ export declare const buildOffsetIndex: (heights: readonly number[]) => OffsetIndex;
52
+ /**
53
+ * The rendered slice for a scroll position, plus the spacer heights.
54
+ *
55
+ * Same contract as `resolveRowWindow`, so the two engines are interchangeable
56
+ * from a component's point of view:
57
+ * - `paddingTop + (height of the rendered items) + paddingBottom` always equals
58
+ * `totalHeight`, so the scroll range never drifts as items scroll by.
59
+ * - The window always covers the visible band; overscan only ever adds items.
60
+ * - Degenerate geometry yields an empty window rather than a guess.
61
+ */
62
+ export declare const resolveOffsetWindow: (index: OffsetIndex, metrics: OffsetWindowMetrics) => RowWindow;
63
+ /**
64
+ * The pixel offset of an item, for scrolling it into view.
65
+ *
66
+ * Out-of-range indices clamp to the ends rather than returning `undefined`:
67
+ * callers are keyboard handlers reaching for "the last one", and a clamp is
68
+ * what they would write anyway.
69
+ */
70
+ export declare const offsetOfItem: (index: OffsetIndex, item: number) => number;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Row windowing for lists whose rows are *not* all the same height.
3
+ *
4
+ * `resolveRowWindow` (./virtualize.ts) multiplies a row count by one row
5
+ * height. That is exact and O(1), and it is the right answer for a flat table
6
+ * of uniform rows — but it cannot describe a surface where the items differ:
7
+ * a grouped audit log alternates short headings with taller event rows, a
8
+ * collapsed group contributes only its heading, and an expanded detail row adds
9
+ * height the count knows nothing about. Feeding those to the fixed-height
10
+ * engine does not degrade gracefully; the spacers under-report the real height,
11
+ * the scroll range stops matching the scrollbar, and `scrollTop` maps to the
12
+ * wrong item. That is why `box-table` refuses to window expandable rows rather
13
+ * than approximating them.
14
+ *
15
+ * The general answer is a cumulative offset index: the running pixel offset of
16
+ * every item, built once, then binary-searched per scroll. Building is O(n) and
17
+ * searching is O(log n), which is the right split — heights change when the
18
+ * data or the collapse state changes, not on every scroll event.
19
+ *
20
+ * Heights are still *estimates* until something measures them. This engine
21
+ * takes whatever heights it is given and is exact with respect to them; keeping
22
+ * them honest is the caller's job, the same bargain `row-height` strikes.
23
+ */
24
+ const DEFAULT_OVERSCAN = 4;
25
+ const EMPTY_INDEX = { offsets: [0], count: 0, totalHeight: 0 };
26
+ /**
27
+ * The cumulative offset index for a list of item heights.
28
+ *
29
+ * A non-finite or negative height contributes zero rather than poisoning every
30
+ * subsequent offset with `NaN`. Heights arrive from measurement and from host
31
+ * estimates; one bad value should cost that item its space, not break the
32
+ * scrollbar for the whole collection.
33
+ */
34
+ export const buildOffsetIndex = (heights) => {
35
+ if (heights.length === 0)
36
+ return EMPTY_INDEX;
37
+ const offsets = new Array(heights.length + 1);
38
+ offsets[0] = 0;
39
+ let running = 0;
40
+ for (let index = 0; index < heights.length; index++) {
41
+ const height = heights[index] ?? 0;
42
+ running += Number.isFinite(height) && height > 0 ? height : 0;
43
+ offsets[index + 1] = running;
44
+ }
45
+ return { offsets, count: heights.length, totalHeight: running };
46
+ };
47
+ /**
48
+ * The index of the last item whose top is at or before `offset`.
49
+ *
50
+ * Binary search rather than a scan: this runs per scroll frame, and a linear
51
+ * walk over ten thousand offsets is the cost windowing exists to avoid.
52
+ * Zero-height items make offsets non-strictly-increasing, so ties resolve to
53
+ * the *first* of a run — the alternative is a window that starts inside a
54
+ * cluster of collapsed items and leaves the one above it unrendered.
55
+ */
56
+ const findItemAt = (offsets, count, offset) => {
57
+ let low = 0;
58
+ let high = count - 1;
59
+ let found = 0;
60
+ while (low <= high) {
61
+ const mid = (low + high) >> 1;
62
+ if ((offsets[mid] ?? 0) <= offset) {
63
+ found = mid;
64
+ low = mid + 1;
65
+ }
66
+ else {
67
+ high = mid - 1;
68
+ }
69
+ }
70
+ // Walk back over a run of equal offsets (zero-height items) so the window
71
+ // starts at the first item sharing this offset, not an arbitrary one.
72
+ const at = offsets[found] ?? 0;
73
+ while (found > 0 && (offsets[found - 1] ?? 0) === at)
74
+ found--;
75
+ return found;
76
+ };
77
+ /**
78
+ * The rendered slice for a scroll position, plus the spacer heights.
79
+ *
80
+ * Same contract as `resolveRowWindow`, so the two engines are interchangeable
81
+ * from a component's point of view:
82
+ * - `paddingTop + (height of the rendered items) + paddingBottom` always equals
83
+ * `totalHeight`, so the scroll range never drifts as items scroll by.
84
+ * - The window always covers the visible band; overscan only ever adds items.
85
+ * - Degenerate geometry yields an empty window rather than a guess.
86
+ */
87
+ export const resolveOffsetWindow = (index, metrics) => {
88
+ const { offsets, count, totalHeight } = index;
89
+ const overscan = Math.max(0, Math.floor(metrics.overscan ?? DEFAULT_OVERSCAN));
90
+ if (count <= 0 || totalHeight <= 0) {
91
+ return { startIndex: 0, endIndex: 0, paddingTop: 0, paddingBottom: 0, totalHeight };
92
+ }
93
+ if (metrics.viewportHeight <= 0) {
94
+ // An unmeasured viewport is the normal state during first paint and under
95
+ // jsdom. Rendering everything there would defeat the purpose exactly when
96
+ // it matters most.
97
+ return { startIndex: 0, endIndex: 0, paddingTop: 0, paddingBottom: totalHeight, totalHeight };
98
+ }
99
+ // Clamp a bounced or past-the-end scrollTop: rubber-banding reports both, and
100
+ // neither may produce a window outside the collection.
101
+ const maxScroll = Math.max(0, totalHeight - metrics.viewportHeight);
102
+ const top = Math.min(Math.max(0, metrics.scrollTop), maxScroll);
103
+ const bottom = top + metrics.viewportHeight;
104
+ const firstVisible = findItemAt(offsets, count, top);
105
+ // Last item that starts before the viewport's bottom edge. Scanning forward
106
+ // from firstVisible is bounded by what fits on screen plus overscan, which is
107
+ // the point of windowing — no walk over the whole collection.
108
+ let lastVisible = firstVisible;
109
+ while (lastVisible + 1 < count && (offsets[lastVisible + 1] ?? 0) < bottom) {
110
+ lastVisible++;
111
+ }
112
+ const startIndex = Math.max(0, firstVisible - overscan);
113
+ const endIndex = Math.min(count, lastVisible + 1 + overscan);
114
+ return {
115
+ startIndex,
116
+ endIndex,
117
+ paddingTop: offsets[startIndex] ?? 0,
118
+ paddingBottom: totalHeight - (offsets[endIndex] ?? totalHeight),
119
+ totalHeight,
120
+ };
121
+ };
122
+ /**
123
+ * The pixel offset of an item, for scrolling it into view.
124
+ *
125
+ * Out-of-range indices clamp to the ends rather than returning `undefined`:
126
+ * callers are keyboard handlers reaching for "the last one", and a clamp is
127
+ * what they would write anyway.
128
+ */
129
+ export const offsetOfItem = (index, item) => {
130
+ if (index.count <= 0)
131
+ return 0;
132
+ const clamped = Math.min(Math.max(0, Math.floor(item)), index.count);
133
+ return index.offsets[clamped] ?? 0;
134
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Row windowing for long lists.
3
+ *
4
+ * A ten-thousand-row audit log rendered whole is ten thousand DOM rows: the
5
+ * first paint stalls, every subsequent patch walks the lot, and the browser
6
+ * holds layout boxes nobody will ever scroll to. Windowing renders only the
7
+ * rows near the viewport and pads the rest with spacer height, so the scrollbar
8
+ * still describes the whole collection.
9
+ *
10
+ * The arithmetic lives here, pure, for the usual reason: jsdom has no layout,
11
+ * so a DOM-level test of a windowed surface can only ever observe zeroes. It is
12
+ * shared rather than per-component so `box-table`, `box-audit-log`, and
13
+ * `box-tree-grid` cannot drift into three different definitions of "near".
14
+ *
15
+ * Fixed row height only. Measured/variable heights are a different algorithm
16
+ * (a cumulative offset index that reflows as rows are measured); pretending one
17
+ * function covers both is how windowing engines end up subtly wrong at the
18
+ * boundaries.
19
+ */
20
+ export interface WindowMetrics {
21
+ /** Total rows in the collection, not the rendered subset. */
22
+ totalRows: number;
23
+ /** Fixed height of one row, in pixels. */
24
+ rowHeight: number;
25
+ /** Height of the scrolling viewport, in pixels. */
26
+ viewportHeight: number;
27
+ /** Current scroll offset of the viewport, in pixels. */
28
+ scrollTop: number;
29
+ /**
30
+ * Extra rows rendered above and below the visible band, so a fast scroll
31
+ * does not expose blank space before the next frame. Default 4.
32
+ */
33
+ overscan?: number;
34
+ }
35
+ export interface RowWindow {
36
+ /** First rendered row index, inclusive. */
37
+ startIndex: number;
38
+ /** Last rendered row index, exclusive — safe for `slice(startIndex, endIndex)`. */
39
+ endIndex: number;
40
+ /** Spacer height above the rendered rows, in pixels. */
41
+ paddingTop: number;
42
+ /** Spacer height below the rendered rows, in pixels. */
43
+ paddingBottom: number;
44
+ /** Height of the whole collection, for the scroll range. */
45
+ totalHeight: number;
46
+ }
47
+ /**
48
+ * The rendered slice for a scroll position, plus the spacer heights that keep
49
+ * the scrollbar honest.
50
+ *
51
+ * Guarantees callers can rely on:
52
+ * - `paddingTop + (endIndex - startIndex) * rowHeight + paddingBottom` always
53
+ * equals `totalHeight`, so the scroll range never drifts as rows scroll by —
54
+ * a windowed list whose height wobbles makes the scrollbar jump under the
55
+ * pointer.
56
+ * - The window always covers the visible band; overscan only ever adds rows.
57
+ * - Degenerate geometry (no rows, unmeasured viewport, non-positive row height)
58
+ * yields an empty window rather than a guess. An unmeasured viewport is the
59
+ * normal state during first paint and under jsdom, and rendering *everything*
60
+ * there would defeat the purpose at exactly the moment it matters most.
61
+ */
62
+ export declare const resolveRowWindow: (metrics: WindowMetrics) => RowWindow;
63
+ /**
64
+ * Whether a scroll moved far enough to need a re-render.
65
+ *
66
+ * Scroll fires continuously; re-rendering on every event would rebuild the row
67
+ * markup dozens of times per second to show the same rows. Comparing the
68
+ * resolved window is cheaper and exact — the render is needed precisely when
69
+ * the slice changes.
70
+ */
71
+ export declare const sameRowWindow: (a: RowWindow, b: RowWindow) => boolean;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Row windowing for long lists.
3
+ *
4
+ * A ten-thousand-row audit log rendered whole is ten thousand DOM rows: the
5
+ * first paint stalls, every subsequent patch walks the lot, and the browser
6
+ * holds layout boxes nobody will ever scroll to. Windowing renders only the
7
+ * rows near the viewport and pads the rest with spacer height, so the scrollbar
8
+ * still describes the whole collection.
9
+ *
10
+ * The arithmetic lives here, pure, for the usual reason: jsdom has no layout,
11
+ * so a DOM-level test of a windowed surface can only ever observe zeroes. It is
12
+ * shared rather than per-component so `box-table`, `box-audit-log`, and
13
+ * `box-tree-grid` cannot drift into three different definitions of "near".
14
+ *
15
+ * Fixed row height only. Measured/variable heights are a different algorithm
16
+ * (a cumulative offset index that reflows as rows are measured); pretending one
17
+ * function covers both is how windowing engines end up subtly wrong at the
18
+ * boundaries.
19
+ */
20
+ const DEFAULT_OVERSCAN = 4;
21
+ /**
22
+ * The rendered slice for a scroll position, plus the spacer heights that keep
23
+ * the scrollbar honest.
24
+ *
25
+ * Guarantees callers can rely on:
26
+ * - `paddingTop + (endIndex - startIndex) * rowHeight + paddingBottom` always
27
+ * equals `totalHeight`, so the scroll range never drifts as rows scroll by —
28
+ * a windowed list whose height wobbles makes the scrollbar jump under the
29
+ * pointer.
30
+ * - The window always covers the visible band; overscan only ever adds rows.
31
+ * - Degenerate geometry (no rows, unmeasured viewport, non-positive row height)
32
+ * yields an empty window rather than a guess. An unmeasured viewport is the
33
+ * normal state during first paint and under jsdom, and rendering *everything*
34
+ * there would defeat the purpose at exactly the moment it matters most.
35
+ */
36
+ export const resolveRowWindow = (metrics) => {
37
+ const { totalRows, rowHeight, viewportHeight, scrollTop } = metrics;
38
+ const overscan = Math.max(0, Math.floor(metrics.overscan ?? DEFAULT_OVERSCAN));
39
+ if (totalRows <= 0 || rowHeight <= 0) {
40
+ return { startIndex: 0, endIndex: 0, paddingTop: 0, paddingBottom: 0, totalHeight: 0 };
41
+ }
42
+ const totalHeight = totalRows * rowHeight;
43
+ if (viewportHeight <= 0) {
44
+ return { startIndex: 0, endIndex: 0, paddingTop: 0, paddingBottom: totalHeight, totalHeight };
45
+ }
46
+ // Clamp the scroll offset: a bounced/rubber-banded scrollTop (negative, or
47
+ // past the end) must not produce a window outside the collection.
48
+ const maxScroll = Math.max(0, totalHeight - viewportHeight);
49
+ const offset = Math.min(Math.max(0, scrollTop), maxScroll);
50
+ const firstVisible = Math.floor(offset / rowHeight);
51
+ // ceil of the band, +1 for the row straddling the bottom edge.
52
+ const visibleCount = Math.ceil(viewportHeight / rowHeight) + 1;
53
+ const startIndex = Math.max(0, firstVisible - overscan);
54
+ const endIndex = Math.min(totalRows, firstVisible + visibleCount + overscan);
55
+ const paddingTop = startIndex * rowHeight;
56
+ const paddingBottom = totalHeight - endIndex * rowHeight;
57
+ return { startIndex, endIndex, paddingTop, paddingBottom, totalHeight };
58
+ };
59
+ /**
60
+ * Whether a scroll moved far enough to need a re-render.
61
+ *
62
+ * Scroll fires continuously; re-rendering on every event would rebuild the row
63
+ * markup dozens of times per second to show the same rows. Comparing the
64
+ * resolved window is cheaper and exact — the render is needed precisely when
65
+ * the slice changes.
66
+ */
67
+ export const sameRowWindow = (a, b) => a.startIndex === b.startIndex && a.endIndex === b.endIndex;
@@ -1,5 +1,6 @@
1
- import type { AuditEvent, AuditFacets, AuditGroupBy } from "./types.js";
1
+ import type { AuditEvent, AuditFacets, AuditGroup, AuditGroupBy } from "./types.js";
2
2
  import { BaseElement } from "../../core/index.js";
3
+ import type { RowWindow } from "../../core/index.js";
3
4
  /**
4
5
  * The aggregation, faceting, and export layer over the `box-timeline` event
5
6
  * contract (opportunity 3 of `plans/component-opportunities.md`): the same
@@ -7,8 +8,9 @@ import { BaseElement } from "../../core/index.js";
7
8
  * sections with counts, narrowed by facets and a date range, drillable to one
8
9
  * workflow run by correlation id, and exportable as CSV.
9
10
  *
10
- * Aggregation is client-side and pure. Server-side paging and row
11
- * virtualization for production-scale logs are tracked depth limitations.
11
+ * Aggregation is client-side and pure. `virtualize` windows the log for
12
+ * production-scale volumes; server-side paging remains a tracked depth
13
+ * limitation.
12
14
  */
13
15
  export declare class AuditLog extends BaseElement {
14
16
  static readonly tagName: string;
@@ -29,6 +31,15 @@ export declare class AuditLog extends BaseElement {
29
31
  private titleEl;
30
32
  /** Section keys the reader has collapsed. Absent means expanded. */
31
33
  private collapsedGroups;
34
+ /** The row window the last render used; null when it rendered everything. */
35
+ private lastWindow;
36
+ private scrollFrame;
37
+ private measuredHeights;
38
+ private measureFrame;
39
+ /** Row set the adoption budget below is counted against. */
40
+ private adoptedFor;
41
+ private adoptionCount;
42
+ private viewportObserver;
32
43
  private contentSignature;
33
44
  private optionsSignature;
34
45
  /** Raw `events` attribute the parse cache was built from. */
@@ -58,6 +69,14 @@ export declare class AuditLog extends BaseElement {
58
69
  set referenceTime(value: string);
59
70
  /** The events currently on screen: the full set narrowed by the facets. */
60
71
  get visibleEvents(): AuditEvent[];
72
+ /**
73
+ * The sections the current facets and grouping produce.
74
+ *
75
+ * Public because windowing takes away the alternative: a host used to be
76
+ * able to read the sections off the DOM, and once only a slice is rendered,
77
+ * that stops being the whole picture.
78
+ */
79
+ get groups(): AuditGroup[];
61
80
  toggleGroup(key: string, expanded?: boolean): void;
62
81
  expandAll(): void;
63
82
  collapseAll(): void;
@@ -68,8 +87,90 @@ export declare class AuditLog extends BaseElement {
68
87
  * archive API).
69
88
  */
70
89
  exportCsv(): string;
90
+ /**
91
+ * Watch the scroller so a measurable — or resized — viewport re-plans.
92
+ *
93
+ * Separate from `setupListeners` because that runs *once*, on the element's
94
+ * first connection, while `disconnectedCallback` tears the observer down
95
+ * every time. A log removed and re-inserted would otherwise come back with
96
+ * its listeners intact and its observer gone, so resizing its container
97
+ * would silently stop re-planning. Idempotent, so both callers are safe.
98
+ */
99
+ private observeViewport;
100
+ connectedCallback(): void;
101
+ disconnectedCallback(): void;
71
102
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
103
+ /**
104
+ * Render only the sections near the viewport. Opt-in for the same reason
105
+ * `box-table`'s is: a windowed surface needs a bounded height to scroll
106
+ * within, and silently windowing an unbounded one would render nothing.
107
+ *
108
+ * Unlike the table, this *can* window a grouped, variable-height surface —
109
+ * it plans over a cumulative offset index rather than a row count times a
110
+ * row height. Grouping is the whole point of an audit log, so refusing to
111
+ * window it would mean refusing the only shape it comes in.
112
+ */
113
+ get virtualize(): boolean;
114
+ set virtualize(value: boolean);
115
+ /**
116
+ * Starting height estimates, in pixels — not contracts. The real heights
117
+ * depend on density, font, and how much an event's summary wraps; after the
118
+ * first paint the element measures one of each and uses that instead.
119
+ */
120
+ get rowHeight(): number;
121
+ get headingHeight(): number;
122
+ set rowHeight(value: number);
123
+ set headingHeight(value: number);
124
+ private declaredHeight;
125
+ /** The window the last render used, or null when it rendered every section. */
126
+ get renderedWindow(): RowWindow | null;
127
+ /**
128
+ * Adopt the real rendered heights, once, when they differ from the ones the
129
+ * last plan used. Re-planning with the measured heights settles immediately —
130
+ * the next measurement matches — so this cannot loop.
131
+ */
132
+ /** Mean rendered height of the elements matching a part, or 0 if none. */
133
+ private meanHeight;
134
+ /**
135
+ * Adopt the real rendered heights when they differ from the ones in use.
136
+ *
137
+ * Two things here exist because the obvious version ran away. Sampling *one*
138
+ * event row and adopting its height looks fine and is not: event rows are not
139
+ * uniform — `summary`, `evidence` and `correlationId` are each optional — so
140
+ * adopting the first rendered row's height moves the window, which renders a
141
+ * different first row, which measures differently, which moves the window.
142
+ * Measured in Chromium with every third event several lines taller: **55 full
143
+ * rebuilds in 0.9 seconds while nothing was scrolling**, the sampled height
144
+ * flipping between 33px and 166.5px.
145
+ *
146
+ * So: average across every rendered row rather than sampling one, which makes
147
+ * the estimate both more representative and far less sensitive to which rows
148
+ * happen to be on screen. And bound the adoptions per row set anyway, because
149
+ * "the average is stable" is an argument about typical data, and this is the
150
+ * code path where being wrong costs a frame-rate collapse rather than a
151
+ * slightly wrong scrollbar.
152
+ */
153
+ private measureRowHeights;
154
+ /**
155
+ * What counts as "the same row set" for the adoption budget: change the data,
156
+ * the grouping, the filters or the collapse state and the rows genuinely
157
+ * differ, so measuring again is warranted.
158
+ */
159
+ private heightSignature;
160
+ private currentPlan;
72
161
  private now;
162
+ /**
163
+ * Grouping, flattening and indexing for the current data, computed once and
164
+ * reused until something that affects them changes.
165
+ *
166
+ * The scroll path asks "did the window move?" on every frame that is not
167
+ * skipped, and answering it used to re-parse the whole `events` attribute,
168
+ * re-filter, re-group, re-flatten and rebuild the offset array — then
169
+ * `update()` did all of it again. That is O(n) twice per frame on a surface
170
+ * whose entire purpose is not being O(n) at scale.
171
+ */
172
+ private rowCache;
173
+ private cachedRows;
73
174
  private currentGroups;
74
175
  private patchFacets;
75
176
  private eventHtml;