@toolbox-web/grid 3.9.2 → 3.10.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.
- package/all.js +1 -1
- package/all.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/types.d.ts +6 -3
- package/lib/features/clipboard.js.map +1 -1
- package/lib/features/column-virtualization.js.map +1 -1
- package/lib/features/context-menu.js.map +1 -1
- package/lib/features/editing.js.map +1 -1
- package/lib/features/export.js.map +1 -1
- package/lib/features/filtering.js.map +1 -1
- package/lib/features/grouping-columns.js.map +1 -1
- package/lib/features/grouping-rows.js.map +1 -1
- package/lib/features/master-detail.js.map +1 -1
- package/lib/features/multi-sort.js.map +1 -1
- package/lib/features/pinned-columns.js.map +1 -1
- package/lib/features/pinned-rows.js.map +1 -1
- package/lib/features/pivot.js.map +1 -1
- package/lib/features/print.js.map +1 -1
- package/lib/features/registry.js +1 -1
- package/lib/features/registry.js.map +1 -1
- package/lib/features/reorder-columns.js.map +1 -1
- package/lib/features/responsive.js.map +1 -1
- package/lib/features/row-drag-drop.js.map +1 -1
- package/lib/features/selection.js.map +1 -1
- package/lib/features/server-side.js.map +1 -1
- package/lib/features/shell.js.map +1 -1
- package/lib/features/sticky-rows.js.map +1 -1
- package/lib/features/tooltip.js.map +1 -1
- package/lib/features/tree.js.map +1 -1
- package/lib/features/undo-redo.js.map +1 -1
- package/lib/features/visibility.js.map +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/editors.d.ts +15 -0
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/editing/types.d.ts +5 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/filter-model.d.ts +20 -1
- package/lib/plugins/filtering/filter-panel-default.d.ts +15 -1
- package/lib/plugins/filtering/index.d.ts +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/filtering/types.d.ts +58 -3
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/index.js +1 -1
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/hierarchy/index.js.map +1 -1
- package/lib/plugins/master-detail/index.js +1 -1
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js.map +1 -1
- package/lib/plugins/pivot/index.js +1 -1
- package/lib/plugins/pivot/index.js.map +1 -1
- package/lib/plugins/pivot/pivot-plugin.d.ts +2 -0
- package/lib/plugins/pivot/types.d.ts +2 -0
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/responsive/index.js.map +1 -1
- package/lib/plugins/row-drag-drop/index.js.map +1 -1
- package/lib/plugins/selection/index.js +1 -1
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/shared/form-field-target.d.ts +10 -0
- package/lib/plugins/shared/format-value.d.ts +12 -0
- package/lib/plugins/shell/index.js.map +1 -1
- package/lib/plugins/sticky-rows/index.js.map +1 -1
- package/lib/plugins/tooltip/index.js.map +1 -1
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/visibility/index.js.map +1 -1
- package/package.json +1 -1
- package/umd/grid.all.umd.js +1 -1
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +1 -1
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/clipboard.umd.js.map +1 -1
- package/umd/plugins/column-virtualization.umd.js.map +1 -1
- package/umd/plugins/context-menu.umd.js.map +1 -1
- package/umd/plugins/editing.umd.js +1 -1
- package/umd/plugins/editing.umd.js.map +1 -1
- package/umd/plugins/export.umd.js +1 -1
- package/umd/plugins/export.umd.js.map +1 -1
- package/umd/plugins/filtering.umd.js +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/grouping-columns.umd.js.map +1 -1
- package/umd/plugins/grouping-rows.umd.js +1 -1
- package/umd/plugins/grouping-rows.umd.js.map +1 -1
- package/umd/plugins/hierarchy.umd.js.map +1 -1
- package/umd/plugins/master-detail.umd.js +1 -1
- package/umd/plugins/master-detail.umd.js.map +1 -1
- package/umd/plugins/multi-sort.umd.js.map +1 -1
- package/umd/plugins/pinned-columns.umd.js.map +1 -1
- package/umd/plugins/pinned-rows.umd.js.map +1 -1
- package/umd/plugins/pivot.umd.js +1 -1
- package/umd/plugins/pivot.umd.js.map +1 -1
- package/umd/plugins/print.umd.js.map +1 -1
- package/umd/plugins/reorder-columns.umd.js.map +1 -1
- package/umd/plugins/responsive.umd.js.map +1 -1
- package/umd/plugins/row-drag-drop.umd.js.map +1 -1
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/server-side.umd.js.map +1 -1
- package/umd/plugins/shell.umd.js.map +1 -1
- package/umd/plugins/sticky-rows.umd.js.map +1 -1
- package/umd/plugins/tooltip.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
- package/umd/plugins/undo-redo.umd.js.map +1 -1
- package/umd/plugins/visibility.umd.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sticky-rows.umd.js","names":[],"sources":["../../../../../libs/grid/src/lib/plugins/sticky-rows/sticky-rows.css?inline","../../../../../libs/grid/src/lib/plugins/sticky-rows/sticky-rows-plugin.ts"],"sourcesContent":["@layer tbw-plugins {\n /* Container for stuck-row clones. OVERLAYS the top of the rows-viewport\n * (not a flex child of rows-body) so it does not push the live rows\n * down. The rows-viewport's `overflow: clip` hides the live row\n * underneath the overlay. See StickyRowsPlugin.ensureContainer().\n *\n * Width matches `min-width: fit-content` so clones inherit the\n * grid-template-columns and scroll horizontally with the body. */\n .tbw-sticky-rows {\n position: absolute;\n top: 0;\n left: 0;\n display: block;\n z-index: var(--tbw-z-layer-sticky-rows, 22);\n background: var(--tbw-color-bg, var(--tbw-color-panel-bg));\n min-width: 100%;\n width: fit-content;\n /* Allow translateY on push-mode clones to render above the wrapper edge. */\n overflow: visible;\n }\n\n .tbw-sticky-rows:empty {\n display: none;\n }\n\n /* The cloned row. We reuse `.data-grid-row` class so existing row CSS applies\n * without duplication. Only the sticky-specific overrides live here. */\n .tbw-sticky-rows .tbw-sticky-row {\n /* Shadow under the bottom edge to separate from scrolling rows below. */\n box-shadow: 0 1px 0 var(--tbw-color-border);\n /* Clones never participate in row striping or focus highlight; force a\n * solid background so they read as a distinct, opaque layer. */\n background: var(--tbw-color-bg, var(--tbw-color-panel-bg));\n }\n\n /* Push-mode: the bottom-most stuck clone may translate up when displaced\n * by the next incoming sticky row. */\n .tbw-sticky-rows[data-mode='push'] .tbw-sticky-row {\n will-change: transform;\n }\n}\n","/**\n * Sticky Rows Plugin (Class-based)\n *\n * Pins selected data rows below the header as the user scrolls past them.\n * See `types.ts` for configuration.\n *\n * @module Plugins/Sticky Rows\n */\n\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport type { RowPosition } from '../../core/internal/virtualization';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { ScrollEvent } from '../../core/plugin/types';\nimport type { ColumnConfig } from '../../core/types';\nimport styles from './sticky-rows.css?inline';\nimport type { StickyPredicate, StickyRowsConfig } from './types';\n\n/** Internal shape of the virtualization state we read from the grid. */\ninterface VirtualStateLike {\n start?: number;\n end?: number;\n rowHeight?: number;\n positionCache?: RowPosition[] | null;\n /** The faux-scroll element — the source of truth for the live scroll\n * position (per-pixel, unlike `start` which only updates when the\n * rendered window shifts). */\n container?: HTMLElement | null;\n}\n\n/**\n * Sticky Rows plugin for `<tbw-grid>`.\n *\n * Marks specific rows as \"sticky\" so they pin below the grid header when their\n * natural scroll position would take them off-screen. Behavior when multiple\n * sticky rows would be stuck simultaneously is controlled by `mode`:\n *\n * - `'push'` (default) — only one stuck at a time; the next sticky row pushes\n * the previous one out of view (iOS section-header behavior).\n * - `'stack'` — sticky rows stack below the header up to `maxStacked`.\n *\n * The plugin renders **clones** of the real rows; the originals continue to\n * exist in the data flow. Clones inherit the row's grid-template alignment so\n * they line up perfectly with the column boundaries below.\n *\n * @example\n * ```ts\n * import '@toolbox-web/grid/features/sticky-rows';\n *\n * grid.gridConfig = {\n * features: { stickyRows: { isSticky: 'isSection', mode: 'stack' } },\n * };\n * ```\n *\n * @see {@link StickyRowsConfig} for all configuration options.\n *\n * @internal Extends BaseGridPlugin.\n * @since 2.7.0\n */\nexport class StickyRowsPlugin extends BaseGridPlugin<StickyRowsConfig> {\n /** @internal */\n readonly name = 'stickyRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<StickyRowsConfig> {\n return {\n mode: 'push',\n maxStacked: Infinity,\n };\n }\n\n // #region Internal State\n /** Container element that holds the stuck-row clones. */\n private container: HTMLElement | null = null;\n\n /** Indices in `grid.rows` whose rows are sticky, sorted ascending. */\n private stickyIndices: number[] = [];\n\n /** Cached clone DOM keyed by row index. Survives the row leaving the\n * virtualization window so we can keep showing it while stuck. */\n private cloneCache: Map<number, HTMLElement> = new Map();\n\n /** Currently displayed indices (in DOM order). Used to skip work when the\n * set hasn't changed between scroll ticks. */\n private displayedIndices: number[] = [];\n\n /** Detached copy of the most recent clone captured from a LIVE row, used as\n * the structural template for {@link synthesizeClone}. Real captures only —\n * never a synthesized stand-in. */\n private templateClone: HTMLElement | null = null;\n\n /** Last applied push-mode translation, to skip writes on no-op ticks. */\n private lastPushOffset = 0;\n\n /** True while a deferred \"settle\" style pass is already queued. Coalesces\n * multiple scroll ticks into a single re-capture. See\n * {@link maybeScheduleSettlePass}. */\n private settlePassScheduled = false;\n // #endregion\n\n // #region Lifecycle\n /** @internal */\n override detach(): void {\n this.container?.remove();\n this.container = null;\n this.cloneCache.clear();\n this.templateClone = null;\n this.stickyIndices = [];\n this.displayedIndices = [];\n this.lastPushOffset = 0;\n this.settlePassScheduled = false;\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal Recompute the sticky-index list and refresh display. */\n override afterRender(): void {\n // A style pass is now running — possibly the deferred settle pass we\n // requested. Clear the flag so a later scroll tick can queue a fresh one.\n this.settlePassScheduled = false;\n this.recomputeStickyIndices();\n this.ensureContainer();\n // Pre-cache clones for any sticky row currently in the rendered window\n // BEFORE it scrolls out — otherwise findRenderedRow() returns null at\n // display time and we'd render an empty container.\n this.primeCloneCache();\n // Re-capture the clones that are CURRENTLY displayed from the (now\n // settled) live DOM. WHY: `primeCloneCache` refreshes the cache, but\n // `refreshDisplay` only swaps the container's child nodes when the\n // displayed *set* changes. On the deferred settle pass after a small\n // scroll-up — where the sticky set is unchanged (e.g. row 0 stays the\n // single pinned row) — the stale clone captured during `onScrollRender`\n // would otherwise remain in the DOM forever, since `sameSet` short-\n // circuits the rebuild. Reconciling displayed clones here replaces the\n // stale node with the freshly-settled content. Symptom it fixes: scroll\n // down past the sticky row, then up a few px → a non-sticky row's content\n // is pinned until you scroll all the way to the absolute top.\n this.refreshClonesInWindow();\n this.refreshDisplay();\n }\n\n /** @internal Re-render clones after a scroll-driven row pool refresh. */\n override onScrollRender(): void {\n // Pre-cache clones for any sticky row currently in the rendered window so\n // we have one ready when it scrolls past. Do NOT re-capture the DISPLAYED\n // clones synchronously here. WHY: when a sticky row is recycled back into\n // the window on scroll-up, the grid sets its cell `data-row` synchronously\n // but async framework adapters (React/Vue) flush the cell content a\n // microtask LATER — so a synchronous re-capture would paint the PREVIOUS\n // row's content for one frame before the deferred settle pass corrects it\n // (visible flash on the sticky row at the scroll boundary). Re-capturing\n // displayed clones is therefore deferred entirely to the settle pass\n // (`afterRender` → `refreshClonesInWindow`), which runs after the\n // microtask flush. A settle pass is always scheduled below whenever a\n // sticky row is in the window — exactly the case this method could refresh\n // — so no displayed-clone update is lost, only delayed one frame.\n this.primeCloneCache();\n this.refreshDisplay();\n this.maybeScheduleSettlePass();\n }\n\n /** @internal Update which clones show / how they're positioned. */\n override onScroll(event: ScrollEvent): void {\n this.refreshDisplay(event.scrollTop);\n }\n\n /**\n * @internal Report how much of the rows viewport the clone overlay covers so\n * keyboard navigation can scroll a focused row clear of it (WCAG 2.2 SC\n * 2.4.11 Focus Not Obscured). The container is `position: absolute; top: 0`\n * over `.rows-viewport`, so a row scrolled to the exact top would otherwise\n * land underneath it.\n */\n override getVerticalScrollOffsets(\n focusedRowIndex?: number,\n ): { top: number; bottom: number; skipScroll?: boolean } | undefined {\n if (!this.container || this.displayedIndices.length === 0) return undefined;\n // The row IS one of the stuck clones — it is already pinned in view, and\n // scrolling to it would only push the row it is stuck above out of reach.\n if (focusedRowIndex != null && this.displayedIndices.includes(focusedRowIndex)) {\n return { top: 0, bottom: 0, skipScroll: true };\n }\n return { top: this.container.offsetHeight, bottom: 0 };\n }\n\n /**\n * Queue a single deferred style pass that re-captures sticky clones once\n * async framework cell content has settled.\n *\n * WHY: framework adapters (React/Vue) flush custom cell content on a\n * microtask (`PortalManager` batches via `queueMicrotask` + `flushSync`)\n * AFTER this synchronous scroll hook. When a sticky row is recycled back\n * into the rendered window on scroll-up, the grid sets the cell\n * `data-row` attribute synchronously but the cell's rendered content still\n * shows the PREVIOUS row. A clone captured during the hook therefore holds\n * stale content, and because clones are cached by index it stays wrong\n * forever (symptom: scrolling down, back up, then down again pins a\n * different row than the first time). Re-running `afterRender` on the next\n * style pass — after the microtask flush — re-captures the now-settled\n * clone. Only schedule when a sticky row is actually in the rendered window\n * (the only case where a stale capture could have happened) to avoid\n * needless style passes during ordinary scrolling.\n */\n private maybeScheduleSettlePass(): void {\n if (this.settlePassScheduled) return;\n if (!this.stickyIndices.some((idx) => this.findRenderedRow(idx) != null)) return;\n this.settlePassScheduled = true;\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Private — predicate + indices\n\n private resolvePredicate(): StickyPredicate {\n const cfg = this.config.isSticky;\n if (typeof cfg === 'function') return cfg;\n if (typeof cfg === 'string') {\n const field = cfg;\n return (row) => {\n if (row == null || typeof row !== 'object') return false;\n return Boolean((row as Record<string, unknown>)[field]);\n };\n }\n return () => false;\n }\n\n private recomputeStickyIndices(): void {\n const rows = this.rows;\n const predicate = this.resolvePredicate();\n const next: number[] = [];\n for (let i = 0; i < rows.length; i++) {\n if (predicate(rows[i], i)) next.push(i);\n }\n // Drop cache entries whose indices are no longer sticky (e.g. data\n // shuffled, predicate flipped). Indices that remain may still point at a\n // different row after re-sort — drop those too so we re-clone from the\n // fresh DOM on the next render.\n if (next.length !== this.stickyIndices.length || next.some((v, i) => v !== this.stickyIndices[i])) {\n this.cloneCache.clear();\n }\n this.stickyIndices = next;\n }\n\n // #endregion\n\n // #region Private — DOM container\n\n private ensureContainer(): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Container is an OVERLAY positioned at the top of the rows-viewport\n // (not a flex child of rows-body). Why: a flow-positioned container\n // takes vertical space, which pushes the rows-viewport down by the\n // sticky height. The rows are positioned in scroll coordinates that\n // are agnostic of that shift, so live-row geometry ends up partially\n // overlapping the sticky container — producing duplicate visible rows\n // during the push transition (#279 follow-up). Overlaying on top of\n // the row pool, with `overflow: clip` on rows-viewport hiding the\n // live row underneath, eliminates the overlap entirely.\n const rowsViewport = grid.querySelector('.rows-viewport');\n if (!rowsViewport) {\n // Grid hasn't built its DOM yet; bail and try again on next afterRender.\n this.container = null;\n return;\n }\n\n if (this.container && rowsViewport.contains(this.container)) return;\n\n // Container was either never created or got wiped out. Re-create.\n this.container = document.createElement('div');\n this.container.className = 'tbw-sticky-rows';\n this.container.setAttribute('role', 'presentation');\n this.container.dataset['mode'] = this.config.mode ?? 'push';\n if (this.config.className) this.container.classList.add(this.config.className);\n\n // Insert as the FIRST child of rows-viewport so it overlays on top of\n // the row pool. Z-index in CSS keeps it above `.rows`.\n rowsViewport.insertBefore(this.container, rowsViewport.firstChild);\n }\n\n // #endregion\n\n // #region Private — offsets + which-to-display\n\n /** Pixel offset of the top of `index` from the start of the scroll area. */\n private offsetOf(index: number): number {\n const v = this.getVirtualState();\n const cache = v?.positionCache;\n if (cache && cache[index]) return cache[index].offset;\n const h = v?.rowHeight ?? 28;\n return index * h;\n }\n\n /** Estimated height of `index` (used for push-mode displacement). */\n private heightOf(index: number): number {\n const v = this.getVirtualState();\n const cache = v?.positionCache;\n if (cache && cache[index]) return cache[index].height;\n return v?.rowHeight ?? 28;\n }\n\n private getVirtualState(): VirtualStateLike | undefined {\n return this.grid?._virtualization;\n }\n\n private getCurrentScrollTop(): number {\n const v = this.getVirtualState();\n // PRIMARY source: the live `scrollTop` of the faux-scroll element. This\n // updates per-pixel as the user drags, unlike `_virtualization.start`\n // which only ticks over when the rendered window shifts (every\n // `rowHeight` px). Reading the stale start-derived value caused\n // `afterRender` to clobber the correct displayed-set that `onScroll`\n // had just computed (#279 follow-up).\n const liveScrollTop = v?.container?.scrollTop;\n if (typeof liveScrollTop === 'number') return liveScrollTop;\n // Fallbacks (unit tests / pre-mount): derive from window-start.\n if (\n typeof v?.start === 'number' &&\n typeof v?.rowHeight === 'number' &&\n (!v.positionCache || !v.positionCache.length)\n ) {\n return v.start * v.rowHeight;\n }\n if (v?.positionCache && typeof v.start === 'number' && v.positionCache[v.start]) {\n return v.positionCache[v.start].offset;\n }\n return 0;\n }\n\n /**\n * Compute the displayed sticky indices and any upward push offset, given\n * the current scroll offset.\n *\n * - `'push'` mode: a row qualifies once its top has scrolled past the\n * viewport top (strict `<`); only the highest qualifying index is\n * shown. As the next sticky approaches from below, the stuck clone\n * slides upward by `heightOfStuck - distance` (iOS section-header\n * behavior).\n * - `'stack'` mode: each subsequent sticky qualifies once its top\n * reaches the bottom of the existing stack (`offsetOf(idx) < scrollTop\n * + cumulativeHeightOfStuck`), so `— B —` latches when it meets\n * `— A —`'s bottom — not after `— A —` covers it. When the stack is\n * at `maxStacked` and the **live** next sticky row's top crosses the\n * bottom of the stack (`distance <= 0`), the entire stack translates\n * upward by `-distance`. Live rows above the stack (including the\n * incoming sticky) scroll up naturally beneath the overlay; rows-\n * viewport's `overflow: clip` hides anything above the top edge. When\n * the oldest is fully off (pushOffset ≥ heightOf(oldest)), the next\n * tick qualifies the new sticky and we slice to `last max` with\n * transform reset — at that exact pixel the live new sticky is sitting\n * precisely behind its slot in the new stack, producing a seamless\n * swap with no duplicate row visible.\n *\n * Strict `<` keeps the qualification edge symmetric with push mode and\n * avoids briefly rendering both a clone and the live row at the swap\n * pixel.\n */\n private computeDisplay(scrollTop: number): { indices: number[]; pushOffset: number } {\n if (this.config.mode === 'stack') {\n const max = this.config.maxStacked ?? Infinity;\n const qualifying: number[] = [];\n let stuckHeight = 0;\n for (const idx of this.stickyIndices) {\n if (qualifying.length < max) {\n // Within the cap: a sticky qualifies once its top reaches the\n // bottom of the existing stack.\n if (this.offsetOf(idx) < scrollTop + stuckHeight) {\n qualifying.push(idx);\n stuckHeight += this.heightOf(idx);\n } else {\n break; // sorted ascending — anything after won't qualify either\n }\n } else {\n // At cap: evict-and-promote only after `idx` has fully crossed\n // the slot of the current oldest. WHY: while `idx` is mid-cross\n // (its top is past the stack bottom but the oldest isn't yet\n // off the top), we want to ANIMATE that transition by sliding\n // the existing stack upward — not snap to the post-evict state.\n // Without this clamp, qualification fires the instant the new\n // sticky crosses the stack bottom (same scrollTop where the\n // anticipation block would otherwise activate), and the\n // anticipation never gets a chance to render.\n const oldestH = this.heightOf(qualifying[0]);\n if (this.offsetOf(idx) < scrollTop + stuckHeight - oldestH) {\n qualifying.shift();\n stuckHeight -= oldestH;\n qualifying.push(idx);\n stuckHeight += this.heightOf(idx);\n } else {\n break;\n }\n }\n }\n\n // Anticipation: at-cap and the live next sticky has crossed the\n // stack bottom but not yet fully past the oldest's slot. Translate\n // the entire stack up by `-distance` so the user sees the live\n // next-sticky scrolling up against the bottom of the stack while\n // the oldest slides off the top — exactly mirroring iOS Contacts.\n // Do NOT add the new sticky to the displayed list during this\n // window: it's still in the row pool and visible there. Adding a\n // clone too would produce a visible duplicate AND would cover the\n // last regular row under the previous section while live-next is\n // still approaching.\n if (qualifying.length === max && qualifying.length > 0) {\n const next = this.findNextSticky(qualifying[qualifying.length - 1]);\n if (next != null) {\n const distance = this.offsetOf(next) - (scrollTop + stuckHeight);\n if (distance < 0) {\n const oldestH = this.heightOf(qualifying[0]);\n return { indices: qualifying, pushOffset: Math.min(-distance, oldestH) };\n }\n }\n }\n return { indices: qualifying, pushOffset: 0 };\n }\n\n // 'push' mode.\n const qualifying: number[] = [];\n for (const idx of this.stickyIndices) {\n if (this.offsetOf(idx) < scrollTop) qualifying.push(idx);\n else break;\n }\n if (qualifying.length === 0) return { indices: qualifying, pushOffset: 0 };\n const stuck = qualifying[qualifying.length - 1];\n const next = this.findNextSticky(stuck);\n let pushOffset = 0;\n if (next != null) {\n const stuckH = this.heightOf(stuck);\n const distance = this.offsetOf(next) - scrollTop;\n if (distance < stuckH) {\n pushOffset = stuckH - Math.max(0, distance);\n }\n }\n return { indices: [stuck], pushOffset };\n }\n\n // #endregion\n\n // #region Private — clone management\n\n /** Locate a rendered row in the viewport by its data index. */\n private findRenderedRow(index: number): HTMLElement | null {\n const grid = this.gridElement;\n if (!grid) return null;\n // Cells carry `data-row=\"<index>\"`. The row element is the parent.\n const cell = grid.querySelector(`.rows .data-grid-row .cell[data-row=\"${index}\"]`);\n return (cell?.parentElement as HTMLElement | null) ?? null;\n }\n\n /** Build (or refresh) a clone for `index`, using the live row when present. */\n private buildClone(index: number): HTMLElement | null {\n const live = this.findRenderedRow(index);\n if (!live) return this.cloneCache.get(index) ?? null;\n const clone = live.cloneNode(true) as HTMLElement;\n clone.classList.add('tbw-sticky-row');\n clone.removeAttribute('aria-rowindex');\n // Stuck clones are decorative — the original row is still present in the\n // accessibility tree below. Hide clones from AT to avoid double-reads.\n clone.setAttribute('aria-hidden', 'true');\n clone.dataset['stickyRow'] = String(index);\n // Drop any focus styling from the source — clones are non-interactive.\n clone.classList.remove('row-focus', 'cell-focus');\n clone.querySelectorAll('.cell-focus, .row-focus').forEach((el) => el.classList.remove('cell-focus', 'row-focus'));\n // Strip tabindex on cells so the clone isn't a tab target.\n clone.removeAttribute('tabindex');\n clone.querySelectorAll('[tabindex]').forEach((el) => el.removeAttribute('tabindex'));\n\n // Keep a detached copy as the structural template for synthesized\n // stand-ins. Captured post-sanitisation so a synthetic inherits the same\n // classes / aria treatment.\n this.templateClone = clone.cloneNode(true) as HTMLElement;\n\n this.cloneCache.set(index, clone);\n return clone;\n }\n\n /**\n * Build a data-derived stand-in for a sticky row that has never been\n * rendered in this session.\n *\n * WHY: clones are DOM captures, so a sticky row only becomes displayable\n * after it has passed through the virtualization window. Two situations\n * break that assumption, and in BOTH of them scrolling further down never\n * brings the row back — the stack silently under-renders forever:\n *\n * 1. A config or data change wipes the cache (`detach` / index change in\n * `recomputeStickyIndices`) while the grid is scrolled deep. Only the\n * sticky rows still inside the window get re-primed.\n * 2. The grid mounts (or restores a scroll position) already scrolled past\n * several sticky rows.\n *\n * Symptom: `'stack'` mode shows one pinned row instead of `maxStacked`.\n *\n * The stand-in reuses the last real capture for structure — so column\n * widths, classes and alignment are exact — and rewrites the cell text from\n * the row data. Returns `null` (caller omits the row, the pre-existing\n * behaviour) when there is no template yet, or when any cell holds element\n * children: those come from a custom cell renderer and re-labelling them\n * would pin ANOTHER row's rendered content, which is worse than a gap.\n */\n private synthesizeClone(index: number): HTMLElement | null {\n const template = this.templateClone;\n const row = this.rows[index];\n if (!template || row == null) return null;\n\n const el = template.cloneNode(true) as HTMLElement;\n const cells = Array.from(el.querySelectorAll<HTMLElement>('.cell'));\n if (cells.some((cell) => cell.firstElementChild != null)) return null;\n\n el.dataset['stickyRow'] = String(index);\n // Marks the row as data-derived rather than DOM-captured. `refreshClones-\n // InWindow` upgrades it to a real capture as soon as the row renders.\n el.dataset['syntheticStickyRow'] = '';\n el.removeAttribute('aria-rowindex');\n\n const columns = this.visibleColumns;\n for (const cell of cells) {\n cell.setAttribute('data-row', String(index));\n const field = cell.getAttribute('data-field');\n const column: ColumnConfig | undefined = field ? columns.find((c) => c.field === field) : undefined;\n const value = column ? resolveCellValue(row, column, index) : null;\n cell.textContent = value == null ? '' : String(value);\n }\n return el;\n }\n\n /** Refresh cached clones for any displayed indices that are now in-window.\n * Pulls fresh DOM so edits / re-renders are reflected when the row swings\n * back into view. */\n private refreshClonesInWindow(): void {\n if (!this.displayedIndices.length) return;\n for (const idx of this.displayedIndices) {\n const live = this.findRenderedRow(idx);\n if (live) {\n // Re-clone to pick up any post-render changes.\n const fresh = this.buildClone(idx);\n const existingClone = this.container?.querySelector(`[data-sticky-row=\"${idx}\"]`);\n if (fresh && existingClone && existingClone !== fresh) {\n existingClone.replaceWith(fresh);\n }\n }\n }\n }\n\n /** Build clones for any sticky row currently rendered in the viewport so we\n * have one ready when the row scrolls past. Without this, a sticky row\n * that's never been displayed-as-stuck won't have a cache entry, and once\n * the live row leaves the rendered window we have nothing to render. */\n private primeCloneCache(): void {\n if (!this.stickyIndices.length) return;\n for (const idx of this.stickyIndices) {\n if (this.findRenderedRow(idx)) {\n // buildClone() handles the live → cache path itself.\n this.buildClone(idx);\n }\n }\n }\n\n /** Reconcile the container DOM with the desired displayed indices. */\n private refreshDisplay(scrollTopOverride?: number): void {\n if (!this.container) return;\n\n const scrollTop = scrollTopOverride ?? this.getCurrentScrollTop();\n const { indices: desired, pushOffset } = this.computeDisplay(scrollTop);\n\n // Update the container's mode marker (in case config changed).\n if (this.container.dataset['mode'] !== this.config.mode) {\n this.container.dataset['mode'] = this.config.mode ?? 'push';\n }\n\n const sameSet =\n desired.length === this.displayedIndices.length && desired.every((v, i) => v === this.displayedIndices[i]);\n\n if (!sameSet) {\n // Rebuild the container's children. cloneCache survives between calls so\n // we re-use existing clones for indices that remain displayed.\n const fragment = document.createDocumentFragment();\n const appended: number[] = [];\n for (const idx of desired) {\n let clone = this.cloneCache.get(idx) ?? null;\n // Always try to refresh from live DOM if available.\n const fresh = this.buildClone(idx);\n clone = fresh ?? clone;\n if (!clone) {\n // Never captured — fall back to a data-derived stand-in, otherwise\n // this index is dropped from the stack permanently (see\n // `synthesizeClone`). Cached so the next tick reuses it; a real\n // capture overwrites it the moment the row renders.\n clone = this.synthesizeClone(idx);\n if (clone) this.cloneCache.set(idx, clone);\n }\n if (clone) {\n fragment.appendChild(clone);\n appended.push(idx);\n }\n }\n this.container.replaceChildren(fragment);\n // Track only the indices we actually appended so a subsequent refresh\n // (after a missing index gets primed via onScrollRender) will detect a\n // diff and retry rather than incorrectly believing the set is settled.\n this.displayedIndices = appended;\n }\n\n // Apply the upward translation to the entire container so push-mode\n // (1 child) and stack-mode anticipation (max+1 children) both slide\n // uniformly. CSS `overflow: clip` on `.rows-viewport` hides anything\n // translated above its top edge.\n if (pushOffset !== this.lastPushOffset) {\n this.container.style.transform = pushOffset > 0 ? `translateY(${-pushOffset}px)` : '';\n this.lastPushOffset = pushOffset;\n }\n }\n\n /** Find the next sticky index strictly greater than `current`. */\n private findNextSticky(current: number): number | null {\n for (const idx of this.stickyIndices) {\n if (idx > current) return idx;\n }\n return null;\n }\n\n // #endregion\n}\n"],"mappings":"gXC0Da,EAAb,cAAsC,EAAA,eAEpC,KAAgB,aAEhB,ieAGA,iBAAuB,GACrB,MAAO,CACL,KAAM,OACN,WAAY,IAEhB,CAIA,UAAwC,KAGxC,cAAkC,GAIlC,WAA+C,IAAI,IAInD,iBAAqC,GAKrC,cAA4C,KAG5C,eAAyB,EAKzB,qBAA8B,EAK9B,MAAA,GACE,KAAK,WAAW,SAChB,KAAK,UAAY,KACjB,KAAK,WAAW,QAChB,KAAK,cAAgB,KACrB,KAAK,cAAgB,GACrB,KAAK,iBAAmB,GACxB,KAAK,eAAiB,EACtB,KAAK,qBAAsB,CAC7B,CAMA,WAAA,GAGE,KAAK,qBAAsB,EAC3B,KAAK,yBACL,KAAK,kBAIL,KAAK,kBAYL,KAAK,wBACL,KAAK,gBACP,CAGA,cAAA,GAcE,KAAK,kBACL,KAAK,iBACL,KAAK,yBACP,CAGA,QAAA,CAAkB,GAChB,KAAK,eAAe,EAAM,UAC5B,CASA,wBAAA,CACE,GAEA,GAAK,KAAK,WAA8C,IAAjC,KAAK,iBAAiB,OAG7C,OAAuB,MAAnB,GAA2B,KAAK,iBAAiB,SAAS,GACrD,CAAE,IAAK,EAAG,OAAQ,EAAG,YAAY,GAEnC,CAAE,IAAK,KAAK,UAAU,aAAc,OAAQ,EACrD,CAoBA,uBAAA,GACM,KAAK,qBACJ,KAAK,cAAc,KAAM,GAAqC,MAA7B,KAAK,gBAAgB,MAC3D,KAAK,qBAAsB,EAC3B,KAAK,qBACP,CAMA,gBAAA,GACE,MAAM,EAAM,KAAK,OAAO,SACxB,GAAmB,mBAAR,EAAoB,OAAO,EACtC,GAAmB,iBAAR,EAAkB,CAC3B,MAAM,EAAQ,EACd,OAAQ,GACK,MAAP,GAA8B,iBAAR,GACnB,QAAS,EAAgC,GAEpD,CACA,MAAA,KAAa,CACf,CAEA,sBAAA,GACE,MAAM,EAAO,KAAK,KACZ,EAAY,KAAK,mBACjB,EAAiB,GACvB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAQ,IAC3B,EAAU,EAAK,GAAI,IAAI,EAAK,KAAK,IAMnC,EAAK,SAAW,KAAK,cAAc,QAAU,EAAK,KAAA,CAAM,EAAG,IAAM,IAAM,KAAK,cAAc,MAC5F,KAAK,WAAW,QAElB,KAAK,cAAgB,CACvB,CAMA,eAAA,GACE,MAAM,EAAO,KAAK,YAClB,IAAK,EAAM,OAWX,MAAM,EAAe,EAAK,cAAc,kBACnC,EAMD,KAAK,WAAa,EAAa,SAAS,KAAK,aAGjD,KAAK,UAAY,SAAS,cAAc,OACxC,KAAK,UAAU,UAAY,kBAC3B,KAAK,UAAU,aAAa,OAAQ,gBACpC,KAAK,UAAU,QAAQ,KAAU,KAAK,OAAO,MAAQ,OACjD,KAAK,OAAO,WAAW,KAAK,UAAU,UAAU,IAAI,KAAK,OAAO,WAIpE,EAAa,aAAa,KAAK,UAAW,EAAa,aAfrD,KAAK,UAAY,IAgBrB,CAOA,QAAA,CAAiB,GACf,MAAM,EAAI,KAAK,kBACT,EAAQ,GAAG,cACjB,OAAI,GAAS,EAAM,GAAe,EAAM,GAAO,OAExC,GADG,GAAG,WAAa,GAE5B,CAGA,QAAA,CAAiB,GACf,MAAM,EAAI,KAAK,kBACT,EAAQ,GAAG,cACjB,OAAI,GAAS,EAAM,GAAe,EAAM,GAAO,OACxC,GAAG,WAAa,EACzB,CAEA,eAAA,GACE,OAAO,KAAK,MAAM,eACpB,CAEA,mBAAA,GACE,MAAM,EAAI,KAAK,kBAOT,EAAgB,GAAG,WAAW,UACpC,MAA6B,iBAAlB,EAAmC,EAGxB,iBAAb,GAAG,OACc,iBAAjB,GAAG,WACR,EAAE,eAAkB,EAAE,cAAc,OAIpC,GAAG,eAAoC,iBAAZ,EAAE,OAAsB,EAAE,cAAc,EAAE,OAChE,EAAE,cAAc,EAAE,OAAO,OAE3B,EALE,EAAE,MAAQ,EAAE,SAMvB,CA8BA,cAAA,CAAuB,GACrB,GAAyB,UAArB,KAAK,OAAO,KAAkB,CAChC,MAAM,EAAM,KAAK,OAAO,YAAc,IAChC,EAAuB,GAC7B,IAAI,EAAc,EAClB,IAAK,MAAM,KAAO,KAAK,cACrB,GAAI,EAAW,OAAS,EAAK,CAG3B,KAAI,KAAK,SAAS,GAAO,EAAY,GAInC,MAHA,EAAW,KAAK,GAChB,GAAe,KAAK,SAAS,EAIjC,KAAO,CAUL,MAAM,EAAU,KAAK,SAAS,EAAW,IACzC,KAAI,KAAK,SAAS,GAAO,EAAY,EAAc,GAMjD,MALA,EAAW,QACX,GAAe,EACf,EAAW,KAAK,GAChB,GAAe,KAAK,SAAS,EAIjC,CAaF,GAAI,EAAW,SAAW,GAAO,EAAW,OAAS,EAAG,CACtD,MAAM,EAAO,KAAK,eAAe,EAAW,EAAW,OAAS,IAChE,GAAY,MAAR,EAAc,CAChB,MAAM,EAAW,KAAK,SAAS,IAAS,EAAY,GACpD,GAAI,EAAW,EAAG,CAChB,MAAM,EAAU,KAAK,SAAS,EAAW,IACzC,MAAO,CAAE,QAAS,EAAY,WAAY,KAAK,KAAK,EAAU,GAChE,CACF,CACF,CACA,MAAO,CAAE,QAAS,EAAY,WAAY,EAC5C,CAGA,MAAM,EAAuB,GAC7B,IAAK,MAAM,KAAO,KAAK,cACrB,MAAI,KAAK,SAAS,GAAO,GACpB,MAD+B,EAAW,KAAK,EAC/C,CAEP,GAA0B,IAAtB,EAAW,OAAc,MAAO,CAAE,QAAS,EAAY,WAAY,GACvE,MAAM,EAAQ,EAAW,EAAW,OAAS,GACvC,EAAO,KAAK,eAAe,GACjC,IAAI,EAAa,EACjB,GAAY,MAAR,EAAc,CAChB,MAAM,EAAS,KAAK,SAAS,GACvB,EAAW,KAAK,SAAS,GAAQ,EACnC,EAAW,IACb,EAAa,EAAS,KAAK,IAAI,EAAG,GAEtC,CACA,MAAO,CAAE,QAAS,CAAC,GAAQ,aAC7B,CAOA,eAAA,CAAwB,GACtB,MAAM,EAAO,KAAK,YAClB,OAAK,EAEQ,EAAK,cAAc,wCAAwC,QAC1D,eAAwC,KAHpC,IAIpB,CAGA,UAAA,CAAmB,GACjB,MAAM,EAAO,KAAK,gBAAgB,GAClC,IAAK,EAAM,OAAO,KAAK,WAAW,IAAI,IAAU,KAChD,MAAM,EAAQ,EAAK,WAAU,GAoB7B,OAnBA,EAAM,UAAU,IAAI,kBACpB,EAAM,gBAAgB,iBAGtB,EAAM,aAAa,cAAe,QAClC,EAAM,QAAQ,UAAe,OAAO,GAEpC,EAAM,UAAU,OAAO,YAAa,cACpC,EAAM,iBAAiB,2BAA2B,QAAS,GAAO,EAAG,UAAU,OAAO,aAAc,cAEpG,EAAM,gBAAgB,YACtB,EAAM,iBAAiB,cAAc,QAAS,GAAO,EAAG,gBAAgB,aAKxE,KAAK,cAAgB,EAAM,WAAU,GAErC,KAAK,WAAW,IAAI,EAAO,GACpB,CACT,CA0BA,eAAA,CAAwB,GACtB,MAAM,EAAW,KAAK,cAChB,EAAM,KAAK,KAAK,GACtB,IAAK,GAAmB,MAAP,EAAa,OAAO,KAErC,MAAM,EAAK,EAAS,WAAU,GACxB,EAAQ,MAAM,KAAK,EAAG,iBAA8B,UAC1D,GAAI,EAAM,KAAM,GAAmC,MAA1B,EAAK,mBAA4B,OAAO,KAEjE,EAAG,QAAQ,UAAe,OAAO,GAGjC,EAAG,QAAQ,mBAAwB,GACnC,EAAG,gBAAgB,iBAEnB,MAAM,EAAU,KAAK,eACrB,IAAK,MAAM,KAAQ,EAAO,CACxB,EAAK,aAAa,WAAY,OAAO,IACrC,MAAM,EAAQ,EAAK,aAAa,cAC1B,EAAmC,EAAQ,EAAQ,KAAM,GAAM,EAAE,QAAU,QAAS,EACpF,EAAQ,GAAA,EAAS,EAAA,kBAAiB,EAAK,EAAQ,GAAS,KAC9D,EAAK,YAAuB,MAAT,EAAgB,GAAK,OAAO,EACjD,CACA,OAAO,CACT,CAKA,qBAAA,GACE,GAAK,KAAK,iBAAiB,OAC3B,IAAK,MAAM,KAAO,KAAK,iBAErB,GADa,KAAK,gBAAgB,GACxB,CAER,MAAM,EAAQ,KAAK,WAAW,GACxB,EAAgB,KAAK,WAAW,cAAc,qBAAqB,OACrE,GAAS,GAAiB,IAAkB,GAC9C,EAAc,YAAY,EAE9B,CAEJ,CAMA,eAAA,GACE,GAAK,KAAK,cAAc,OACxB,IAAK,MAAM,KAAO,KAAK,cACjB,KAAK,gBAAgB,IAEvB,KAAK,WAAW,EAGtB,CAGA,cAAA,CAAuB,GACrB,IAAK,KAAK,UAAW,OAErB,MAAM,EAAY,GAAqB,KAAK,uBACpC,QAAS,EAAA,WAAS,GAAe,KAAK,eAAe,GAU7D,GAPI,KAAK,UAAU,QAAQ,OAAY,KAAK,OAAO,OACjD,KAAK,UAAU,QAAQ,KAAU,KAAK,OAAO,MAAQ,QAIrD,EAAQ,SAAW,KAAK,iBAAiB,SAAU,EAAQ,MAAA,CAAO,EAAG,IAAM,IAAM,KAAK,iBAAiB,IAE3F,CAGZ,MAAM,EAAW,SAAS,yBACpB,EAAqB,GAC3B,IAAK,MAAM,KAAO,EAAS,CACzB,IAAI,EAAQ,KAAK,WAAW,IAAI,IAAQ,KAGxC,EADc,KAAK,WAAW,IACb,EACZ,IAKH,EAAQ,KAAK,gBAAgB,GACzB,GAAO,KAAK,WAAW,IAAI,EAAK,IAElC,IACF,EAAS,YAAY,GACrB,EAAS,KAAK,GAElB,CACA,KAAK,UAAU,gBAAgB,GAI/B,KAAK,iBAAmB,CAC1B,CAMI,IAAe,KAAK,iBACtB,KAAK,UAAU,MAAM,UAAY,EAAa,EAAI,eAAe,OAAkB,GACnF,KAAK,eAAiB,EAE1B,CAGA,cAAA,CAAuB,GACrB,IAAK,MAAM,KAAO,KAAK,cACrB,GAAI,EAAM,EAAS,OAAO,EAE5B,OAAO,IACT"}
|
|
1
|
+
{"version":3,"file":"sticky-rows.umd.js","names":["BaseGridPlugin","styles","resolveCellValue","global","factory","exports","module","require","define","amd","globalThis","self","TbwGridPlugin_stickyRows","TbwGrid","this","_toolbox_web_grid","Object","defineProperty","Symbol","toStringTag","value","StickyRowsPlugin","BaseGridPlugin","name","styles","defaultConfig","mode","maxStacked","Infinity","container","stickyIndices","cloneCache","Map","displayedIndices","templateClone","lastPushOffset","settlePassScheduled","detach","remove","clear","afterRender","recomputeStickyIndices","ensureContainer","primeCloneCache","refreshClonesInWindow","refreshDisplay","onScrollRender","maybeScheduleSettlePass","onScroll","event","scrollTop","getVerticalScrollOffsets","focusedRowIndex","length","includes","top","bottom","skipScroll","offsetHeight","some","idx","findRenderedRow","requestAfterRender","resolvePredicate","cfg","config","isSticky","field","row","Boolean","rows","predicate","next","i","push","v","grid","gridElement","rowsViewport","querySelector","contains","document","createElement","className","setAttribute","dataset","classList","add","insertBefore","firstChild","offsetOf","index","getVirtualState","cache","positionCache","offset","rowHeight","heightOf","height","_virtualization","getCurrentScrollTop","liveScrollTop","start","computeDisplay","max","qualifying","stuckHeight","oldestH","shift","findNextSticky","distance","indices","pushOffset","Math","min","stuck","stuckH","parentElement","buildClone","live","get","clone","cloneNode","removeAttribute","String","querySelectorAll","forEach","el","set","synthesizeClone","template","cells","Array","from","cell","firstElementChild","columns","visibleColumns","getAttribute","column","find","c","resolveCellValue","textContent","fresh","existingClone","replaceWith","scrollTopOverride","desired","every","fragment","createDocumentFragment","appended","appendChild","replaceChildren","style","transform","current"],"sources":["../../../../../libs/grid/src/lib/plugins/sticky-rows/sticky-rows.css?inline","../../../../../libs/grid/src/lib/plugins/sticky-rows/sticky-rows-plugin.ts"],"sourcesContent":["@layer tbw-plugins {\n /* Container for stuck-row clones. OVERLAYS the top of the rows-viewport\n * (not a flex child of rows-body) so it does not push the live rows\n * down. The rows-viewport's `overflow: clip` hides the live row\n * underneath the overlay. See StickyRowsPlugin.ensureContainer().\n *\n * Width matches `min-width: fit-content` so clones inherit the\n * grid-template-columns and scroll horizontally with the body. */\n .tbw-sticky-rows {\n position: absolute;\n top: 0;\n left: 0;\n display: block;\n z-index: var(--tbw-z-layer-sticky-rows, 22);\n background: var(--tbw-color-bg, var(--tbw-color-panel-bg));\n min-width: 100%;\n width: fit-content;\n /* Allow translateY on push-mode clones to render above the wrapper edge. */\n overflow: visible;\n }\n\n .tbw-sticky-rows:empty {\n display: none;\n }\n\n /* The cloned row. We reuse `.data-grid-row` class so existing row CSS applies\n * without duplication. Only the sticky-specific overrides live here. */\n .tbw-sticky-rows .tbw-sticky-row {\n /* Shadow under the bottom edge to separate from scrolling rows below. */\n box-shadow: 0 1px 0 var(--tbw-color-border);\n /* Clones never participate in row striping or focus highlight; force a\n * solid background so they read as a distinct, opaque layer. */\n background: var(--tbw-color-bg, var(--tbw-color-panel-bg));\n }\n\n /* Push-mode: the bottom-most stuck clone may translate up when displaced\n * by the next incoming sticky row. */\n .tbw-sticky-rows[data-mode='push'] .tbw-sticky-row {\n will-change: transform;\n }\n}\n","/**\n * Sticky Rows Plugin (Class-based)\n *\n * Pins selected data rows below the header as the user scrolls past them.\n * See `types.ts` for configuration.\n *\n * @module Plugins/Sticky Rows\n */\n\nimport { resolveCellValue } from '../../core/internal/value-accessor';\nimport type { RowPosition } from '../../core/internal/virtualization';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { ScrollEvent } from '../../core/plugin/types';\nimport type { ColumnConfig } from '../../core/types';\nimport styles from './sticky-rows.css?inline';\nimport type { StickyPredicate, StickyRowsConfig } from './types';\n\n/** Internal shape of the virtualization state we read from the grid. */\ninterface VirtualStateLike {\n start?: number;\n end?: number;\n rowHeight?: number;\n positionCache?: RowPosition[] | null;\n /** The faux-scroll element — the source of truth for the live scroll\n * position (per-pixel, unlike `start` which only updates when the\n * rendered window shifts). */\n container?: HTMLElement | null;\n}\n\n/**\n * Sticky Rows plugin for `<tbw-grid>`.\n *\n * Marks specific rows as \"sticky\" so they pin below the grid header when their\n * natural scroll position would take them off-screen. Behavior when multiple\n * sticky rows would be stuck simultaneously is controlled by `mode`:\n *\n * - `'push'` (default) — only one stuck at a time; the next sticky row pushes\n * the previous one out of view (iOS section-header behavior).\n * - `'stack'` — sticky rows stack below the header up to `maxStacked`.\n *\n * The plugin renders **clones** of the real rows; the originals continue to\n * exist in the data flow. Clones inherit the row's grid-template alignment so\n * they line up perfectly with the column boundaries below.\n *\n * @example\n * ```ts\n * import '@toolbox-web/grid/features/sticky-rows';\n *\n * grid.gridConfig = {\n * features: { stickyRows: { isSticky: 'isSection', mode: 'stack' } },\n * };\n * ```\n *\n * @see {@link StickyRowsConfig} for all configuration options.\n *\n * @internal Extends BaseGridPlugin.\n * @since 2.7.0\n */\nexport class StickyRowsPlugin extends BaseGridPlugin<StickyRowsConfig> {\n /** @internal */\n readonly name = 'stickyRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<StickyRowsConfig> {\n return {\n mode: 'push',\n maxStacked: Infinity,\n };\n }\n\n // #region Internal State\n /** Container element that holds the stuck-row clones. */\n private container: HTMLElement | null = null;\n\n /** Indices in `grid.rows` whose rows are sticky, sorted ascending. */\n private stickyIndices: number[] = [];\n\n /** Cached clone DOM keyed by row index. Survives the row leaving the\n * virtualization window so we can keep showing it while stuck. */\n private cloneCache: Map<number, HTMLElement> = new Map();\n\n /** Currently displayed indices (in DOM order). Used to skip work when the\n * set hasn't changed between scroll ticks. */\n private displayedIndices: number[] = [];\n\n /** Detached copy of the most recent clone captured from a LIVE row, used as\n * the structural template for {@link synthesizeClone}. Real captures only —\n * never a synthesized stand-in. */\n private templateClone: HTMLElement | null = null;\n\n /** Last applied push-mode translation, to skip writes on no-op ticks. */\n private lastPushOffset = 0;\n\n /** True while a deferred \"settle\" style pass is already queued. Coalesces\n * multiple scroll ticks into a single re-capture. See\n * {@link maybeScheduleSettlePass}. */\n private settlePassScheduled = false;\n // #endregion\n\n // #region Lifecycle\n /** @internal */\n override detach(): void {\n this.container?.remove();\n this.container = null;\n this.cloneCache.clear();\n this.templateClone = null;\n this.stickyIndices = [];\n this.displayedIndices = [];\n this.lastPushOffset = 0;\n this.settlePassScheduled = false;\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal Recompute the sticky-index list and refresh display. */\n override afterRender(): void {\n // A style pass is now running — possibly the deferred settle pass we\n // requested. Clear the flag so a later scroll tick can queue a fresh one.\n this.settlePassScheduled = false;\n this.recomputeStickyIndices();\n this.ensureContainer();\n // Pre-cache clones for any sticky row currently in the rendered window\n // BEFORE it scrolls out — otherwise findRenderedRow() returns null at\n // display time and we'd render an empty container.\n this.primeCloneCache();\n // Re-capture the clones that are CURRENTLY displayed from the (now\n // settled) live DOM. WHY: `primeCloneCache` refreshes the cache, but\n // `refreshDisplay` only swaps the container's child nodes when the\n // displayed *set* changes. On the deferred settle pass after a small\n // scroll-up — where the sticky set is unchanged (e.g. row 0 stays the\n // single pinned row) — the stale clone captured during `onScrollRender`\n // would otherwise remain in the DOM forever, since `sameSet` short-\n // circuits the rebuild. Reconciling displayed clones here replaces the\n // stale node with the freshly-settled content. Symptom it fixes: scroll\n // down past the sticky row, then up a few px → a non-sticky row's content\n // is pinned until you scroll all the way to the absolute top.\n this.refreshClonesInWindow();\n this.refreshDisplay();\n }\n\n /** @internal Re-render clones after a scroll-driven row pool refresh. */\n override onScrollRender(): void {\n // Pre-cache clones for any sticky row currently in the rendered window so\n // we have one ready when it scrolls past. Do NOT re-capture the DISPLAYED\n // clones synchronously here. WHY: when a sticky row is recycled back into\n // the window on scroll-up, the grid sets its cell `data-row` synchronously\n // but async framework adapters (React/Vue) flush the cell content a\n // microtask LATER — so a synchronous re-capture would paint the PREVIOUS\n // row's content for one frame before the deferred settle pass corrects it\n // (visible flash on the sticky row at the scroll boundary). Re-capturing\n // displayed clones is therefore deferred entirely to the settle pass\n // (`afterRender` → `refreshClonesInWindow`), which runs after the\n // microtask flush. A settle pass is always scheduled below whenever a\n // sticky row is in the window — exactly the case this method could refresh\n // — so no displayed-clone update is lost, only delayed one frame.\n this.primeCloneCache();\n this.refreshDisplay();\n this.maybeScheduleSettlePass();\n }\n\n /** @internal Update which clones show / how they're positioned. */\n override onScroll(event: ScrollEvent): void {\n this.refreshDisplay(event.scrollTop);\n }\n\n /**\n * @internal Report how much of the rows viewport the clone overlay covers so\n * keyboard navigation can scroll a focused row clear of it (WCAG 2.2 SC\n * 2.4.11 Focus Not Obscured). The container is `position: absolute; top: 0`\n * over `.rows-viewport`, so a row scrolled to the exact top would otherwise\n * land underneath it.\n */\n override getVerticalScrollOffsets(\n focusedRowIndex?: number,\n ): { top: number; bottom: number; skipScroll?: boolean } | undefined {\n if (!this.container || this.displayedIndices.length === 0) return undefined;\n // The row IS one of the stuck clones — it is already pinned in view, and\n // scrolling to it would only push the row it is stuck above out of reach.\n if (focusedRowIndex != null && this.displayedIndices.includes(focusedRowIndex)) {\n return { top: 0, bottom: 0, skipScroll: true };\n }\n return { top: this.container.offsetHeight, bottom: 0 };\n }\n\n /**\n * Queue a single deferred style pass that re-captures sticky clones once\n * async framework cell content has settled.\n *\n * WHY: framework adapters (React/Vue) flush custom cell content on a\n * microtask (`PortalManager` batches via `queueMicrotask` + `flushSync`)\n * AFTER this synchronous scroll hook. When a sticky row is recycled back\n * into the rendered window on scroll-up, the grid sets the cell\n * `data-row` attribute synchronously but the cell's rendered content still\n * shows the PREVIOUS row. A clone captured during the hook therefore holds\n * stale content, and because clones are cached by index it stays wrong\n * forever (symptom: scrolling down, back up, then down again pins a\n * different row than the first time). Re-running `afterRender` on the next\n * style pass — after the microtask flush — re-captures the now-settled\n * clone. Only schedule when a sticky row is actually in the rendered window\n * (the only case where a stale capture could have happened) to avoid\n * needless style passes during ordinary scrolling.\n */\n private maybeScheduleSettlePass(): void {\n if (this.settlePassScheduled) return;\n if (!this.stickyIndices.some((idx) => this.findRenderedRow(idx) != null)) return;\n this.settlePassScheduled = true;\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Private — predicate + indices\n\n private resolvePredicate(): StickyPredicate {\n const cfg = this.config.isSticky;\n if (typeof cfg === 'function') return cfg;\n if (typeof cfg === 'string') {\n const field = cfg;\n return (row) => {\n if (row == null || typeof row !== 'object') return false;\n return Boolean((row as Record<string, unknown>)[field]);\n };\n }\n return () => false;\n }\n\n private recomputeStickyIndices(): void {\n const rows = this.rows;\n const predicate = this.resolvePredicate();\n const next: number[] = [];\n for (let i = 0; i < rows.length; i++) {\n if (predicate(rows[i], i)) next.push(i);\n }\n // Drop cache entries whose indices are no longer sticky (e.g. data\n // shuffled, predicate flipped). Indices that remain may still point at a\n // different row after re-sort — drop those too so we re-clone from the\n // fresh DOM on the next render.\n if (next.length !== this.stickyIndices.length || next.some((v, i) => v !== this.stickyIndices[i])) {\n this.cloneCache.clear();\n }\n this.stickyIndices = next;\n }\n\n // #endregion\n\n // #region Private — DOM container\n\n private ensureContainer(): void {\n const grid = this.gridElement;\n if (!grid) return;\n\n // Container is an OVERLAY positioned at the top of the rows-viewport\n // (not a flex child of rows-body). Why: a flow-positioned container\n // takes vertical space, which pushes the rows-viewport down by the\n // sticky height. The rows are positioned in scroll coordinates that\n // are agnostic of that shift, so live-row geometry ends up partially\n // overlapping the sticky container — producing duplicate visible rows\n // during the push transition (#279 follow-up). Overlaying on top of\n // the row pool, with `overflow: clip` on rows-viewport hiding the\n // live row underneath, eliminates the overlap entirely.\n const rowsViewport = grid.querySelector('.rows-viewport');\n if (!rowsViewport) {\n // Grid hasn't built its DOM yet; bail and try again on next afterRender.\n this.container = null;\n return;\n }\n\n if (this.container && rowsViewport.contains(this.container)) return;\n\n // Container was either never created or got wiped out. Re-create.\n this.container = document.createElement('div');\n this.container.className = 'tbw-sticky-rows';\n this.container.setAttribute('role', 'presentation');\n this.container.dataset['mode'] = this.config.mode ?? 'push';\n if (this.config.className) this.container.classList.add(this.config.className);\n\n // Insert as the FIRST child of rows-viewport so it overlays on top of\n // the row pool. Z-index in CSS keeps it above `.rows`.\n rowsViewport.insertBefore(this.container, rowsViewport.firstChild);\n }\n\n // #endregion\n\n // #region Private — offsets + which-to-display\n\n /** Pixel offset of the top of `index` from the start of the scroll area. */\n private offsetOf(index: number): number {\n const v = this.getVirtualState();\n const cache = v?.positionCache;\n if (cache && cache[index]) return cache[index].offset;\n const h = v?.rowHeight ?? 28;\n return index * h;\n }\n\n /** Estimated height of `index` (used for push-mode displacement). */\n private heightOf(index: number): number {\n const v = this.getVirtualState();\n const cache = v?.positionCache;\n if (cache && cache[index]) return cache[index].height;\n return v?.rowHeight ?? 28;\n }\n\n private getVirtualState(): VirtualStateLike | undefined {\n return this.grid?._virtualization;\n }\n\n private getCurrentScrollTop(): number {\n const v = this.getVirtualState();\n // PRIMARY source: the live `scrollTop` of the faux-scroll element. This\n // updates per-pixel as the user drags, unlike `_virtualization.start`\n // which only ticks over when the rendered window shifts (every\n // `rowHeight` px). Reading the stale start-derived value caused\n // `afterRender` to clobber the correct displayed-set that `onScroll`\n // had just computed (#279 follow-up).\n const liveScrollTop = v?.container?.scrollTop;\n if (typeof liveScrollTop === 'number') return liveScrollTop;\n // Fallbacks (unit tests / pre-mount): derive from window-start.\n if (\n typeof v?.start === 'number' &&\n typeof v?.rowHeight === 'number' &&\n (!v.positionCache || !v.positionCache.length)\n ) {\n return v.start * v.rowHeight;\n }\n if (v?.positionCache && typeof v.start === 'number' && v.positionCache[v.start]) {\n return v.positionCache[v.start].offset;\n }\n return 0;\n }\n\n /**\n * Compute the displayed sticky indices and any upward push offset, given\n * the current scroll offset.\n *\n * - `'push'` mode: a row qualifies once its top has scrolled past the\n * viewport top (strict `<`); only the highest qualifying index is\n * shown. As the next sticky approaches from below, the stuck clone\n * slides upward by `heightOfStuck - distance` (iOS section-header\n * behavior).\n * - `'stack'` mode: each subsequent sticky qualifies once its top\n * reaches the bottom of the existing stack (`offsetOf(idx) < scrollTop\n * + cumulativeHeightOfStuck`), so `— B —` latches when it meets\n * `— A —`'s bottom — not after `— A —` covers it. When the stack is\n * at `maxStacked` and the **live** next sticky row's top crosses the\n * bottom of the stack (`distance <= 0`), the entire stack translates\n * upward by `-distance`. Live rows above the stack (including the\n * incoming sticky) scroll up naturally beneath the overlay; rows-\n * viewport's `overflow: clip` hides anything above the top edge. When\n * the oldest is fully off (pushOffset ≥ heightOf(oldest)), the next\n * tick qualifies the new sticky and we slice to `last max` with\n * transform reset — at that exact pixel the live new sticky is sitting\n * precisely behind its slot in the new stack, producing a seamless\n * swap with no duplicate row visible.\n *\n * Strict `<` keeps the qualification edge symmetric with push mode and\n * avoids briefly rendering both a clone and the live row at the swap\n * pixel.\n */\n private computeDisplay(scrollTop: number): { indices: number[]; pushOffset: number } {\n if (this.config.mode === 'stack') {\n const max = this.config.maxStacked ?? Infinity;\n const qualifying: number[] = [];\n let stuckHeight = 0;\n for (const idx of this.stickyIndices) {\n if (qualifying.length < max) {\n // Within the cap: a sticky qualifies once its top reaches the\n // bottom of the existing stack.\n if (this.offsetOf(idx) < scrollTop + stuckHeight) {\n qualifying.push(idx);\n stuckHeight += this.heightOf(idx);\n } else {\n break; // sorted ascending — anything after won't qualify either\n }\n } else {\n // At cap: evict-and-promote only after `idx` has fully crossed\n // the slot of the current oldest. WHY: while `idx` is mid-cross\n // (its top is past the stack bottom but the oldest isn't yet\n // off the top), we want to ANIMATE that transition by sliding\n // the existing stack upward — not snap to the post-evict state.\n // Without this clamp, qualification fires the instant the new\n // sticky crosses the stack bottom (same scrollTop where the\n // anticipation block would otherwise activate), and the\n // anticipation never gets a chance to render.\n const oldestH = this.heightOf(qualifying[0]);\n if (this.offsetOf(idx) < scrollTop + stuckHeight - oldestH) {\n qualifying.shift();\n stuckHeight -= oldestH;\n qualifying.push(idx);\n stuckHeight += this.heightOf(idx);\n } else {\n break;\n }\n }\n }\n\n // Anticipation: at-cap and the live next sticky has crossed the\n // stack bottom but not yet fully past the oldest's slot. Translate\n // the entire stack up by `-distance` so the user sees the live\n // next-sticky scrolling up against the bottom of the stack while\n // the oldest slides off the top — exactly mirroring iOS Contacts.\n // Do NOT add the new sticky to the displayed list during this\n // window: it's still in the row pool and visible there. Adding a\n // clone too would produce a visible duplicate AND would cover the\n // last regular row under the previous section while live-next is\n // still approaching.\n if (qualifying.length === max && qualifying.length > 0) {\n const next = this.findNextSticky(qualifying[qualifying.length - 1]);\n if (next != null) {\n const distance = this.offsetOf(next) - (scrollTop + stuckHeight);\n if (distance < 0) {\n const oldestH = this.heightOf(qualifying[0]);\n return { indices: qualifying, pushOffset: Math.min(-distance, oldestH) };\n }\n }\n }\n return { indices: qualifying, pushOffset: 0 };\n }\n\n // 'push' mode.\n const qualifying: number[] = [];\n for (const idx of this.stickyIndices) {\n if (this.offsetOf(idx) < scrollTop) qualifying.push(idx);\n else break;\n }\n if (qualifying.length === 0) return { indices: qualifying, pushOffset: 0 };\n const stuck = qualifying[qualifying.length - 1];\n const next = this.findNextSticky(stuck);\n let pushOffset = 0;\n if (next != null) {\n const stuckH = this.heightOf(stuck);\n const distance = this.offsetOf(next) - scrollTop;\n if (distance < stuckH) {\n pushOffset = stuckH - Math.max(0, distance);\n }\n }\n return { indices: [stuck], pushOffset };\n }\n\n // #endregion\n\n // #region Private — clone management\n\n /** Locate a rendered row in the viewport by its data index. */\n private findRenderedRow(index: number): HTMLElement | null {\n const grid = this.gridElement;\n if (!grid) return null;\n // Cells carry `data-row=\"<index>\"`. The row element is the parent.\n const cell = grid.querySelector(`.rows .data-grid-row .cell[data-row=\"${index}\"]`);\n return (cell?.parentElement as HTMLElement | null) ?? null;\n }\n\n /** Build (or refresh) a clone for `index`, using the live row when present. */\n private buildClone(index: number): HTMLElement | null {\n const live = this.findRenderedRow(index);\n if (!live) return this.cloneCache.get(index) ?? null;\n const clone = live.cloneNode(true) as HTMLElement;\n clone.classList.add('tbw-sticky-row');\n clone.removeAttribute('aria-rowindex');\n // Stuck clones are decorative — the original row is still present in the\n // accessibility tree below. Hide clones from AT to avoid double-reads.\n clone.setAttribute('aria-hidden', 'true');\n clone.dataset['stickyRow'] = String(index);\n // Drop any focus styling from the source — clones are non-interactive.\n clone.classList.remove('row-focus', 'cell-focus');\n clone.querySelectorAll('.cell-focus, .row-focus').forEach((el) => el.classList.remove('cell-focus', 'row-focus'));\n // Strip tabindex on cells so the clone isn't a tab target.\n clone.removeAttribute('tabindex');\n clone.querySelectorAll('[tabindex]').forEach((el) => el.removeAttribute('tabindex'));\n\n // Keep a detached copy as the structural template for synthesized\n // stand-ins. Captured post-sanitisation so a synthetic inherits the same\n // classes / aria treatment.\n this.templateClone = clone.cloneNode(true) as HTMLElement;\n\n this.cloneCache.set(index, clone);\n return clone;\n }\n\n /**\n * Build a data-derived stand-in for a sticky row that has never been\n * rendered in this session.\n *\n * WHY: clones are DOM captures, so a sticky row only becomes displayable\n * after it has passed through the virtualization window. Two situations\n * break that assumption, and in BOTH of them scrolling further down never\n * brings the row back — the stack silently under-renders forever:\n *\n * 1. A config or data change wipes the cache (`detach` / index change in\n * `recomputeStickyIndices`) while the grid is scrolled deep. Only the\n * sticky rows still inside the window get re-primed.\n * 2. The grid mounts (or restores a scroll position) already scrolled past\n * several sticky rows.\n *\n * Symptom: `'stack'` mode shows one pinned row instead of `maxStacked`.\n *\n * The stand-in reuses the last real capture for structure — so column\n * widths, classes and alignment are exact — and rewrites the cell text from\n * the row data. Returns `null` (caller omits the row, the pre-existing\n * behaviour) when there is no template yet, or when any cell holds element\n * children: those come from a custom cell renderer and re-labelling them\n * would pin ANOTHER row's rendered content, which is worse than a gap.\n */\n private synthesizeClone(index: number): HTMLElement | null {\n const template = this.templateClone;\n const row = this.rows[index];\n if (!template || row == null) return null;\n\n const el = template.cloneNode(true) as HTMLElement;\n const cells = Array.from(el.querySelectorAll<HTMLElement>('.cell'));\n if (cells.some((cell) => cell.firstElementChild != null)) return null;\n\n el.dataset['stickyRow'] = String(index);\n // Marks the row as data-derived rather than DOM-captured. `refreshClones-\n // InWindow` upgrades it to a real capture as soon as the row renders.\n el.dataset['syntheticStickyRow'] = '';\n el.removeAttribute('aria-rowindex');\n\n const columns = this.visibleColumns;\n for (const cell of cells) {\n cell.setAttribute('data-row', String(index));\n const field = cell.getAttribute('data-field');\n const column: ColumnConfig | undefined = field ? columns.find((c) => c.field === field) : undefined;\n const value = column ? resolveCellValue(row, column, index) : null;\n cell.textContent = value == null ? '' : String(value);\n }\n return el;\n }\n\n /** Refresh cached clones for any displayed indices that are now in-window.\n * Pulls fresh DOM so edits / re-renders are reflected when the row swings\n * back into view. */\n private refreshClonesInWindow(): void {\n if (!this.displayedIndices.length) return;\n for (const idx of this.displayedIndices) {\n const live = this.findRenderedRow(idx);\n if (live) {\n // Re-clone to pick up any post-render changes.\n const fresh = this.buildClone(idx);\n const existingClone = this.container?.querySelector(`[data-sticky-row=\"${idx}\"]`);\n if (fresh && existingClone && existingClone !== fresh) {\n existingClone.replaceWith(fresh);\n }\n }\n }\n }\n\n /** Build clones for any sticky row currently rendered in the viewport so we\n * have one ready when the row scrolls past. Without this, a sticky row\n * that's never been displayed-as-stuck won't have a cache entry, and once\n * the live row leaves the rendered window we have nothing to render. */\n private primeCloneCache(): void {\n if (!this.stickyIndices.length) return;\n for (const idx of this.stickyIndices) {\n if (this.findRenderedRow(idx)) {\n // buildClone() handles the live → cache path itself.\n this.buildClone(idx);\n }\n }\n }\n\n /** Reconcile the container DOM with the desired displayed indices. */\n private refreshDisplay(scrollTopOverride?: number): void {\n if (!this.container) return;\n\n const scrollTop = scrollTopOverride ?? this.getCurrentScrollTop();\n const { indices: desired, pushOffset } = this.computeDisplay(scrollTop);\n\n // Update the container's mode marker (in case config changed).\n if (this.container.dataset['mode'] !== this.config.mode) {\n this.container.dataset['mode'] = this.config.mode ?? 'push';\n }\n\n const sameSet =\n desired.length === this.displayedIndices.length && desired.every((v, i) => v === this.displayedIndices[i]);\n\n if (!sameSet) {\n // Rebuild the container's children. cloneCache survives between calls so\n // we re-use existing clones for indices that remain displayed.\n const fragment = document.createDocumentFragment();\n const appended: number[] = [];\n for (const idx of desired) {\n let clone = this.cloneCache.get(idx) ?? null;\n // Always try to refresh from live DOM if available.\n const fresh = this.buildClone(idx);\n clone = fresh ?? clone;\n if (!clone) {\n // Never captured — fall back to a data-derived stand-in, otherwise\n // this index is dropped from the stack permanently (see\n // `synthesizeClone`). Cached so the next tick reuses it; a real\n // capture overwrites it the moment the row renders.\n clone = this.synthesizeClone(idx);\n if (clone) this.cloneCache.set(idx, clone);\n }\n if (clone) {\n fragment.appendChild(clone);\n appended.push(idx);\n }\n }\n this.container.replaceChildren(fragment);\n // Track only the indices we actually appended so a subsequent refresh\n // (after a missing index gets primed via onScrollRender) will detect a\n // diff and retry rather than incorrectly believing the set is settled.\n this.displayedIndices = appended;\n }\n\n // Apply the upward translation to the entire container so push-mode\n // (1 child) and stack-mode anticipation (max+1 children) both slide\n // uniformly. CSS `overflow: clip` on `.rows-viewport` hides anything\n // translated above its top edge.\n if (pushOffset !== this.lastPushOffset) {\n this.container.style.transform = pushOffset > 0 ? `translateY(${-pushOffset}px)` : '';\n this.lastPushOffset = pushOffset;\n }\n }\n\n /** Find the next sticky index strictly greater than `current`. */\n private findNextSticky(current: number): number | null {\n for (const idx of this.stickyIndices) {\n if (idx > current) return idx;\n }\n return null;\n }\n\n // #endregion\n}\n"],"mappings":"C,+WC0DaqB,EAAb,cAAsCrB,EAAAA,eAEpCuB,KAAgB,aAEhBC,O,0dAGA,iBAAuBC,GACrB,MAAO,CACLC,KAAM,OACNC,WAAYC,IAEhB,CAIAC,UAAwC,KAGxCC,cAAkC,GAIlCC,WAA+C,IAAIC,IAInDC,iBAAqC,GAKrCC,cAA4C,KAG5CC,eAAyB,EAKzBC,qBAA8B,EAK9B,MAAAC,GACEvB,KAAKe,WAAWS,SAChBxB,KAAKe,UAAY,KACjBf,KAAKiB,WAAWQ,QAChBzB,KAAKoB,cAAgB,KACrBpB,KAAKgB,cAAgB,GACrBhB,KAAKmB,iBAAmB,GACxBnB,KAAKqB,eAAiB,EACtBrB,KAAKsB,qBAAsB,CAC7B,CAMA,WAAAI,GAGE1B,KAAKsB,qBAAsB,EAC3BtB,KAAK2B,yBACL3B,KAAK4B,kBAIL5B,KAAK6B,kBAYL7B,KAAK8B,wBACL9B,KAAK+B,gBACP,CAGA,cAAAC,GAcEhC,KAAK6B,kBACL7B,KAAK+B,iBACL/B,KAAKiC,yBACP,CAGA,QAAAC,CAAkBC,GAChBnC,KAAK+B,eAAeI,EAAMC,UAC5B,CASA,wBAAAC,CACEC,GAEA,GAAKtC,KAAKe,WAA8C,IAAjCf,KAAKmB,iBAAiBoB,OAG7C,OAAuB,MAAnBD,GAA2BtC,KAAKmB,iBAAiBqB,SAASF,GACrD,CAAEG,IAAK,EAAGC,OAAQ,EAAGC,YAAY,GAEnC,CAAEF,IAAKzC,KAAKe,UAAU6B,aAAcF,OAAQ,EACrD,CAoBA,uBAAAT,GACMjC,KAAKsB,qBACJtB,KAAKgB,cAAc6B,KAAMC,GAAqC,MAA7B9C,KAAK+C,gBAAgBD,MAC3D9C,KAAKsB,qBAAsB,EAC3BtB,KAAKgD,qBACP,CAMA,gBAAAC,GACE,MAAMC,EAAMlD,KAAKmD,OAAOC,SACxB,GAAmB,mBAARF,EAAoB,OAAOA,EACtC,GAAmB,iBAARA,EAAkB,CAC3B,MAAMG,EAAQH,EACd,OAAQI,GACK,MAAPA,GAA8B,iBAARA,GACnBC,QAASD,EAAgCD,GAEpD,CACA,MAAA,KAAa,CACf,CAEA,sBAAA1B,GACE,MAAM6B,EAAOxD,KAAKwD,KACZC,EAAYzD,KAAKiD,mBACjBS,EAAiB,GACvB,IAAK,IAAIC,EAAI,EAAGA,EAAIH,EAAKjB,OAAQoB,IAC3BF,EAAUD,EAAKG,GAAIA,IAAID,EAAKE,KAAKD,IAMnCD,EAAKnB,SAAWvC,KAAKgB,cAAcuB,QAAUmB,EAAKb,KAAA,CAAMgB,EAAGF,IAAME,IAAM7D,KAAKgB,cAAc2C,MAC5F3D,KAAKiB,WAAWQ,QAElBzB,KAAKgB,cAAgB0C,CACvB,CAMA,eAAA9B,GACE,MAAMkC,EAAO9D,KAAK+D,YAClB,IAAKD,EAAM,OAWX,MAAME,EAAeF,EAAKG,cAAc,kBACnCD,EAMDhE,KAAKe,WAAaiD,EAAaE,SAASlE,KAAKe,aAGjDf,KAAKe,UAAYoD,SAASC,cAAc,OACxCpE,KAAKe,UAAUsD,UAAY,kBAC3BrE,KAAKe,UAAUuD,aAAa,OAAQ,gBACpCtE,KAAKe,UAAUwD,QAAQ,KAAUvE,KAAKmD,OAAOvC,MAAQ,OACjDZ,KAAKmD,OAAOkB,WAAWrE,KAAKe,UAAUyD,UAAUC,IAAIzE,KAAKmD,OAAOkB,WAIpEL,EAAaU,aAAa1E,KAAKe,UAAWiD,EAAaW,aAfrD3E,KAAKe,UAAY,IAgBrB,CAOA,QAAA6D,CAAiBC,GACf,MAAMhB,EAAI7D,KAAK8E,kBACTC,EAAQlB,GAAGmB,cACjB,OAAID,GAASA,EAAMF,GAAeE,EAAMF,GAAOI,OAExCJ,GADGhB,GAAGqB,WAAa,GAE5B,CAGA,QAAAC,CAAiBN,GACf,MAAMhB,EAAI7D,KAAK8E,kBACTC,EAAQlB,GAAGmB,cACjB,OAAID,GAASA,EAAMF,GAAeE,EAAMF,GAAOO,OACxCvB,GAAGqB,WAAa,EACzB,CAEA,eAAAJ,GACE,OAAO9E,KAAK8D,MAAMuB,eACpB,CAEA,mBAAAC,GACE,MAAMzB,EAAI7D,KAAK8E,kBAOTS,EAAgB1B,GAAG9C,WAAWqB,UACpC,MAA6B,iBAAlBmD,EAAmCA,EAGxB,iBAAb1B,GAAG2B,OACc,iBAAjB3B,GAAGqB,WACRrB,EAAEmB,eAAkBnB,EAAEmB,cAAczC,OAIpCsB,GAAGmB,eAAoC,iBAAZnB,EAAE2B,OAAsB3B,EAAEmB,cAAcnB,EAAE2B,OAChE3B,EAAEmB,cAAcnB,EAAE2B,OAAOP,OAE3B,EALEpB,EAAE2B,MAAQ3B,EAAEqB,SAMvB,CA8BA,cAAAO,CAAuBrD,GACrB,GAAyB,UAArBpC,KAAKmD,OAAOvC,KAAkB,CAChC,MAAM8E,EAAM1F,KAAKmD,OAAOtC,YAAcC,IAChC6E,EAAuB,GAC7B,IAAIC,EAAc,EAClB,IAAK,MAAM9C,KAAO9C,KAAKgB,cACrB,GAAI2E,EAAWpD,OAASmD,EAAK,CAG3B,KAAI1F,KAAK4E,SAAS9B,GAAOV,EAAYwD,GAInC,MAHAD,EAAW/B,KAAKd,GAChB8C,GAAe5F,KAAKmF,SAASrC,EAIjC,KAAO,CAUL,MAAM+C,EAAU7F,KAAKmF,SAASQ,EAAW,IACzC,KAAI3F,KAAK4E,SAAS9B,GAAOV,EAAYwD,EAAcC,GAMjD,MALAF,EAAWG,QACXF,GAAeC,EACfF,EAAW/B,KAAKd,GAChB8C,GAAe5F,KAAKmF,SAASrC,EAIjC,CAaF,GAAI6C,EAAWpD,SAAWmD,GAAOC,EAAWpD,OAAS,EAAG,CACtD,MAAMmB,EAAO1D,KAAK+F,eAAeJ,EAAWA,EAAWpD,OAAS,IAChE,GAAY,MAARmB,EAAc,CAChB,MAAMsC,EAAWhG,KAAK4E,SAASlB,IAAStB,EAAYwD,GACpD,GAAII,EAAW,EAAG,CAChB,MAAMH,EAAU7F,KAAKmF,SAASQ,EAAW,IACzC,MAAO,CAAEM,QAASN,EAAYO,WAAYC,KAAKC,KAAKJ,EAAUH,GAChE,CACF,CACF,CACA,MAAO,CAAEI,QAASN,EAAYO,WAAY,EAC5C,CAGA,MAAMP,EAAuB,GAC7B,IAAK,MAAM7C,KAAO9C,KAAKgB,cACrB,MAAIhB,KAAK4E,SAAS9B,GAAOV,GACpB,MAD+BuD,EAAW/B,KAAKd,EAC/C,CAEP,GAA0B,IAAtB6C,EAAWpD,OAAc,MAAO,CAAE0D,QAASN,EAAYO,WAAY,GACvE,MAAMG,EAAQV,EAAWA,EAAWpD,OAAS,GACvCmB,EAAO1D,KAAK+F,eAAeM,GACjC,IAAIH,EAAa,EACjB,GAAY,MAARxC,EAAc,CAChB,MAAM4C,EAAStG,KAAKmF,SAASkB,GACvBL,EAAWhG,KAAK4E,SAASlB,GAAQtB,EACnC4D,EAAWM,IACbJ,EAAaI,EAASH,KAAKT,IAAI,EAAGM,GAEtC,CACA,MAAO,CAAEC,QAAS,CAACI,GAAQH,aAC7B,CAOA,eAAAnD,CAAwB8B,GACtB,MAAMf,EAAO9D,KAAK+D,YAClB,OAAKD,EAEQA,EAAKG,cAAc,wCAAwCY,QAC1D0B,eAAwC,KAHpC,IAIpB,CAGA,UAAAC,CAAmB3B,GACjB,MAAM4B,EAAOzG,KAAK+C,gBAAgB8B,GAClC,IAAK4B,EAAM,OAAOzG,KAAKiB,WAAWyF,IAAI7B,IAAU,KAChD,MAAM8B,EAAQF,EAAKG,WAAU,GAoB7B,OAnBAD,EAAMnC,UAAUC,IAAI,kBACpBkC,EAAME,gBAAgB,iBAGtBF,EAAMrC,aAAa,cAAe,QAClCqC,EAAMpC,QAAQ,UAAeuC,OAAOjC,GAEpC8B,EAAMnC,UAAUhD,OAAO,YAAa,cACpCmF,EAAMI,iBAAiB,2BAA2BC,QAASC,GAAOA,EAAGzC,UAAUhD,OAAO,aAAc,cAEpGmF,EAAME,gBAAgB,YACtBF,EAAMI,iBAAiB,cAAcC,QAASC,GAAOA,EAAGJ,gBAAgB,aAKxE7G,KAAKoB,cAAgBuF,EAAMC,WAAU,GAErC5G,KAAKiB,WAAWiG,IAAIrC,EAAO8B,GACpBA,CACT,CA0BA,eAAAQ,CAAwBtC,GACtB,MAAMuC,EAAWpH,KAAKoB,cAChBkC,EAAMtD,KAAKwD,KAAKqB,GACtB,IAAKuC,GAAmB,MAAP9D,EAAa,OAAO,KAErC,MAAM2D,EAAKG,EAASR,WAAU,GACxBS,EAAQC,MAAMC,KAAKN,EAAGF,iBAA8B,UAC1D,GAAIM,EAAMxE,KAAM2E,GAAmC,MAA1BA,EAAKC,mBAA4B,OAAO,KAEjER,EAAG1C,QAAQ,UAAeuC,OAAOjC,GAGjCoC,EAAG1C,QAAQ,mBAAwB,GACnC0C,EAAGJ,gBAAgB,iBAEnB,MAAMa,EAAU1H,KAAK2H,eACrB,IAAK,MAAMH,KAAQH,EAAO,CACxBG,EAAKlD,aAAa,WAAYwC,OAAOjC,IACrC,MAAMxB,EAAQmE,EAAKI,aAAa,cAC1BC,EAAmCxE,EAAQqE,EAAQI,KAAMC,GAAMA,EAAE1E,QAAUA,QAAS,EACpF/C,EAAQuH,GAAA,EAASzI,EAAAA,kBAAiBkE,EAAKuE,EAAQhD,GAAS,KAC9D2C,EAAKS,YAAuB,MAAT3H,EAAgB,GAAKwG,OAAOxG,EACjD,CACA,OAAO2G,CACT,CAKA,qBAAAnF,GACE,GAAK9B,KAAKmB,iBAAiBoB,OAC3B,IAAK,MAAMO,KAAO9C,KAAKmB,iBAErB,GADanB,KAAK+C,gBAAgBD,GACxB,CAER,MAAMoF,EAAQlI,KAAKwG,WAAW1D,GACxBqF,EAAgBnI,KAAKe,WAAWkD,cAAc,qBAAqBnB,OACrEoF,GAASC,GAAiBA,IAAkBD,GAC9CC,EAAcC,YAAYF,EAE9B,CAEJ,CAMA,eAAArG,GACE,GAAK7B,KAAKgB,cAAcuB,OACxB,IAAK,MAAMO,KAAO9C,KAAKgB,cACjBhB,KAAK+C,gBAAgBD,IAEvB9C,KAAKwG,WAAW1D,EAGtB,CAGA,cAAAf,CAAuBsG,GACrB,IAAKrI,KAAKe,UAAW,OAErB,MAAMqB,EAAYiG,GAAqBrI,KAAKsF,uBACpCW,QAASqC,EAAApC,WAASA,GAAelG,KAAKyF,eAAerD,GAU7D,GAPIpC,KAAKe,UAAUwD,QAAQ,OAAYvE,KAAKmD,OAAOvC,OACjDZ,KAAKe,UAAUwD,QAAQ,KAAUvE,KAAKmD,OAAOvC,MAAQ,QAIrD0H,EAAQ/F,SAAWvC,KAAKmB,iBAAiBoB,SAAU+F,EAAQC,MAAA,CAAO1E,EAAGF,IAAME,IAAM7D,KAAKmB,iBAAiBwC,IAE3F,CAGZ,MAAM6E,EAAWrE,SAASsE,yBACpBC,EAAqB,GAC3B,IAAK,MAAM5F,KAAOwF,EAAS,CACzB,IAAI3B,EAAQ3G,KAAKiB,WAAWyF,IAAI5D,IAAQ,KAGxC6D,EADc3G,KAAKwG,WAAW1D,IACb6D,EACZA,IAKHA,EAAQ3G,KAAKmH,gBAAgBrE,GACzB6D,GAAO3G,KAAKiB,WAAWiG,IAAIpE,EAAK6D,IAElCA,IACF6B,EAASG,YAAYhC,GACrB+B,EAAS9E,KAAKd,GAElB,CACA9C,KAAKe,UAAU6H,gBAAgBJ,GAI/BxI,KAAKmB,iBAAmBuH,CAC1B,CAMIxC,IAAelG,KAAKqB,iBACtBrB,KAAKe,UAAU8H,MAAMC,UAAY5C,EAAa,EAAI,eAAeA,OAAkB,GACnFlG,KAAKqB,eAAiB6E,EAE1B,CAGA,cAAAH,CAAuBgD,GACrB,IAAK,MAAMjG,KAAO9C,KAAKgB,cACrB,GAAI8B,EAAMiG,EAAS,OAAOjG,EAE5B,OAAO,IACT,G"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip.umd.js","names":[],"sources":["../../../../../libs/grid/src/lib/plugins/shared/cell-text.ts","../../../../../libs/grid/src/lib/plugins/tooltip/tooltip.css?inline","../../../../../libs/grid/src/lib/plugins/tooltip/tooltip-plugin.ts"],"sourcesContent":["/**\n * Displayed text of a body cell, excluding a custom `loadingRenderer` overlay that lives inside\n * the cell (`aria-hidden` does not remove it from `textContent`). Only text and element children\n * are read, matching `textContent`, which skips comment nodes (e.g. Angular's anchors).\n * @internal\n */\nexport function readCellDisplayText(cell: HTMLElement): string {\n const loader = cell.querySelector(':scope > .tbw-cell-loading-overlay');\n if (!loader) return cell.textContent?.trim() ?? '';\n let text = '';\n for (const node of cell.childNodes) {\n if (node === loader) continue;\n if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE) text += node.textContent ?? '';\n }\n return text.trim();\n}\n","@layer tbw-plugins {\n .tbw-tooltip-popover {\n /* Reset browser popover defaults — overflow:visible set inline for UA specificity */\n margin: 0;\n padding: var(--tbw-tooltip-padding, var(--tbw-spacing-sm, 0.375rem) var(--tbw-spacing-md, 0.5rem));\n background: var(--tbw-tooltip-bg, light-dark(#333338, #484850));\n color: var(--tbw-tooltip-fg, light-dark(#f5f5f5, #f0f0f0));\n border: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n border-radius: var(--tbw-tooltip-radius, var(--tbw-border-radius, 0.25rem));\n box-shadow: none;\n filter: var(--tbw-tooltip-shadow, drop-shadow(0 4px 4px rgba(0, 0, 0, 0.45)));\n font-family: var(--tbw-font-family, system-ui, sans-serif);\n font-size: var(--tbw-tooltip-font-size, var(--tbw-font-size-sm, 0.9285em));\n max-width: var(--tbw-tooltip-max-width, 300px);\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n /* WCAG 2.2 SC 1.4.13 \"Hoverable\": the pointer must be able to rest on the\n tooltip without dismissing it, so long or truncated content can be read,\n magnified, and selected. The plugin's grace-period hide covers the gap\n between the cell and the popover. */\n pointer-events: auto;\n user-select: text;\n z-index: 10001;\n }\n\n /* Multi-line content (text contains `\\n`): preserve author line breaks\n verbatim and let the popover grow horizontally instead of wrapping\n mid-line. `max-width` is lifted so long lines aren't broken. */\n .tbw-tooltip-popover.tbw-tooltip-multiline {\n white-space: pre;\n max-width: none;\n word-wrap: normal;\n overflow-wrap: normal;\n }\n\n /* Arrow — a 14×14 box rotated 45° to form a diamond, with its center\n pinned to the popover edge. Without `clip-path` the diamond's inner\n half sits *inside* the popover, painting `background: inherit` over\n the first ~7px of text content. The clip-path keeps only the\n pre-rotation triangle whose three corners rotate to the outside of\n the popover (the visible \"V\"), so the inner half is removed before\n it can overlap the content. clip-path is applied in the element's\n own (pre-transform) coordinate space per the CSS Transforms spec,\n which is why the polygon is expressed in the unrotated square. */\n .tbw-tooltip-popover::after {\n content: '';\n position: absolute;\n width: var(--tbw-tooltip-arrow-size, 14px);\n height: var(--tbw-tooltip-arrow-size, 14px);\n left: var(--tbw-tooltip-arrow-offset, 16px);\n background: inherit;\n pointer-events: none;\n }\n\n /* Arrow below-anchor default: diamond points up from the top edge.\n Pre-rotation: top-left, top-right, bottom-left → after rotate(45deg)\n these become the top, right, and left points of the diamond, i.e.\n the upper \"V\" that pokes above the popover. The bottom-right corner\n (which would land inside the popover) is clipped away. */\n .tbw-tooltip-popover:not(.tbw-tooltip-above)::after {\n top: calc(var(--tbw-tooltip-arrow-size, 14px) / -2);\n transform: rotate(45deg);\n border-top: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n border-left: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n clip-path: polygon(0 0, 100% 0, 0 100%);\n }\n\n /* Above-anchor: diamond points down from the bottom edge. Mirror of\n the below-anchor clip — keep the corners (top-right, bottom-right,\n bottom-left) that rotate to the right, bottom, and left points of\n the diamond, i.e. the lower \"Λ\" that pokes below the popover. */\n .tbw-tooltip-popover.tbw-tooltip-above::after {\n bottom: calc(var(--tbw-tooltip-arrow-size, 14px) / -2);\n transform: rotate(45deg);\n border-bottom: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n border-right: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n clip-path: polygon(100% 0, 100% 100%, 0 100%);\n }\n\n /* CSS Anchor Positioning — modern browsers.\n Placement is chosen in JS (`#prefersAbove`) rather than by\n `position-try-fallbacks`: the browser's flip is invisible to script, so the\n arrow-direction class could disagree with where the popover was painted —\n most visibly when re-hovering while a previous tooltip is still open. */\n @supports (anchor-name: --test) {\n .tbw-tooltip-popover {\n position-anchor: --tbw-tooltip-anchor;\n top: calc(anchor(bottom) + 11px);\n bottom: auto;\n left: anchor(left);\n }\n\n .tbw-tooltip-popover.tbw-tooltip-above {\n top: auto;\n bottom: calc(anchor(top) + 11px);\n }\n }\n}\n","/**\n * Tooltip Plugin\n *\n * Shows styled popover tooltips on header and data cells when text\n * overflows (ellipsis). Uses the Popover API (`popover=\"hint\"`) with\n * CSS anchor positioning for consistent, themed placement.\n *\n * Supports per-column overrides via `cellTooltip` and `headerTooltip`\n * on column config.\n */\n\nimport type { GridElement, PluginManifest } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { CellRenderContext, ColumnConfig, HeaderLabelContext } from '../../core/types';\nimport { readCellDisplayText } from '../shared/cell-text';\nimport tooltipStyles from './tooltip.css?inline';\nimport type { TooltipConfig } from './types';\n\n// #region Helpers\n\n/** Check if an element's text content overflows its visible width. */\nfunction isOverflowing(el: HTMLElement): boolean {\n return el.scrollWidth > el.clientWidth;\n}\n\n/**\n * True when a delegated pointer event merely crossed a child boundary *inside*\n * `cell` rather than entering or leaving it.\n *\n * `mouseover`/`mouseout` fire on every element boundary and bubble, so a\n * delegated handler sees one event per child. `relatedTarget` is what recovers\n * `mouseenter`/`mouseleave` semantics from that stream.\n */\nfunction isSameCellCrossing(cell: HTMLElement, related: EventTarget | null): boolean {\n const node = related as Node | null;\n return !!node && cell.contains(node);\n}\n\n/**\n * Resolve the tooltip text for a cell.\n * Returns the text to show, or `null` to suppress.\n */\nfunction resolveCellTooltip(\n column: ColumnConfig,\n cell: HTMLElement,\n row: unknown,\n value: unknown,\n grid?: GridElement,\n): string | null {\n const spec = column.cellTooltip;\n\n if (spec === false) return null;\n if (typeof spec === 'string') return spec;\n if (typeof spec === 'function') {\n const ctx: CellRenderContext = { value, row, column, field: column.field, grid: grid as any };\n return spec(ctx);\n }\n\n // Default: show textContent only when overflowing\n if (isOverflowing(cell)) {\n return readCellDisplayText(cell) || null;\n }\n\n return null;\n}\n\n/**\n * Resolve the tooltip text for a header cell.\n * Returns the text to show, or `null` to suppress.\n */\nfunction resolveHeaderTooltip(column: ColumnConfig, headerCell: HTMLElement): string | null {\n const spec = column.headerTooltip;\n\n if (spec === false) return null;\n if (typeof spec === 'string') return spec;\n if (typeof spec === 'function') {\n const ctx: HeaderLabelContext = {\n column,\n value: column.header ?? column.field,\n };\n return spec(ctx);\n }\n\n // Default: show header text only when overflowing\n const labelSpan = headerCell.querySelector('span:first-child') as HTMLElement | null;\n const target = labelSpan ?? headerCell;\n\n if (isOverflowing(target)) {\n return target.textContent?.trim() || null;\n }\n\n return null;\n}\n\n/** Runtime check — happy-dom and older browsers may lack Popover API. */\nfunction supportsPopover(): boolean {\n return typeof HTMLElement.prototype?.showPopover === 'function';\n}\n\n/** Runtime check for CSS anchor positioning. */\nfunction supportsAnchor(): boolean {\n return typeof CSS !== 'undefined' && CSS.supports?.('anchor-name', '--test') === true;\n}\n\n/** Keys that move the virtual cell focus and should therefore re-target the tooltip. */\nconst NAVIGATION_KEYS = new Set([\n 'ArrowUp',\n 'ArrowDown',\n 'ArrowLeft',\n 'ArrowRight',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n 'Tab',\n]);\n\n/** Default grace period (ms) allowing the pointer to travel onto the tooltip. */\nconst DEFAULT_HIDE_DELAY_MS = 120;\n\n/** Gap (px) between the anchor cell and the popover, leaving room for the arrow. */\nconst ARROW_GAP_PX = 11;\n\n/** `id` of the shared popover, referenced by `aria-describedby` on the anchor. */\nconst POPOVER_ID = 'tbw-tooltip-popover';\n// #endregion\n\n// #region TooltipPlugin\n/**\n * Tooltip Plugin for tbw-grid\n *\n * Shows styled popover tooltips when header or cell text overflows its\n * container. Uses the Popover API with CSS anchor positioning for\n * consistent themed appearance across light and dark modes.\n *\n * ## Installation\n *\n * ```ts\n * import { TooltipPlugin } from '@toolbox-web/grid/plugins/tooltip';\n * ```\n *\n * @example Default — auto-tooltip on overflow\n * ```ts\n * grid.gridConfig = {\n * plugins: [new TooltipPlugin()],\n * };\n * ```\n *\n * @example Header-only tooltips\n * ```ts\n * grid.gridConfig = {\n * plugins: [new TooltipPlugin({ cell: false })],\n * };\n * ```\n *\n * @example Per-column overrides\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', cellTooltip: (ctx) => `${ctx.row.first} ${ctx.row.last}` },\n * { field: 'actions', cellTooltip: false },\n * { field: 'revenue', headerTooltip: 'Total revenue in USD (before tax)' },\n * ],\n * plugins: [new TooltipPlugin()],\n * };\n * ```\n *\n * ## Accessibility\n *\n * Conforms to WCAG 2.2 SC 1.4.13 (Content on Hover or Focus) out of the box:\n * the tooltip is **dismissible** with `Escape`, **hoverable** (the pointer can\n * rest on it to read or copy the text), **persistent** (never hidden on a\n * timer), and appears on **keyboard focus** as cell focus moves. Pointer\n * interaction never triggers the focus path, so mouse behaviour is unchanged.\n * See {@link TooltipConfig.focus} and {@link TooltipConfig.hideDelay} to tune it.\n *\n * @category Plugins\n * @since 1.28.0\n */\nexport class TooltipPlugin extends BaseGridPlugin<TooltipConfig> {\n readonly name = 'tooltip';\n override readonly styles = tooltipStyles;\n\n static override readonly manifest: PluginManifest<TooltipConfig> = {\n ownedProperties: [\n { property: 'cellTooltip', level: 'column', description: 'the \"cellTooltip\" column property' },\n { property: 'headerTooltip', level: 'column', description: 'the \"headerTooltip\" column property' },\n ],\n configRules: [],\n };\n\n /** The shared popover element for all tooltips. */\n #popoverEl: HTMLElement | null = null;\n\n /** The cell currently acting as CSS anchor. */\n #anchorCell: HTMLElement | null = null;\n\n /** Whether delegated listeners are bound. */\n #bound = false;\n\n /** Pending grace-period hide, cancelled when the pointer enters the tooltip. */\n #hideTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** Pending focus-driven retarget, so rapid arrow-key repeats coalesce. */\n #focusFrame: number | null = null;\n\n /** Whether header tooltips are enabled globally. */\n get #headerEnabled(): boolean {\n return this.config.header !== false;\n }\n\n /** Whether cell tooltips are enabled globally. */\n get #cellEnabled(): boolean {\n return this.config.cell !== false;\n }\n\n /** Whether keyboard focus shows a tooltip (WCAG 2.2 SC 1.4.13). */\n get #focusEnabled(): boolean {\n return this.config.focus !== false;\n }\n\n /** Grace period before hiding, allowing the pointer to reach the tooltip. */\n get #hideDelay(): number {\n return this.config.hideDelay ?? DEFAULT_HIDE_DELAY_MS;\n }\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n }\n\n override detach(): void {\n this.#cancelPendingHide();\n if (this.#focusFrame !== null) {\n cancelAnimationFrame(this.#focusFrame);\n this.#focusFrame = null;\n }\n this.#hideTooltip();\n this.#popoverEl?.remove();\n this.#popoverEl = null;\n this.#bound = false;\n super.detach();\n }\n\n override afterRender(): void {\n this.#ensurePopover();\n this.#bindEvents();\n }\n\n // #region Popover Lifecycle\n\n /** Create the shared popover element (once). */\n #ensurePopover(): void {\n if (this.#popoverEl) return;\n const el = document.createElement('div');\n el.className = 'tbw-tooltip-popover';\n el.setAttribute('popover', 'hint');\n el.id = POPOVER_ID;\n el.setAttribute('role', 'tooltip');\n // Override UA popover defaults that @layer CSS cannot beat\n el.style.overflow = 'visible';\n el.style.margin = '0';\n document.body.appendChild(el);\n this.#popoverEl = el;\n\n // SC 1.4.13 \"Hoverable\" — the pointer must be able to rest on the tooltip\n // without dismissing it, so its content can be read, magnified, or copied.\n el.addEventListener('mouseenter', () => this.#cancelPendingHide(), { signal: this.disconnectSignal });\n el.addEventListener('mouseleave', () => this.#scheduleHide(), { signal: this.disconnectSignal });\n // Scrolling slides the anchored popover under a stationary cursor; without this the\n // popover (in <body>) would swallow every wheel tick and the grid would stop scrolling.\n el.addEventListener('wheel', (e) => this.#forwardWheel(e), { passive: false, signal: this.disconnectSignal });\n }\n\n /** Dismiss the tooltip and re-dispatch the wheel on its anchor cell so the grid scrolls. */\n #forwardWheel(e: WheelEvent): void {\n const target = this.#anchorCell;\n this.#hideTooltip();\n if (!target?.isConnected) return;\n const forwarded = new WheelEvent('wheel', {\n deltaX: e.deltaX,\n deltaY: e.deltaY,\n deltaZ: e.deltaZ,\n deltaMode: e.deltaMode,\n clientX: e.clientX,\n clientY: e.clientY,\n shiftKey: e.shiftKey,\n ctrlKey: e.ctrlKey,\n altKey: e.altKey,\n metaKey: e.metaKey,\n bubbles: true,\n cancelable: true,\n composed: true,\n });\n if (!target.dispatchEvent(forwarded)) e.preventDefault();\n }\n\n /** Show the popover anchored to `cell` with the given `text`. */\n #showTooltip(cell: HTMLElement, text: string): void {\n if (!this.#popoverEl) return;\n this.#cancelPendingHide();\n\n // Move the CSS anchor to the hovered cell\n this.#clearAnchor();\n cell.style.setProperty('anchor-name', '--tbw-tooltip-anchor');\n cell.setAttribute('aria-describedby', POPOVER_ID);\n this.#anchorCell = cell;\n\n // Set content (always textContent — safe, no XSS).\n // Multi-line content uses `white-space: pre` so author-supplied line\n // breaks survive verbatim and the popover grows to fit; single-line\n // content keeps `pre-wrap` so long values wrap inside `max-width`.\n this.#popoverEl.textContent = text;\n this.#popoverEl.classList.toggle('tbw-tooltip-multiline', text.includes('\\n'));\n\n // Show via Popover API\n if (supportsPopover()) {\n try {\n this.#popoverEl.showPopover();\n } catch {\n /* already shown */\n }\n }\n\n if (supportsAnchor()) {\n this.#popoverEl.classList.toggle('tbw-tooltip-above', this.#prefersAbove(cell));\n } else {\n this.#positionFallback(cell);\n }\n }\n\n /** Hide the popover and clear the anchor reference. */\n #hideTooltip(): void {\n this.#cancelPendingHide();\n if (this.#popoverEl) {\n if (supportsPopover()) {\n try {\n this.#popoverEl.hidePopover();\n } catch {\n /* already hidden */\n }\n }\n this.#popoverEl.classList.remove('tbw-tooltip-above');\n }\n this.#clearAnchor();\n }\n\n /** Whether the popover is currently anchored to a cell (i.e. visible). */\n get #isVisible(): boolean {\n return this.#anchorCell !== null;\n }\n\n /**\n * Hide after the configured grace period rather than immediately, so the\n * pointer can travel from the cell onto the tooltip (SC 1.4.13 Hoverable).\n */\n #scheduleHide(): void {\n this.#cancelPendingHide();\n if (this.#hideDelay <= 0) {\n this.#hideTooltip();\n return;\n }\n this.#hideTimer = setTimeout(() => {\n this.#hideTimer = null;\n this.#hideTooltip();\n }, this.#hideDelay);\n }\n\n #cancelPendingHide(): void {\n if (this.#hideTimer !== null) {\n clearTimeout(this.#hideTimer);\n this.#hideTimer = null;\n }\n }\n\n /** Remove the CSS anchor-name from the previous cell, but only if it's still our tooltip anchor. */\n #clearAnchor(): void {\n if (this.#anchorCell) {\n if (this.#anchorCell.style.getPropertyValue('anchor-name') === '--tbw-tooltip-anchor') {\n this.#anchorCell.style.removeProperty('anchor-name');\n }\n if (this.#anchorCell.getAttribute('aria-describedby') === POPOVER_ID) {\n this.#anchorCell.removeAttribute('aria-describedby');\n }\n this.#anchorCell = null;\n }\n }\n\n /**\n * Decide whether the popover sits above the cell. Above is preferred so the\n * tooltip never covers the rows the pointer is heading toward; it drops below\n * only when the popover does not fit in the space above the cell.\n *\n * Placement is resolved here rather than left to `position-try-fallbacks` so\n * the arrow-direction class can never disagree with where the browser\n * actually painted the popover.\n */\n #prefersAbove(cell: HTMLElement): boolean {\n if (!this.#popoverEl) return false;\n const cellRect = cell.getBoundingClientRect();\n const spaceAbove = cellRect.top - ARROW_GAP_PX;\n const spaceBelow = window.innerHeight - cellRect.bottom - ARROW_GAP_PX;\n // Popover is already shown and filled, so its box is measurable.\n const height = this.#popoverEl.offsetHeight;\n return height <= spaceAbove || spaceAbove >= spaceBelow;\n }\n\n /**\n * Fallback positioning for browsers without CSS anchor support.\n * Places the popover above or below the cell using fixed coordinates.\n */\n #positionFallback(cell: HTMLElement): void {\n if (!this.#popoverEl) return;\n const cellRect = cell.getBoundingClientRect();\n const above = this.#prefersAbove(cell);\n\n this.#popoverEl.style.position = 'fixed';\n this.#popoverEl.style.left = `${cellRect.left}px`;\n\n if (above) {\n this.#popoverEl.style.top = '';\n this.#popoverEl.style.bottom = `${window.innerHeight - cellRect.top + ARROW_GAP_PX}px`;\n } else {\n this.#popoverEl.style.top = `${cellRect.bottom + ARROW_GAP_PX}px`;\n this.#popoverEl.style.bottom = '';\n }\n this.#popoverEl.classList.toggle('tbw-tooltip-above', above);\n }\n // #endregion\n\n // #region Event Delegation\n\n /** Bind delegated mouseover/mouseout once. */\n #bindEvents(): void {\n if (this.#bound) return;\n const container = this.gridElement?.querySelector('.tbw-grid-root');\n if (!container) return;\n\n this.#bound = true;\n\n container.addEventListener('mouseover', (e: Event) => this.#onMouseOver(e as MouseEvent), {\n signal: this.disconnectSignal,\n });\n\n container.addEventListener('mouseout', (e: Event) => this.#onMouseOut(e as MouseEvent), {\n signal: this.disconnectSignal,\n });\n\n // SC 1.4.13 \"Dismissible\" — Escape must hide the tooltip without moving the\n // pointer. Bound on `document` because a hover-triggered tooltip can be\n // visible while focus sits entirely outside the grid, where the grid's own\n // key handling never runs. The event is deliberately NOT consumed: Escape\n // also cancels an in-progress edit, and swallowing it here would break that.\n document.addEventListener('keydown', (e: Event) => this.#onKeyDownGlobal(e as KeyboardEvent), {\n capture: true,\n signal: this.disconnectSignal,\n });\n\n // Keyboard navigation keeps DOM focus on the grid HOST (`tabindex=0`), so\n // keydown never travels through `.tbw-grid-root` — a child of the host.\n // Bind on the host, exactly where `setupRootEventDelegation` binds the\n // grid's own navigation handler.\n this.gridElement.addEventListener('keydown', (e: Event) => this.#onKeyDownGrid(e as KeyboardEvent), {\n signal: this.disconnectSignal,\n });\n }\n\n #onMouseOver(e: MouseEvent): void {\n const target = e.target as HTMLElement;\n if (!target?.closest) return;\n\n // Check for header cell\n const headerCell = target.closest('[part~=\"header-cell\"]') as HTMLElement | null;\n if (headerCell && this.#headerEnabled) {\n // Re-showing on every child boundary would re-measure (scrollWidth,\n // getBoundingClientRect) and re-anchor the popover, thrashing layout.\n if (!isSameCellCrossing(headerCell, e.relatedTarget)) {\n this.#showHeaderTooltip(headerCell);\n }\n return;\n }\n\n // Check for data cell — skip cells that already have a CSS anchor (e.g. overlay editors)\n // to avoid overwriting their anchor-name and breaking their positioning.\n const dataCell = target.closest('[data-row][data-col]') as HTMLElement | null;\n if (dataCell && this.#cellEnabled && !dataCell.style.getPropertyValue('anchor-name')) {\n if (isSameCellCrossing(dataCell, e.relatedTarget)) return;\n this.#showCellTooltip(dataCell);\n }\n }\n\n #onMouseOut(e: MouseEvent): void {\n const target = e.target as HTMLElement;\n if (!target?.closest) return;\n\n const cell = target.closest('[part~=\"header-cell\"], [data-row][data-col]') as HTMLElement | null;\n if (!cell) return;\n\n // Keep tooltip if pointer moved to a child still inside the same cell\n if (isSameCellCrossing(cell, e.relatedTarget)) return;\n\n // Grace period, not an immediate hide, so the pointer can reach the tooltip.\n this.#scheduleHide();\n }\n\n /** Escape dismisses a visible tooltip from anywhere on the page. */\n #onKeyDownGlobal(e: KeyboardEvent): void {\n if (e.key === 'Escape' && this.#isVisible) {\n this.#hideTooltip();\n }\n }\n\n /**\n * Retarget the tooltip after keyboard navigation moves the virtual cell\n * focus (SC 1.4.13 \"focus\" trigger). Only key-driven focus qualifies —\n * pointer interaction never reaches here, so mouse users are unaffected.\n */\n #onKeyDownGrid(e: KeyboardEvent): void {\n if (!this.#focusEnabled || !this.#cellEnabled) return;\n if (e.key === 'Escape') return;\n if (!NAVIGATION_KEYS.has(e.key)) {\n // Any other key (typing, Enter to edit) means the user has moved on.\n this.#hideTooltip();\n return;\n }\n // Cell focus is virtual and moves in the grid's own keydown handler, which\n // runs after this one — read the resulting `.cell-focus` on the next frame.\n if (this.#focusFrame !== null) cancelAnimationFrame(this.#focusFrame);\n this.#focusFrame = requestAnimationFrame(() => {\n this.#focusFrame = null;\n this.#syncTooltipToFocusedCell();\n });\n }\n\n /** Show (or clear) the tooltip for the cell that currently holds focus. */\n #syncTooltipToFocusedCell(): void {\n const cell = this.gridElement?.querySelector('.cell-focus') as HTMLElement | null;\n if (!cell || !cell.hasAttribute('data-row')) {\n this.#hideTooltip();\n return;\n }\n // Focus was clamped rather than moved (ArrowUp on the first row, ArrowDown\n // on the last, …) — the tooltip already describes this cell.\n if (cell === this.#anchorCell) return;\n // A foreign anchor means an overlay editor owns this cell's positioning, so\n // we cannot re-anchor here; drop the previous cell's tooltip rather than\n // leave it pointing somewhere focus has left.\n if (cell.style.getPropertyValue('anchor-name')) {\n this.#hideTooltip();\n return;\n }\n const before = this.#anchorCell;\n this.#showCellTooltip(cell);\n // No tooltip text for this cell — drop whatever the previous cell showed.\n if (this.#anchorCell === before) this.#hideTooltip();\n }\n // #endregion\n\n // #region Tooltip Resolution\n\n #showHeaderTooltip(headerCell: HTMLElement): void {\n const colIndex = parseInt(headerCell.getAttribute('data-col') ?? '-1', 10);\n if (colIndex < 0) return;\n\n const column = this.visibleColumns[colIndex];\n if (!column) return;\n\n const text = resolveHeaderTooltip(column, headerCell);\n if (text) {\n this.#showTooltip(headerCell, text);\n }\n }\n\n #showCellTooltip(cell: HTMLElement): void {\n const rowIndex = parseInt(cell.getAttribute('data-row') ?? '-1', 10);\n const colIndex = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n if (rowIndex < 0 || colIndex < 0) return;\n\n const column = this.visibleColumns[colIndex];\n if (!column) return;\n\n const row = this.rows[rowIndex];\n const value = row?.[column.field as keyof typeof row];\n\n const text = resolveCellTooltip(column, cell, row, value, this.grid);\n if (text) {\n this.#showTooltip(cell, text);\n }\n }\n // #endregion\n}\n// #endregion\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tooltip.umd.js","names":["BaseGridPlugin","tooltipStyles","global","factory","exports","module","require","define","amd","globalThis","self","TbwGridPlugin_tooltip","TbwGrid","this","_toolbox_web_grid","Object","defineProperty","Symbol","toStringTag","value","isOverflowing","el","scrollWidth","clientWidth","isSameCellCrossing","cell","related","node","contains","resolveCellTooltip","column","row","grid","spec","cellTooltip","field","loader","querySelector","textContent","trim","text","childNodes","nodeType","Node","TEXT_NODE","ELEMENT_NODE","readCellDisplayText","supportsPopover","HTMLElement","prototype","showPopover","NAVIGATION_KEYS","Set","POPOVER_ID","TooltipPlugin","BaseGridPlugin","name","styles","static","ownedProperties","property","level","description","configRules","popoverEl","anchorCell","bound","hideTimer","focusFrame","headerEnabled","config","header","cellEnabled","focusEnabled","focus","hideDelay","attach","super","detach","cancelPendingHide","cancelAnimationFrame","hideTooltip","remove","afterRender","ensurePopover","bindEvents","document","createElement","className","setAttribute","id","style","overflow","margin","body","appendChild","addEventListener","signal","disconnectSignal","scheduleHide","e","forwardWheel","passive","target","isConnected","forwarded","WheelEvent","deltaX","deltaY","deltaZ","deltaMode","clientX","clientY","shiftKey","ctrlKey","altKey","metaKey","bubbles","cancelable","composed","dispatchEvent","preventDefault","showTooltip","clearAnchor","setProperty","classList","toggle","includes","CSS","supports","prefersAbove","positionFallback","hidePopover","isVisible","setTimeout","clearTimeout","getPropertyValue","removeProperty","getAttribute","removeAttribute","cellRect","getBoundingClientRect","spaceAbove","top","spaceBelow","window","innerHeight","bottom","offsetHeight","above","position","left","container","gridElement","onMouseOver","onMouseOut","onKeyDownGlobal","capture","onKeyDownGrid","closest","headerCell","relatedTarget","showHeaderTooltip","dataCell","showCellTooltip","key","has","requestAnimationFrame","syncTooltipToFocusedCell","hasAttribute","before","colIndex","parseInt","visibleColumns","headerTooltip","resolveHeaderTooltip","rowIndex","rows"],"sources":["../../../../../libs/grid/src/lib/plugins/shared/cell-text.ts","../../../../../libs/grid/src/lib/plugins/tooltip/tooltip.css?inline","../../../../../libs/grid/src/lib/plugins/tooltip/tooltip-plugin.ts"],"sourcesContent":["/**\n * Displayed text of a body cell, excluding a custom `loadingRenderer` overlay that lives inside\n * the cell (`aria-hidden` does not remove it from `textContent`). Only text and element children\n * are read, matching `textContent`, which skips comment nodes (e.g. Angular's anchors).\n * @internal\n */\nexport function readCellDisplayText(cell: HTMLElement): string {\n const loader = cell.querySelector(':scope > .tbw-cell-loading-overlay');\n if (!loader) return cell.textContent?.trim() ?? '';\n let text = '';\n for (const node of cell.childNodes) {\n if (node === loader) continue;\n if (node.nodeType === Node.TEXT_NODE || node.nodeType === Node.ELEMENT_NODE) text += node.textContent ?? '';\n }\n return text.trim();\n}\n","@layer tbw-plugins {\n .tbw-tooltip-popover {\n /* Reset browser popover defaults — overflow:visible set inline for UA specificity */\n margin: 0;\n padding: var(--tbw-tooltip-padding, var(--tbw-spacing-sm, 0.375rem) var(--tbw-spacing-md, 0.5rem));\n background: var(--tbw-tooltip-bg, light-dark(#333338, #484850));\n color: var(--tbw-tooltip-fg, light-dark(#f5f5f5, #f0f0f0));\n border: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n border-radius: var(--tbw-tooltip-radius, var(--tbw-border-radius, 0.25rem));\n box-shadow: none;\n filter: var(--tbw-tooltip-shadow, drop-shadow(0 4px 4px rgba(0, 0, 0, 0.45)));\n font-family: var(--tbw-font-family, system-ui, sans-serif);\n font-size: var(--tbw-tooltip-font-size, var(--tbw-font-size-sm, 0.9285em));\n max-width: var(--tbw-tooltip-max-width, 300px);\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n /* WCAG 2.2 SC 1.4.13 \"Hoverable\": the pointer must be able to rest on the\n tooltip without dismissing it, so long or truncated content can be read,\n magnified, and selected. The plugin's grace-period hide covers the gap\n between the cell and the popover. */\n pointer-events: auto;\n user-select: text;\n z-index: 10001;\n }\n\n /* Multi-line content (text contains `\\n`): preserve author line breaks\n verbatim and let the popover grow horizontally instead of wrapping\n mid-line. `max-width` is lifted so long lines aren't broken. */\n .tbw-tooltip-popover.tbw-tooltip-multiline {\n white-space: pre;\n max-width: none;\n word-wrap: normal;\n overflow-wrap: normal;\n }\n\n /* Arrow — a 14×14 box rotated 45° to form a diamond, with its center\n pinned to the popover edge. Without `clip-path` the diamond's inner\n half sits *inside* the popover, painting `background: inherit` over\n the first ~7px of text content. The clip-path keeps only the\n pre-rotation triangle whose three corners rotate to the outside of\n the popover (the visible \"V\"), so the inner half is removed before\n it can overlap the content. clip-path is applied in the element's\n own (pre-transform) coordinate space per the CSS Transforms spec,\n which is why the polygon is expressed in the unrotated square. */\n .tbw-tooltip-popover::after {\n content: '';\n position: absolute;\n width: var(--tbw-tooltip-arrow-size, 14px);\n height: var(--tbw-tooltip-arrow-size, 14px);\n left: var(--tbw-tooltip-arrow-offset, 16px);\n background: inherit;\n pointer-events: none;\n }\n\n /* Arrow below-anchor default: diamond points up from the top edge.\n Pre-rotation: top-left, top-right, bottom-left → after rotate(45deg)\n these become the top, right, and left points of the diamond, i.e.\n the upper \"V\" that pokes above the popover. The bottom-right corner\n (which would land inside the popover) is clipped away. */\n .tbw-tooltip-popover:not(.tbw-tooltip-above)::after {\n top: calc(var(--tbw-tooltip-arrow-size, 14px) / -2);\n transform: rotate(45deg);\n border-top: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n border-left: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n clip-path: polygon(0 0, 100% 0, 0 100%);\n }\n\n /* Above-anchor: diamond points down from the bottom edge. Mirror of\n the below-anchor clip — keep the corners (top-right, bottom-right,\n bottom-left) that rotate to the right, bottom, and left points of\n the diamond, i.e. the lower \"Λ\" that pokes below the popover. */\n .tbw-tooltip-popover.tbw-tooltip-above::after {\n bottom: calc(var(--tbw-tooltip-arrow-size, 14px) / -2);\n transform: rotate(45deg);\n border-bottom: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n border-right: 1px solid var(--tbw-tooltip-border, light-dark(#222226, #6a6a72));\n clip-path: polygon(100% 0, 100% 100%, 0 100%);\n }\n\n /* CSS Anchor Positioning — modern browsers.\n Placement is chosen in JS (`#prefersAbove`) rather than by\n `position-try-fallbacks`: the browser's flip is invisible to script, so the\n arrow-direction class could disagree with where the popover was painted —\n most visibly when re-hovering while a previous tooltip is still open. */\n @supports (anchor-name: --test) {\n .tbw-tooltip-popover {\n position-anchor: --tbw-tooltip-anchor;\n top: calc(anchor(bottom) + 11px);\n bottom: auto;\n left: anchor(left);\n }\n\n .tbw-tooltip-popover.tbw-tooltip-above {\n top: auto;\n bottom: calc(anchor(top) + 11px);\n }\n }\n}\n","/**\n * Tooltip Plugin\n *\n * Shows styled popover tooltips on header and data cells when text\n * overflows (ellipsis). Uses the Popover API (`popover=\"hint\"`) with\n * CSS anchor positioning for consistent, themed placement.\n *\n * Supports per-column overrides via `cellTooltip` and `headerTooltip`\n * on column config.\n */\n\nimport type { GridElement, PluginManifest } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin } from '../../core/plugin/base-plugin';\nimport type { CellRenderContext, ColumnConfig, HeaderLabelContext } from '../../core/types';\nimport { readCellDisplayText } from '../shared/cell-text';\nimport tooltipStyles from './tooltip.css?inline';\nimport type { TooltipConfig } from './types';\n\n// #region Helpers\n\n/** Check if an element's text content overflows its visible width. */\nfunction isOverflowing(el: HTMLElement): boolean {\n return el.scrollWidth > el.clientWidth;\n}\n\n/**\n * True when a delegated pointer event merely crossed a child boundary *inside*\n * `cell` rather than entering or leaving it.\n *\n * `mouseover`/`mouseout` fire on every element boundary and bubble, so a\n * delegated handler sees one event per child. `relatedTarget` is what recovers\n * `mouseenter`/`mouseleave` semantics from that stream.\n */\nfunction isSameCellCrossing(cell: HTMLElement, related: EventTarget | null): boolean {\n const node = related as Node | null;\n return !!node && cell.contains(node);\n}\n\n/**\n * Resolve the tooltip text for a cell.\n * Returns the text to show, or `null` to suppress.\n */\nfunction resolveCellTooltip(\n column: ColumnConfig,\n cell: HTMLElement,\n row: unknown,\n value: unknown,\n grid?: GridElement,\n): string | null {\n const spec = column.cellTooltip;\n\n if (spec === false) return null;\n if (typeof spec === 'string') return spec;\n if (typeof spec === 'function') {\n const ctx: CellRenderContext = { value, row, column, field: column.field, grid: grid as any };\n return spec(ctx);\n }\n\n // Default: show textContent only when overflowing\n if (isOverflowing(cell)) {\n return readCellDisplayText(cell) || null;\n }\n\n return null;\n}\n\n/**\n * Resolve the tooltip text for a header cell.\n * Returns the text to show, or `null` to suppress.\n */\nfunction resolveHeaderTooltip(column: ColumnConfig, headerCell: HTMLElement): string | null {\n const spec = column.headerTooltip;\n\n if (spec === false) return null;\n if (typeof spec === 'string') return spec;\n if (typeof spec === 'function') {\n const ctx: HeaderLabelContext = {\n column,\n value: column.header ?? column.field,\n };\n return spec(ctx);\n }\n\n // Default: show header text only when overflowing\n const labelSpan = headerCell.querySelector('span:first-child') as HTMLElement | null;\n const target = labelSpan ?? headerCell;\n\n if (isOverflowing(target)) {\n return target.textContent?.trim() || null;\n }\n\n return null;\n}\n\n/** Runtime check — happy-dom and older browsers may lack Popover API. */\nfunction supportsPopover(): boolean {\n return typeof HTMLElement.prototype?.showPopover === 'function';\n}\n\n/** Runtime check for CSS anchor positioning. */\nfunction supportsAnchor(): boolean {\n return typeof CSS !== 'undefined' && CSS.supports?.('anchor-name', '--test') === true;\n}\n\n/** Keys that move the virtual cell focus and should therefore re-target the tooltip. */\nconst NAVIGATION_KEYS = new Set([\n 'ArrowUp',\n 'ArrowDown',\n 'ArrowLeft',\n 'ArrowRight',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n 'Tab',\n]);\n\n/** Default grace period (ms) allowing the pointer to travel onto the tooltip. */\nconst DEFAULT_HIDE_DELAY_MS = 120;\n\n/** Gap (px) between the anchor cell and the popover, leaving room for the arrow. */\nconst ARROW_GAP_PX = 11;\n\n/** `id` of the shared popover, referenced by `aria-describedby` on the anchor. */\nconst POPOVER_ID = 'tbw-tooltip-popover';\n// #endregion\n\n// #region TooltipPlugin\n/**\n * Tooltip Plugin for tbw-grid\n *\n * Shows styled popover tooltips when header or cell text overflows its\n * container. Uses the Popover API with CSS anchor positioning for\n * consistent themed appearance across light and dark modes.\n *\n * ## Installation\n *\n * ```ts\n * import { TooltipPlugin } from '@toolbox-web/grid/plugins/tooltip';\n * ```\n *\n * @example Default — auto-tooltip on overflow\n * ```ts\n * grid.gridConfig = {\n * plugins: [new TooltipPlugin()],\n * };\n * ```\n *\n * @example Header-only tooltips\n * ```ts\n * grid.gridConfig = {\n * plugins: [new TooltipPlugin({ cell: false })],\n * };\n * ```\n *\n * @example Per-column overrides\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', cellTooltip: (ctx) => `${ctx.row.first} ${ctx.row.last}` },\n * { field: 'actions', cellTooltip: false },\n * { field: 'revenue', headerTooltip: 'Total revenue in USD (before tax)' },\n * ],\n * plugins: [new TooltipPlugin()],\n * };\n * ```\n *\n * ## Accessibility\n *\n * Conforms to WCAG 2.2 SC 1.4.13 (Content on Hover or Focus) out of the box:\n * the tooltip is **dismissible** with `Escape`, **hoverable** (the pointer can\n * rest on it to read or copy the text), **persistent** (never hidden on a\n * timer), and appears on **keyboard focus** as cell focus moves. Pointer\n * interaction never triggers the focus path, so mouse behaviour is unchanged.\n * See {@link TooltipConfig.focus} and {@link TooltipConfig.hideDelay} to tune it.\n *\n * @category Plugins\n * @since 1.28.0\n */\nexport class TooltipPlugin extends BaseGridPlugin<TooltipConfig> {\n readonly name = 'tooltip';\n override readonly styles = tooltipStyles;\n\n static override readonly manifest: PluginManifest<TooltipConfig> = {\n ownedProperties: [\n { property: 'cellTooltip', level: 'column', description: 'the \"cellTooltip\" column property' },\n { property: 'headerTooltip', level: 'column', description: 'the \"headerTooltip\" column property' },\n ],\n configRules: [],\n };\n\n /** The shared popover element for all tooltips. */\n #popoverEl: HTMLElement | null = null;\n\n /** The cell currently acting as CSS anchor. */\n #anchorCell: HTMLElement | null = null;\n\n /** Whether delegated listeners are bound. */\n #bound = false;\n\n /** Pending grace-period hide, cancelled when the pointer enters the tooltip. */\n #hideTimer: ReturnType<typeof setTimeout> | null = null;\n\n /** Pending focus-driven retarget, so rapid arrow-key repeats coalesce. */\n #focusFrame: number | null = null;\n\n /** Whether header tooltips are enabled globally. */\n get #headerEnabled(): boolean {\n return this.config.header !== false;\n }\n\n /** Whether cell tooltips are enabled globally. */\n get #cellEnabled(): boolean {\n return this.config.cell !== false;\n }\n\n /** Whether keyboard focus shows a tooltip (WCAG 2.2 SC 1.4.13). */\n get #focusEnabled(): boolean {\n return this.config.focus !== false;\n }\n\n /** Grace period before hiding, allowing the pointer to reach the tooltip. */\n get #hideDelay(): number {\n return this.config.hideDelay ?? DEFAULT_HIDE_DELAY_MS;\n }\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n }\n\n override detach(): void {\n this.#cancelPendingHide();\n if (this.#focusFrame !== null) {\n cancelAnimationFrame(this.#focusFrame);\n this.#focusFrame = null;\n }\n this.#hideTooltip();\n this.#popoverEl?.remove();\n this.#popoverEl = null;\n this.#bound = false;\n super.detach();\n }\n\n override afterRender(): void {\n this.#ensurePopover();\n this.#bindEvents();\n }\n\n // #region Popover Lifecycle\n\n /** Create the shared popover element (once). */\n #ensurePopover(): void {\n if (this.#popoverEl) return;\n const el = document.createElement('div');\n el.className = 'tbw-tooltip-popover';\n el.setAttribute('popover', 'hint');\n el.id = POPOVER_ID;\n el.setAttribute('role', 'tooltip');\n // Override UA popover defaults that @layer CSS cannot beat\n el.style.overflow = 'visible';\n el.style.margin = '0';\n document.body.appendChild(el);\n this.#popoverEl = el;\n\n // SC 1.4.13 \"Hoverable\" — the pointer must be able to rest on the tooltip\n // without dismissing it, so its content can be read, magnified, or copied.\n el.addEventListener('mouseenter', () => this.#cancelPendingHide(), { signal: this.disconnectSignal });\n el.addEventListener('mouseleave', () => this.#scheduleHide(), { signal: this.disconnectSignal });\n // Scrolling slides the anchored popover under a stationary cursor; without this the\n // popover (in <body>) would swallow every wheel tick and the grid would stop scrolling.\n el.addEventListener('wheel', (e) => this.#forwardWheel(e), { passive: false, signal: this.disconnectSignal });\n }\n\n /** Dismiss the tooltip and re-dispatch the wheel on its anchor cell so the grid scrolls. */\n #forwardWheel(e: WheelEvent): void {\n const target = this.#anchorCell;\n this.#hideTooltip();\n if (!target?.isConnected) return;\n const forwarded = new WheelEvent('wheel', {\n deltaX: e.deltaX,\n deltaY: e.deltaY,\n deltaZ: e.deltaZ,\n deltaMode: e.deltaMode,\n clientX: e.clientX,\n clientY: e.clientY,\n shiftKey: e.shiftKey,\n ctrlKey: e.ctrlKey,\n altKey: e.altKey,\n metaKey: e.metaKey,\n bubbles: true,\n cancelable: true,\n composed: true,\n });\n if (!target.dispatchEvent(forwarded)) e.preventDefault();\n }\n\n /** Show the popover anchored to `cell` with the given `text`. */\n #showTooltip(cell: HTMLElement, text: string): void {\n if (!this.#popoverEl) return;\n this.#cancelPendingHide();\n\n // Move the CSS anchor to the hovered cell\n this.#clearAnchor();\n cell.style.setProperty('anchor-name', '--tbw-tooltip-anchor');\n cell.setAttribute('aria-describedby', POPOVER_ID);\n this.#anchorCell = cell;\n\n // Set content (always textContent — safe, no XSS).\n // Multi-line content uses `white-space: pre` so author-supplied line\n // breaks survive verbatim and the popover grows to fit; single-line\n // content keeps `pre-wrap` so long values wrap inside `max-width`.\n this.#popoverEl.textContent = text;\n this.#popoverEl.classList.toggle('tbw-tooltip-multiline', text.includes('\\n'));\n\n // Show via Popover API\n if (supportsPopover()) {\n try {\n this.#popoverEl.showPopover();\n } catch {\n /* already shown */\n }\n }\n\n if (supportsAnchor()) {\n this.#popoverEl.classList.toggle('tbw-tooltip-above', this.#prefersAbove(cell));\n } else {\n this.#positionFallback(cell);\n }\n }\n\n /** Hide the popover and clear the anchor reference. */\n #hideTooltip(): void {\n this.#cancelPendingHide();\n if (this.#popoverEl) {\n if (supportsPopover()) {\n try {\n this.#popoverEl.hidePopover();\n } catch {\n /* already hidden */\n }\n }\n this.#popoverEl.classList.remove('tbw-tooltip-above');\n }\n this.#clearAnchor();\n }\n\n /** Whether the popover is currently anchored to a cell (i.e. visible). */\n get #isVisible(): boolean {\n return this.#anchorCell !== null;\n }\n\n /**\n * Hide after the configured grace period rather than immediately, so the\n * pointer can travel from the cell onto the tooltip (SC 1.4.13 Hoverable).\n */\n #scheduleHide(): void {\n this.#cancelPendingHide();\n if (this.#hideDelay <= 0) {\n this.#hideTooltip();\n return;\n }\n this.#hideTimer = setTimeout(() => {\n this.#hideTimer = null;\n this.#hideTooltip();\n }, this.#hideDelay);\n }\n\n #cancelPendingHide(): void {\n if (this.#hideTimer !== null) {\n clearTimeout(this.#hideTimer);\n this.#hideTimer = null;\n }\n }\n\n /** Remove the CSS anchor-name from the previous cell, but only if it's still our tooltip anchor. */\n #clearAnchor(): void {\n if (this.#anchorCell) {\n if (this.#anchorCell.style.getPropertyValue('anchor-name') === '--tbw-tooltip-anchor') {\n this.#anchorCell.style.removeProperty('anchor-name');\n }\n if (this.#anchorCell.getAttribute('aria-describedby') === POPOVER_ID) {\n this.#anchorCell.removeAttribute('aria-describedby');\n }\n this.#anchorCell = null;\n }\n }\n\n /**\n * Decide whether the popover sits above the cell. Above is preferred so the\n * tooltip never covers the rows the pointer is heading toward; it drops below\n * only when the popover does not fit in the space above the cell.\n *\n * Placement is resolved here rather than left to `position-try-fallbacks` so\n * the arrow-direction class can never disagree with where the browser\n * actually painted the popover.\n */\n #prefersAbove(cell: HTMLElement): boolean {\n if (!this.#popoverEl) return false;\n const cellRect = cell.getBoundingClientRect();\n const spaceAbove = cellRect.top - ARROW_GAP_PX;\n const spaceBelow = window.innerHeight - cellRect.bottom - ARROW_GAP_PX;\n // Popover is already shown and filled, so its box is measurable.\n const height = this.#popoverEl.offsetHeight;\n return height <= spaceAbove || spaceAbove >= spaceBelow;\n }\n\n /**\n * Fallback positioning for browsers without CSS anchor support.\n * Places the popover above or below the cell using fixed coordinates.\n */\n #positionFallback(cell: HTMLElement): void {\n if (!this.#popoverEl) return;\n const cellRect = cell.getBoundingClientRect();\n const above = this.#prefersAbove(cell);\n\n this.#popoverEl.style.position = 'fixed';\n this.#popoverEl.style.left = `${cellRect.left}px`;\n\n if (above) {\n this.#popoverEl.style.top = '';\n this.#popoverEl.style.bottom = `${window.innerHeight - cellRect.top + ARROW_GAP_PX}px`;\n } else {\n this.#popoverEl.style.top = `${cellRect.bottom + ARROW_GAP_PX}px`;\n this.#popoverEl.style.bottom = '';\n }\n this.#popoverEl.classList.toggle('tbw-tooltip-above', above);\n }\n // #endregion\n\n // #region Event Delegation\n\n /** Bind delegated mouseover/mouseout once. */\n #bindEvents(): void {\n if (this.#bound) return;\n const container = this.gridElement?.querySelector('.tbw-grid-root');\n if (!container) return;\n\n this.#bound = true;\n\n container.addEventListener('mouseover', (e: Event) => this.#onMouseOver(e as MouseEvent), {\n signal: this.disconnectSignal,\n });\n\n container.addEventListener('mouseout', (e: Event) => this.#onMouseOut(e as MouseEvent), {\n signal: this.disconnectSignal,\n });\n\n // SC 1.4.13 \"Dismissible\" — Escape must hide the tooltip without moving the\n // pointer. Bound on `document` because a hover-triggered tooltip can be\n // visible while focus sits entirely outside the grid, where the grid's own\n // key handling never runs. The event is deliberately NOT consumed: Escape\n // also cancels an in-progress edit, and swallowing it here would break that.\n document.addEventListener('keydown', (e: Event) => this.#onKeyDownGlobal(e as KeyboardEvent), {\n capture: true,\n signal: this.disconnectSignal,\n });\n\n // Keyboard navigation keeps DOM focus on the grid HOST (`tabindex=0`), so\n // keydown never travels through `.tbw-grid-root` — a child of the host.\n // Bind on the host, exactly where `setupRootEventDelegation` binds the\n // grid's own navigation handler.\n this.gridElement.addEventListener('keydown', (e: Event) => this.#onKeyDownGrid(e as KeyboardEvent), {\n signal: this.disconnectSignal,\n });\n }\n\n #onMouseOver(e: MouseEvent): void {\n const target = e.target as HTMLElement;\n if (!target?.closest) return;\n\n // Check for header cell\n const headerCell = target.closest('[part~=\"header-cell\"]') as HTMLElement | null;\n if (headerCell && this.#headerEnabled) {\n // Re-showing on every child boundary would re-measure (scrollWidth,\n // getBoundingClientRect) and re-anchor the popover, thrashing layout.\n if (!isSameCellCrossing(headerCell, e.relatedTarget)) {\n this.#showHeaderTooltip(headerCell);\n }\n return;\n }\n\n // Check for data cell — skip cells that already have a CSS anchor (e.g. overlay editors)\n // to avoid overwriting their anchor-name and breaking their positioning.\n const dataCell = target.closest('[data-row][data-col]') as HTMLElement | null;\n if (dataCell && this.#cellEnabled && !dataCell.style.getPropertyValue('anchor-name')) {\n if (isSameCellCrossing(dataCell, e.relatedTarget)) return;\n this.#showCellTooltip(dataCell);\n }\n }\n\n #onMouseOut(e: MouseEvent): void {\n const target = e.target as HTMLElement;\n if (!target?.closest) return;\n\n const cell = target.closest('[part~=\"header-cell\"], [data-row][data-col]') as HTMLElement | null;\n if (!cell) return;\n\n // Keep tooltip if pointer moved to a child still inside the same cell\n if (isSameCellCrossing(cell, e.relatedTarget)) return;\n\n // Grace period, not an immediate hide, so the pointer can reach the tooltip.\n this.#scheduleHide();\n }\n\n /** Escape dismisses a visible tooltip from anywhere on the page. */\n #onKeyDownGlobal(e: KeyboardEvent): void {\n if (e.key === 'Escape' && this.#isVisible) {\n this.#hideTooltip();\n }\n }\n\n /**\n * Retarget the tooltip after keyboard navigation moves the virtual cell\n * focus (SC 1.4.13 \"focus\" trigger). Only key-driven focus qualifies —\n * pointer interaction never reaches here, so mouse users are unaffected.\n */\n #onKeyDownGrid(e: KeyboardEvent): void {\n if (!this.#focusEnabled || !this.#cellEnabled) return;\n if (e.key === 'Escape') return;\n if (!NAVIGATION_KEYS.has(e.key)) {\n // Any other key (typing, Enter to edit) means the user has moved on.\n this.#hideTooltip();\n return;\n }\n // Cell focus is virtual and moves in the grid's own keydown handler, which\n // runs after this one — read the resulting `.cell-focus` on the next frame.\n if (this.#focusFrame !== null) cancelAnimationFrame(this.#focusFrame);\n this.#focusFrame = requestAnimationFrame(() => {\n this.#focusFrame = null;\n this.#syncTooltipToFocusedCell();\n });\n }\n\n /** Show (or clear) the tooltip for the cell that currently holds focus. */\n #syncTooltipToFocusedCell(): void {\n const cell = this.gridElement?.querySelector('.cell-focus') as HTMLElement | null;\n if (!cell || !cell.hasAttribute('data-row')) {\n this.#hideTooltip();\n return;\n }\n // Focus was clamped rather than moved (ArrowUp on the first row, ArrowDown\n // on the last, …) — the tooltip already describes this cell.\n if (cell === this.#anchorCell) return;\n // A foreign anchor means an overlay editor owns this cell's positioning, so\n // we cannot re-anchor here; drop the previous cell's tooltip rather than\n // leave it pointing somewhere focus has left.\n if (cell.style.getPropertyValue('anchor-name')) {\n this.#hideTooltip();\n return;\n }\n const before = this.#anchorCell;\n this.#showCellTooltip(cell);\n // No tooltip text for this cell — drop whatever the previous cell showed.\n if (this.#anchorCell === before) this.#hideTooltip();\n }\n // #endregion\n\n // #region Tooltip Resolution\n\n #showHeaderTooltip(headerCell: HTMLElement): void {\n const colIndex = parseInt(headerCell.getAttribute('data-col') ?? '-1', 10);\n if (colIndex < 0) return;\n\n const column = this.visibleColumns[colIndex];\n if (!column) return;\n\n const text = resolveHeaderTooltip(column, headerCell);\n if (text) {\n this.#showTooltip(headerCell, text);\n }\n }\n\n #showCellTooltip(cell: HTMLElement): void {\n const rowIndex = parseInt(cell.getAttribute('data-row') ?? '-1', 10);\n const colIndex = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n if (rowIndex < 0 || colIndex < 0) return;\n\n const column = this.visibleColumns[colIndex];\n if (!column) return;\n\n const row = this.rows[rowIndex];\n const value = row?.[column.field as keyof typeof row];\n\n const text = resolveCellTooltip(column, cell, row, value, this.grid);\n if (text) {\n this.#showTooltip(cell, text);\n }\n }\n // #endregion\n}\n// #endregion\n"],"mappings":"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"}
|
package/umd/plugins/tree.umd.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@toolbox-web/grid")):"function"==typeof define&&define.amd?define(["exports","@toolbox-web/grid"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TbwGridPlugin_tree={},e.TbwGrid)}(this,function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var r="expanded",n="tree-toggle",i="__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles".split("|").map(e=>e.split("").reverse().join("")),s=(new RegExp(`__(proto|defineGetter|defineSetter)|${i.slice(3).join("|")}|this\\b`),new Set("script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math".split("|"))),o=/^on\w+$/i,a=new Set("href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset".split("|")),d=/^\s*(javascript|vbscript|data|blob):/i;function l(e){const t=document.createElement("template");return e&&"string"==typeof e&&(t.innerHTML=e,function(e){const t=[],r=e.querySelectorAll("*");for(const n of r){const e=n.tagName.toLowerCase();if(s.has(e)){t.push(n);continue}if(("svg"===e||"http://www.w3.org/2000/svg"===n.namespaceURI)&&Array.from(n.attributes).some(e=>o.test(e.name)||"href"===e.name||"xlink:href"===e.name)){t.push(n);continue}const r=[];for(const t of n.attributes){const e=t.name.toLowerCase();o.test(e)?r.push(t.name):"is"!==e?(a.has(e)&&d.test(t.value)||"style"===e&&/expression\s*\(|javascript:|behavior\s*:/i.test(t.value))&&r.push(t.name):r.push(t.name)}r.forEach(e=>n.removeAttribute(e))}t.forEach(e=>e.remove())}(t.content)),t.content}function h(e,t,r=new WeakMap){const n=[];for(const h of["structure","transform"])for(const r of t)r.hierarchyStage===h&&r.beginHierarchy(e)&&n.push(r);if(0===n.length)return[...e];const i=[],s=(e,t,r)=>{const i={parent:t?t.row:null,parentKey:t?.key??null,depth:r};for(const s of n)e=s.processSiblings(e,i);return e},o=(e,t)=>{const n=e.length;for(let a=0;a<n;a++){const d=e[a];i.push(d.row),"object"==typeof d.row&&null!==d.row&&r.set(d.row,{level:t+1,setSize:n,posInSet:a+1});const l=d.children;d.expanded&&l&&o(d.shaped?l:s(l,d,t+1),t+1)}},a=new Array(e.length);for(let h=0;h<e.length;h++)a[h]={row:e[h]};o(s(a,null,0),0);const d=e=>"object"==typeof e&&null!==e?r.get(e):void 0;let l=!1;for(const h of n)h.endHierarchy(i,d)&&(l=!0);if(!l)for(const h of i)"object"==typeof h&&null!==h&&r.delete(h);return i}function c(e,t,r){const n=String(r);e.getAttribute(t)!==n&&e.setAttribute(t,n)}function u(e){return"function"==typeof e.processSiblings}function p(e){return e.constructor.manifest?.hookPriority?.processRows??0}var g=class extends t.BaseGridPlugin{static manifest={modifiesRowStructure:!0};name="hierarchy";#e=new WeakMap;detach(){this.gridElement?.querySelector(".rows-body")?.setAttribute("role","grid"),this.#e=new WeakMap}process(e,t){const r=this.#t();return r.includes(t)?r[r.length-1]!==t?e:(this.#e=new WeakMap,h(e,r,this.#e)):(this.#e=new WeakMap,h(e,[t],this.#e))}getRowMeta(e){return"object"==typeof e&&null!==e?this.#e.get(e):void 0}afterRender(){this.#r()}onScrollRender(){this.#r()}#t(){return(this.grid?._pluginManager?.getPlugins?.()??[]).filter(u).sort((e,t)=>p(e)-p(t))}#r(){if(0===this.#t().length)return;const e=this.gridElement?.querySelector(".rows");if(!e)return;let t=!1;for(const i of e.querySelectorAll(".data-grid-row")){const e=this.getRowMeta(i.__rowDataRef);e?(t=!0,c(i,"aria-level",e.level),c(i,"aria-setsize",e.setSize),c(i,"aria-posinset",e.posInSet)):(i.removeAttribute("aria-level"),i.removeAttribute("aria-setsize"),i.removeAttribute("aria-posinset"))}const r=t?"treegrid":"grid",n=this.gridElement?.querySelector(".rows-body");n&&n.getAttribute("role")!==r&&n.setAttribute("role",r)}};function f(e,t,r){return void 0!==e.id?String(e.id):r?`${r}-${t}`:String(t)}function w(e,t){const r=new Set(e);return r.has(t)?r.delete(t):r.add(t),r}function y(e,t,r=null,n=0){const i=t.childrenField??"children",s=new Set;for(let o=0;o<e.length;o++){const a=e[o],d=f(a,o,r),l=a[i];if(Array.isArray(l)&&l.length>0){s.add(d);const e=y(l,t,d,n+1);for(const t of e)s.add(t)}}return s}function m(e,t,r,n=null,i=0){const s=r.childrenField??"children";for(let o=0;o<e.length;o++){const a=e[o],d=f(a,o,n);if(d===t)return[d];const l=a[s];if(Array.isArray(l)&&l.length>0){const e=m(l,t,r,d,i+1);if(e)return[d,...e]}}return null}function b(e,t,r,n){const i=m(e,t,r);if(!i)return n;const s=new Set(n);for(let o=0;o<i.length-1;o++)s.add(i[o]);return s}function x(e,t="children",r){if(!Array.isArray(e)||0===e.length)return!1;for(const n of e){if(!n||!0===n.__loading)continue;const e=n[t];if(Array.isArray(e)&&e.length>0)return!0;if(null!=e&&!Array.isArray(e)&&e)return!0;if(r?.(n))return!0}return!1}var v=class extends t.BaseGridPlugin{static manifest={modifiesRowStructure:!0,hookPriority:{processRows:10},incompatibleWith:[{name:"pivot",reason:"PivotPlugin replaces the entire row and column structure with aggregated pivot data. Tree hierarchy cannot coexist with pivot aggregation."}],events:[{type:"tree-expand",description:"Emitted when tree expansion state changes (toggle, expand all, collapse all). Broadcast to both DOM consumers and plugin bus."},{type:"tree-load-start",description:"Emitted when lazy child loading starts for a node whose children are not yet loaded."},{type:"tree-load-end",description:"Emitted when lazy child loading completes and children have been merged into the parent row."},{type:"tree-load-error",description:"Emitted when lazy child loading fails. The node leaves its loading state and can be retried."}],queries:[{type:"canMoveRow",description:"Returns false for rows with children (parent nodes cannot be reordered)"},{type:"datasource:viewport-mapping",description:"Translates flat viewport row indices to top-level node indices for ServerSide pagination."}]};static dependencies=[{name:"hierarchy",required:!0,reason:"Builds the hierarchical row model",provide:()=>new g},{name:"multiSort",required:!1,reason:"Queries sort model for coordinated tree sorting"},{name:"serverSide",required:!1,reason:"Consumes datasource events for lazy-loaded tree data"}];name="tree";hierarchyStage="structure";styles='@layer tbw-plugins{tbw-grid .cell[data-field=__tbw_expander]{justify-content:flex-start;align-items:center;padding:0;display:flex;border-inline-end:none!important}tbw-grid .header-row .cell[data-field=__tbw_expander]{display:none}tbw-grid .header-row .cell[data-field=__tbw_expander]+.cell{grid-column:1/3}tbw-grid .tree-cell-wrapper{vertical-align:middle;align-items:center;padding-inline-start:calc(var(--tbw-tree-depth,0) * var(--tbw-tree-indent-width,var(--tbw-tree-toggle-size,1.25em)));display:inline-flex;overflow:visible}tbw-grid .tree-expander{box-sizing:border-box;justify-content:flex-start;align-items:center;width:100%;height:100%;display:flex}tbw-grid .tree-toggle{cursor:pointer;-webkit-user-select:none;user-select:none;width:var(--tbw-tree-toggle-size,1.25em);height:var(--tbw-tree-toggle-size,1.25em);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}tbw-grid .tree-toggle:hover{color:var(--tbw-tree-accent,var(--tbw-color-accent))}tbw-grid .tree-toggle{position:relative}tbw-grid .tree-toggle:after{content:"";inline-size:var(--tbw-touch-target-min,24px);block-size:var(--tbw-touch-target-min,24px);position:absolute;inset-block-start:50%;inset-inline-start:50%;translate:-50% -50%}tbw-grid .tree-spacer{width:var(--tbw-tree-toggle-size,1.25em);height:var(--tbw-tree-toggle-size,1.25em);flex-shrink:0;display:inline-block}@media (pointer:coarse){tbw-grid .tree-toggle,tbw-grid .tree-spacer{min-width:var(--tbw-touch-target-min,24px);min-height:var(--tbw-touch-target-min,24px)}}tbw-grid .tree-loading{--tbw-spinner-size:var(--tbw-tree-toggle-size,1.25em);flex-shrink:0}tbw-grid .data-grid-row.tbw-tree-slide-in{animation:tbw-tree-slide-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}tbw-grid .data-grid-row.tbw-tree-fade-in{animation:tbw-tree-fade-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}@keyframes tbw-tree-slide-in{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}@keyframes tbw-tree-fade-in{0%{opacity:0}to{opacity:1}}}';get defaultConfig(){return{childrenField:"children",autoDetect:!0,defaultExpanded:!1,indentWidth:20,showExpandIcons:!0,animation:"slide"}}expandedKeys=new Set;initialExpansionDone=!1;flattenedRows=[];rowKeyMap=new Map;previousVisibleKeys=new Set;keysToAnimate=new Set;#n=[];#i=0;sortState=null;#s=null;#o=new Map;#a=new WeakSet;#d=new WeakSet;#l=new WeakSet;#h=[];#c=new WeakMap;#u=new WeakMap;originalTreeColumnRenderer;originalTreeColumnRendererProperty="viewRenderer";wrappedTreeColumnField;#p=!1;#g=!1;detach(){this.expandedKeys.clear(),this.initialExpansionDone=!1,this.flattenedRows=[],this.#n=[],this.#i=0,this.rowKeyMap.clear(),this.previousVisibleKeys.clear(),this.keysToAnimate.clear(),this.sortState=null,this.#s=null,this.#a=new WeakSet,this.#d=new WeakSet,this.#l=new WeakSet,this.#h=[];for(const e of this.#o.values())e.controller.abort();this.#o.clear(),this.originalTreeColumnRenderer=void 0,this.originalTreeColumnRendererProperty="viewRenderer",this.wrappedTreeColumnField=void 0,this.#p=!1}handleQuery(e){if("canMoveRow"===e.type){const t=e.context,r=this.config.childrenField??"children",n=t?.[r];if(Array.isArray(n)&&n.length>0)return!1}if("datasource:viewport-mapping"===e.type){const{viewportStart:t,viewportEnd:r}=e.context;if(0===this.flattenedRows.length)return;return function(e,t,r,n){const i=e.length-1,s=t=>Math.max(0,e[Math.max(0,Math.min(t,i))]??0);return{startNode:s(r),endNode:s(n)+1,totalLoadedNodes:t}}(this.#n,this.#i,t,r)}}attach(e){super.attach(e),this.on("datasource:data",e=>{const t=e;t.claimed||(t.claimed=!0)}),this.on("datasource:children",e=>{const t=e;if("tree"!==t.context?.source)return;t.claimed=!0;const r=t.context.parentNode;if(r){const e=this.#f(r),n=this.#w(e,r);if(!n&&(this.#d.has(r)||this.#l.has(r)))return;this.#y(r,t.rows??[]),this.broadcast("tree-load-end",{key:e,row:r,depth:n?.depth??this.rowKeyMap.get(e)?.depth??0,childCount:t.rows?.length??0}),this.requestRender()}}),this.on("datasource:error",e=>{const t=e;if("tree"!==t.context?.source)return;const r=t.context.parentNode;if(!r)return;const n=this.#f(r),i=this.#w(n,r);i&&(this.broadcast("tree-load-error",{key:n,row:r,depth:i.depth,error:t.error}),this.requestRender())})}get animationStyle(){return!!this.isAnimationEnabled&&(this.config.animation??"slide")}detect(e){if(!this.config.autoDetect)return!1;const t=e,r=this.config.childrenField??function(e){if(!Array.isArray(e)||0===e.length)return null;const t=["children","items","nodes","subRows","nested"];for(const r of e)if(r&&"object"==typeof r)for(const e of t){const t=r[e];if(Array.isArray(t)&&t.length>0)return e}return null}(t)??"children";return x(t,r)}processRows(e){const t=this.grid?.getPluginByName?.("hierarchy");return t?t.process(e,this):h(e,[this])}beginHierarchy(e){const t=e;if(0===t.length||!x(t,this.config.childrenField??"children",this.config.hasChildren))return this.flattenedRows=[],this.#n=[],this.#i=0,this.rowKeyMap.clear(),this.previousVisibleKeys.clear(),this.#m(t),this.#b(),!1;const r="server"===this.grid?.query?.("datasource:sort-mode",null)?.[0];return this.#s=r?null:this.resolveEffectiveSortState(),this.config.defaultExpanded&&!this.initialExpansionDone&&(this.expandedKeys=y(t,this.config),this.initialExpansionDone=!0),this.#u=new WeakMap,!0}processSiblings(e,t){const{parentKey:r,depth:n}=t,i=this.config.childrenField??"children",s=this.config.hasChildren;for(let d=0;d<e.length;d++)this.#x(e[d].row,d,r);const o=this.#s,a=o?this.#v(e,o.field,o.direction):e;for(let d=0;d<a.length;d++){const e=a[d],t=e.row,o=this.#x(t,d,r),l=t[i],h=Array.isArray(l)&&l.length>0,c=h||!0!==t.__loading&&(s?s(t):null!=l&&!Array.isArray(l)&&!!l),u=this.expandedKeys.has(o);this.#u.set(t,{key:o,data:t,depth:n,hasChildren:c,isExpanded:u,parentKey:r,posInSet:d+1,setSize:a.length}),e.key=o,h&&u&&(e.expanded=!0,e.children=l.map(e=>({row:e})))}return a}endHierarchy(e,t){const r=this.#u;this.#u=new WeakMap,this.flattenedRows=[],this.rowKeyMap.clear(),this.keysToAnimate.clear();const n=new Set,i=new Array(e.length);let s=-1;for(let o=0;o<e.length;o++){const a=e[o],d=r.get(a);if(null===d?.parentKey&&s++,i[o]=s,!d)continue;const l=t(a)?.level;void 0!==l&&(d.depth=l-1),this.flattenedRows.push(d),this.rowKeyMap.set(d.key,d),this.#u.set(a,d),n.add(d.key),!this.previousVisibleKeys.has(d.key)&&d.depth>0&&this.keysToAnimate.add(d.key),this.#S(d)&&this.#R(d)}return this.previousVisibleKeys=n,this.#n=i,this.#i=s+1,this.#b(),!0}#b(){this.#g||(this.#g=!0,queueMicrotask(()=>{this.#g=!1,this.grid&&this.#p!==this.flattenedRows.length>0&&this.requestColumnsRender()}))}#x(e,t,r){if(void 0!==e.id){const t=String(e.id);return this.#c.set(e,t),t}const n=this.#c.get(e);if(void 0!==n)return n;const i=r?`${r}-${t}`:String(t);return this.#c.set(e,i),i}#v(e,t,r){const n=new Map;for(const i of e)n.set(i.row,i);return this.#k(e.map(e=>e.row),t,r).map(e=>n.get(e)??{row:e})}#k(e,r,n){const i=this.grid,s=i?._columns??[],o={field:r,direction:n},a=[...e],d=(i?.effectiveConfig?.sortHandler??t.builtInSort)(a,o,s);return d&&"function"==typeof d.then?(d.catch(()=>{}),(0,t.builtInSort)(a,o,s)):d}#f(e){return this.#c.get(e)??String(e.id??"?")}#y(e,t){e[this.config.childrenField??"children"]=t,this.#a.add(e)}#C(e){if(this.#a.has(e))return!1;const t=e[this.config.childrenField??"children"];if(Array.isArray(t)&&t.length>0)return!1;const r=this.config.hasChildren;return r?r(e):null!=t&&!Array.isArray(t)&&!!t}#S(e){if(!0===e.data.__loading)return!1;const t=this.#o.get(e.key);return t?t.row!==e.data:e.isExpanded&&e.hasChildren&&!this.#d.has(e.data)&&this.#C(e.data)}#m(e){if(0===this.#o.size)return;const t=new Map;for(let r=0;r<e.length;r++){const n=e[r];if(!0===n.__loading)continue;const i=void 0!==n.id?String(n.id):this.#c.get(n)??String(r);this.#o.has(i)&&t.set(i,n)}for(const[r,n]of t)this.#o.get(r)?.row!==n&&this.#R({key:r,data:n,depth:0,hasChildren:!1,isExpanded:!1,parentKey:null,posInSet:0,setSize:0},!0)}#R(e,t=!1){this.#h.push({meta:e,flat:t})>1||queueMicrotask(()=>{const e=this.#h;if(this.#h=[],!this.grid)return;let t=!1;for(const{meta:r,flat:n}of e){if(!n&&this.rowKeyMap.get(r.key)?.data!==r.data)continue;const e=this.#o.get(r.key);e&&e.row!==r.data&&this.#A(r.key),n||!this.expandedKeys.has(r.key)||this.#d.has(r.data)||(this.#d.add(r.data),t=this.requestLazyChildren(r)||t)}t&&this.requestRender()})}requestLazyChildren(e){const{key:t,depth:r,data:n}=e,i=this.#o.get(t);if(i?.row===n)return!1;if(!this.#C(n))return!1;const s=this.queryBoolean("datasource:is-active"),{loadChildren:o}=this.config;if(!s&&!o)return!1;i&&this.#A(t);const a=new AbortController;return this.#o.set(t,{row:n,depth:r,controller:a}),this.#d.add(n),this.#l.delete(n),this.broadcast("tree-load-start",{key:t,row:n,depth:r}),s?(this.grid.query("datasource:fetch-children",{context:{source:"tree",parentNode:n,nodePath:[t]},signal:a.signal}),!0):(this.#K(t,n,r,a),!0)}#q(e){return this.#o.get(e.key)?.row===e.data}#w(e,t){const r=this.#o.get(e);if(r?.row===t)return this.#o.delete(e),r}#A(e){const t=this.#o.get(e);t&&(this.#o.delete(e),this.#d.delete(t.row),this.#l.add(t.row),t.controller.abort(),this.broadcast("tree-load-end",{key:e,row:t.row,depth:t.depth,childCount:0,cancelled:!0}))}#K(e,t,r,n){const i=this.config.loadChildren,s=()=>!n.signal.aborted&&(this.#o.delete(e),!0),o=n=>{if(!s())return;const i=n??[];this.#y(t,i),this.broadcast("tree-load-end",{key:e,row:t,depth:r,childCount:i.length}),this.requestRender()},a=n=>{s()&&(this.broadcast("tree-load-error",{key:e,row:t,depth:r,error:n}),this.requestRender())};let d;try{d=i({row:t,key:e,depth:r,signal:n.signal})}catch(l){return void a(l)}if(d&&"function"==typeof d.subscribe){let e=!1,t=null;const r=()=>{e=!0,t?.unsubscribe(),t=null},i=d.subscribe({next:e=>{o(e),r()},error:e=>{a(e),r()}});return e?void i.unsubscribe():(t=i,void n.signal.addEventListener("abort",r,{once:!0}))}Promise.resolve(d).then(o,a)}resolveEffectiveSortState(){const e=this.grid?.query?.("sort:get-model",null);if(Array.isArray(e)&&e.length>0){const t=e[0];if(Array.isArray(t)&&t.length>0)return{field:t[0].field,direction:"desc"===t[0].direction?-1:1}}return this.sortState}processColumns(e){if(0===this.flattenedRows.length){const t=[...e];if(this.#p){const e=t.findIndex(e=>e.field===this.wrappedTreeColumnField);e>=0&&(t[e]={...t[e],renderer:"renderer"===this.originalTreeColumnRendererProperty?this.originalTreeColumnRenderer:void 0})}return this.#p=!1,this.originalTreeColumnRenderer=void 0,this.originalTreeColumnRendererProperty="viewRenderer",this.wrappedTreeColumnField=void 0,t}const t=[...e];if(0===t.length)return t;const{treeColumn:i}=this.config;let s=0;if(i){const e=t.findIndex(e=>e.field===i);e>=0&&(s=e)}const o=t[s],a=o.field;this.wrappedTreeColumnField!==a&&(this.originalTreeColumnRenderer=o.renderer||o.viewRenderer,this.originalTreeColumnRendererProperty=o.renderer?"renderer":"viewRenderer",this.wrappedTreeColumnField=a);const d=this.originalTreeColumnRenderer,h=()=>this.config,c=this.setIcon.bind(this);return t[s]={...o,renderer:e=>{const{row:t,value:i}=e,{showExpandIcons:s=!0,indentWidth:o}=h(),a=this.#u.get(t),u=a?.depth??0,p=document.createElement("span");if(p.className="tree-cell-wrapper",p.style.setProperty("--tbw-tree-depth",String(u)),void 0!==o&&p.style.setProperty("--tbw-tree-indent-width",`${o}px`),s)if(a&&this.#q(a)){const e=function(e){const t=document.createElement("div");return t.className=`tbw-spinner tbw-spinner--${e}`,t.setAttribute("role","progressbar"),t.setAttribute("aria-label","Loading"),t}("small");e.classList.add("tree-loading"),p.appendChild(e)}else if(a&&a.hasChildren){const e=document.createElement("span");e.className=`${n}${a.isExpanded?` ${r}`:""}`,c(e,a.isExpanded?"collapse":"expand"),e.setAttribute("data-tree-key",a.key),p.appendChild(e)}else{const e=document.createElement("span");e.className="tree-spacer",p.appendChild(e)}const g=document.createElement("span");if(g.className="tree-content",d){const t=d(e);t instanceof Node?g.appendChild(t):"string"==typeof t&&(f=g,(w=t)&&"string"==typeof w?-1!==w.indexOf("<")||-1!==w.indexOf("&")?f.replaceChildren(l(w)):f.textContent=w:f.replaceChildren())}else g.textContent=null!=i?String(i):"";var f,w;return p.appendChild(g),p}},this.#p=!0,t}onCellClick(e){const t=e.originalEvent?.target;if(!t?.classList.contains(n))return!1;const r=t.getAttribute("data-tree-key");if(!r)return!1;const i=this.rowKeyMap.get(r);return!!i&&(this.expandedKeys=w(this.expandedKeys,r),this.expandedKeys.has(r)&&this.requestLazyChildren(i),this.broadcast("tree-expand",{key:r,row:i.data,expanded:this.expandedKeys.has(r),depth:i.depth,expandedKeys:[...this.expandedKeys]}),this.requestRender(),!0)}onKeyDown(e){if(" "!==e.key)return;const t=this.grid._focusRow,r=this.#u.get(this.rows[t]);return r?.hasChildren?(e.preventDefault(),this.expandedKeys=w(this.expandedKeys,r.key),this.expandedKeys.has(r.key)&&this.requestLazyChildren(r),this.broadcast("tree-expand",{key:r.key,row:r.data,expanded:this.expandedKeys.has(r.key),depth:r.depth,expandedKeys:[...this.expandedKeys]}),this.requestRenderWithFocus(),!0):void 0}onHeaderClick(e){if(0===this.flattenedRows.length||!e.column.sortable)return!1;const t=this.grid?.query?.("sort:get-model",null);if(Array.isArray(t)&&t.length>0)return!1;const{field:r}=e.column;this.sortState&&this.sortState.field===r?1===this.sortState.direction?this.sortState={field:r,direction:-1}:this.sortState=null:this.sortState={field:r,direction:1};const n=this.grid;return void 0!==n._sortState&&(n._sortState=this.sortState?{...this.sortState}:null),this.broadcast("sort-change",{field:r,direction:this.sortState?.direction??0}),this.requestRender(),!0}afterRender(){const e=this.gridElement?.querySelector(".rows");if(!e)return;const t=this.animationStyle,r=!1!==t&&this.keysToAnimate.size>0,n="fade"===t?"tbw-tree-fade-in":"tbw-tree-slide-in";for(const i of e.querySelectorAll(".data-grid-row")){const e=this.#u.get(i.__rowDataRef);e&&(e.hasChildren?i.setAttribute("aria-expanded",String(e.isExpanded)):i.hasAttribute("aria-expanded")&&i.removeAttribute("aria-expanded"),i.classList.toggle("tbw-row-expanded",e.hasChildren&&e.isExpanded),this.#q(e)?i.setAttribute("aria-busy","true"):i.hasAttribute("aria-busy")&&!i.classList.contains("tbw-row-loading")&&i.removeAttribute("aria-busy"),r&&e.key&&this.keysToAnimate.has(e.key)&&(i.classList.add(n),i.addEventListener("animationend",()=>i.classList.remove(n),{once:!0})))}this.keysToAnimate.clear()}expand(e){this.expandedKeys.add(e);const t=this.rowKeyMap.get(e);t&&this.requestLazyChildren(t),this.requestRender()}collapse(e){this.expandedKeys.delete(e),this.requestRender()}toggle(e){this.expandedKeys=w(this.expandedKeys,e);const t=this.rowKeyMap.get(e);t?(this.expandedKeys.has(e)&&this.requestLazyChildren(t),this.broadcast("tree-expand",{key:e,row:t.data,expanded:this.expandedKeys.has(e),depth:t.depth,expandedKeys:[...this.expandedKeys]})):this.emitPluginEvent("tree-expand",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}expandAll(){this.expandedKeys=y(this.rows,this.config),this.emitPluginEvent("tree-expand",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}collapseAll(){this.expandedKeys=new Set,this.emitPluginEvent("tree-expand",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}isExpanded(e){return this.expandedKeys.has(e)}getExpandedKeys(){return[...this.expandedKeys]}getFlattenedRows(){return[...this.flattenedRows]}getRowMeta(e){return this.#u.get(e)}getRowByKey(e){return this.rowKeyMap.get(e)?.data}expandToKey(e){this.expandedKeys=b(this.rows,e,this.config,this.expandedKeys),this.requestRender()}};e.TreePlugin=v});
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@toolbox-web/grid")):"function"==typeof define&&define.amd?define(["exports","@toolbox-web/grid"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).TbwGridPlugin_tree={},e.TbwGrid)}(this,function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:"Module"});var r="expanded",n="tree-toggle",i="__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles".split("|").map(e=>e.split("").reverse().join("")),s=(new RegExp(`__(proto|defineGetter|defineSetter)|${i.slice(3).join("|")}|this\\b`),new Set("script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math".split("|"))),o=/^on\w+$/i,a=new Set("href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset".split("|")),d=/^\s*(javascript|vbscript|data|blob):/i;function l(e){const t=document.createElement("template");return e&&"string"==typeof e&&(t.innerHTML=e,function(e){const t=[],r=e.querySelectorAll("*");for(const n of r){const e=n.tagName.toLowerCase();if(s.has(e)){t.push(n);continue}if(("svg"===e||"http://www.w3.org/2000/svg"===n.namespaceURI)&&Array.from(n.attributes).some(e=>o.test(e.name)||"href"===e.name||"xlink:href"===e.name)){t.push(n);continue}const r=[];for(const t of n.attributes){const e=t.name.toLowerCase();o.test(e)?r.push(t.name):"is"!==e?(a.has(e)&&d.test(t.value)||"style"===e&&/expression\s*\(|javascript:|behavior\s*:/i.test(t.value))&&r.push(t.name):r.push(t.name)}r.forEach(e=>n.removeAttribute(e))}t.forEach(e=>e.remove())}(t.content)),t.content}function h(e,t,r=new WeakMap){const n=[];for(const h of["structure","transform"])for(const r of t)r.hierarchyStage===h&&r.beginHierarchy(e)&&n.push(r);if(0===n.length)return[...e];const i=[],s=(e,t,r)=>{const i={parent:t?t.row:null,parentKey:t?.key??null,depth:r};for(const s of n)e=s.processSiblings(e,i);return e},o=(e,t)=>{const n=e.length;for(let a=0;a<n;a++){const d=e[a];i.push(d.row),"object"==typeof d.row&&null!==d.row&&r.set(d.row,{level:t+1,setSize:n,posInSet:a+1});const l=d.children;d.expanded&&l&&o(d.shaped?l:s(l,d,t+1),t+1)}},a=new Array(e.length);for(let h=0;h<e.length;h++)a[h]={row:e[h]};o(s(a,null,0),0);const d=e=>"object"==typeof e&&null!==e?r.get(e):void 0;let l=!1;for(const h of n)h.endHierarchy(i,d)&&(l=!0);if(!l)for(const h of i)"object"==typeof h&&null!==h&&r.delete(h);return i}function c(e,t,r){const n=String(r);e.getAttribute(t)!==n&&e.setAttribute(t,n)}function u(e){return"function"==typeof e.processSiblings}function p(e){return e.constructor.manifest?.hookPriority?.processRows??0}var g=class extends t.BaseGridPlugin{static manifest={modifiesRowStructure:!0};name="hierarchy";#e=new WeakMap;detach(){this.gridElement?.querySelector(".rows-body")?.setAttribute("role","grid"),this.#e=new WeakMap}process(e,t){const r=this.#t();return r.includes(t)?r[r.length-1]!==t?e:(this.#e=new WeakMap,h(e,r,this.#e)):(this.#e=new WeakMap,h(e,[t],this.#e))}getRowMeta(e){return"object"==typeof e&&null!==e?this.#e.get(e):void 0}afterRender(){this.#r()}onScrollRender(){this.#r()}#t(){return(this.grid?._pluginManager?.getPlugins?.()??[]).filter(u).sort((e,t)=>p(e)-p(t))}#r(){if(0===this.#t().length)return;const e=this.gridElement?.querySelector(".rows");if(!e)return;let t=!1;for(const i of e.querySelectorAll(".data-grid-row")){const e=this.getRowMeta(i.__rowDataRef);e?(t=!0,c(i,"aria-level",e.level),c(i,"aria-setsize",e.setSize),c(i,"aria-posinset",e.posInSet)):(i.removeAttribute("aria-level"),i.removeAttribute("aria-setsize"),i.removeAttribute("aria-posinset"))}const r=t?"treegrid":"grid",n=this.gridElement?.querySelector(".rows-body");n&&n.getAttribute("role")!==r&&n.setAttribute("role",r)}};function f(e,t,r){return void 0!==e.id?String(e.id):r?`${r}-${t}`:String(t)}function w(e,t){const r=new Set(e);return r.has(t)?r.delete(t):r.add(t),r}function y(e,t,r=null,n=0){const i=t.childrenField??"children",s=new Set;for(let o=0;o<e.length;o++){const a=e[o],d=f(a,o,r),l=a[i];if(Array.isArray(l)&&l.length>0){s.add(d);const e=y(l,t,d,n+1);for(const t of e)s.add(t)}}return s}function m(e,t,r,n=null,i=0){const s=r.childrenField??"children";for(let o=0;o<e.length;o++){const a=e[o],d=f(a,o,n);if(d===t)return[d];const l=a[s];if(Array.isArray(l)&&l.length>0){const e=m(l,t,r,d,i+1);if(e)return[d,...e]}}return null}function b(e,t,r,n){const i=m(e,t,r);if(!i)return n;const s=new Set(n);for(let o=0;o<i.length-1;o++)s.add(i[o]);return s}function x(e,t="children",r){if(!Array.isArray(e)||0===e.length)return!1;for(const n of e){if(!n||!0===n.__loading)continue;const e=n[t];if(Array.isArray(e)&&e.length>0)return!0;if(null!=e&&!Array.isArray(e)&&e)return!0;if(r?.(n))return!0}return!1}var v=class extends t.BaseGridPlugin{static manifest={modifiesRowStructure:!0,hookPriority:{processRows:10},incompatibleWith:[{name:"pivot",reason:"PivotPlugin replaces the entire row and column structure with aggregated pivot data. Tree hierarchy cannot coexist with pivot aggregation."}],events:[{type:"tree-expand",description:"Emitted when tree expansion state changes (toggle, expand all, collapse all). Broadcast to both DOM consumers and plugin bus."},{type:"tree-load-start",description:"Emitted when lazy child loading starts for a node whose children are not yet loaded."},{type:"tree-load-end",description:"Emitted when lazy child loading completes and children have been merged into the parent row."},{type:"tree-load-error",description:"Emitted when lazy child loading fails. The node leaves its loading state and can be retried."}],queries:[{type:"canMoveRow",description:"Returns false for rows with children (parent nodes cannot be reordered)"},{type:"datasource:viewport-mapping",description:"Translates flat viewport row indices to top-level node indices for ServerSide pagination."}]};static dependencies=[{name:"hierarchy",required:!0,reason:"Builds the hierarchical row model",provide:()=>new g},{name:"multiSort",required:!1,reason:"Queries sort model for coordinated tree sorting"},{name:"serverSide",required:!1,reason:"Consumes datasource events for lazy-loaded tree data"}];name="tree";hierarchyStage="structure";styles='@layer tbw-plugins{tbw-grid .cell[data-field=__tbw_expander]{justify-content:flex-start;align-items:center;padding:0;display:flex;border-inline-end:none!important}tbw-grid .header-row .cell[data-field=__tbw_expander]{display:none}tbw-grid .header-row .cell[data-field=__tbw_expander]+.cell{grid-column:1/3}tbw-grid .tree-cell-wrapper{vertical-align:middle;align-items:center;padding-inline-start:calc(var(--tbw-tree-depth,0) * var(--tbw-tree-indent-width,var(--tbw-tree-toggle-size,1.25em)));display:inline-flex;overflow:visible}tbw-grid .tree-expander{box-sizing:border-box;justify-content:flex-start;align-items:center;width:100%;height:100%;display:flex}tbw-grid .tree-toggle{cursor:pointer;-webkit-user-select:none;user-select:none;width:var(--tbw-tree-toggle-size,1.25em);height:var(--tbw-tree-toggle-size,1.25em);flex-shrink:0;justify-content:center;align-items:center;display:inline-flex}tbw-grid .tree-toggle:hover{color:var(--tbw-tree-accent,var(--tbw-color-accent))}tbw-grid .tree-toggle{position:relative}tbw-grid .tree-toggle:after{content:"";inline-size:var(--tbw-touch-target-min,24px);block-size:var(--tbw-touch-target-min,24px);position:absolute;inset-block-start:50%;inset-inline-start:50%;translate:-50% -50%}tbw-grid .tree-spacer{width:var(--tbw-tree-toggle-size,1.25em);height:var(--tbw-tree-toggle-size,1.25em);flex-shrink:0;display:inline-block}@media (pointer:coarse){tbw-grid .tree-toggle,tbw-grid .tree-spacer{min-width:var(--tbw-touch-target-min,24px);min-height:var(--tbw-touch-target-min,24px)}}tbw-grid .tree-loading{--tbw-spinner-size:var(--tbw-tree-toggle-size,1.25em);flex-shrink:0}tbw-grid .data-grid-row.tbw-tree-slide-in{animation:tbw-tree-slide-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}tbw-grid .data-grid-row.tbw-tree-fade-in{animation:tbw-tree-fade-in var(--tbw-animation-duration,.2s) var(--tbw-animation-easing,ease-out) forwards}@keyframes tbw-tree-slide-in{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}@keyframes tbw-tree-fade-in{0%{opacity:0}to{opacity:1}}}';get defaultConfig(){return{childrenField:"children",autoDetect:!0,defaultExpanded:!1,indentWidth:20,showExpandIcons:!0,animation:"slide"}}expandedKeys=new Set;initialExpansionDone=!1;flattenedRows=[];rowKeyMap=new Map;previousVisibleKeys=new Set;keysToAnimate=new Set;#n=[];#i=0;sortState=null;#s=null;#o=new Map;#a=new WeakSet;#d=new WeakSet;#l=new WeakSet;#h=[];#c=new WeakMap;#u=new WeakMap;originalTreeColumnRenderer;originalTreeColumnRendererProperty="viewRenderer";wrappedTreeColumnField;#p=!1;#g=!1;detach(){this.expandedKeys.clear(),this.initialExpansionDone=!1,this.flattenedRows=[],this.#n=[],this.#i=0,this.rowKeyMap.clear(),this.previousVisibleKeys.clear(),this.keysToAnimate.clear(),this.sortState=null,this.#s=null,this.#a=new WeakSet,this.#d=new WeakSet,this.#l=new WeakSet,this.#h=[];for(const e of this.#o.values())e.controller.abort();this.#o.clear(),this.originalTreeColumnRenderer=void 0,this.originalTreeColumnRendererProperty="viewRenderer",this.wrappedTreeColumnField=void 0,this.#p=!1}handleQuery(e){if("canMoveRow"===e.type){const t=e.context,r=this.config.childrenField??"children",n=t?.[r];if(Array.isArray(n)&&n.length>0)return!1}if("datasource:viewport-mapping"===e.type){const{viewportStart:t,viewportEnd:r}=e.context;if(0===this.flattenedRows.length)return;return function(e,t,r,n){const i=e.length-1,s=t=>Math.max(0,e[Math.max(0,Math.min(t,i))]??0);return{startNode:s(r),endNode:s(n)+1,totalLoadedNodes:t}}(this.#n,this.#i,t,r)}}attach(e){super.attach(e),this.on("datasource:data",e=>{const t=e;t.claimed||(t.claimed=!0)}),this.on("datasource:children",e=>{const t=e;if("tree"!==t.context?.source)return;t.claimed=!0;const r=t.context.parentNode;if(r){const e=this.#f(r),n=this.#w(e,r);if(!n&&(this.#d.has(r)||this.#l.has(r)))return;this.#y(r,t.rows??[]),this.broadcast("tree-load-end",{key:e,row:r,depth:n?.depth??this.rowKeyMap.get(e)?.depth??0,childCount:t.rows?.length??0}),this.requestRender()}}),this.on("datasource:error",e=>{const t=e;if("tree"!==t.context?.source)return;const r=t.context.parentNode;if(!r)return;const n=this.#f(r),i=this.#w(n,r);i&&(this.broadcast("tree-load-error",{key:n,row:r,depth:i.depth,error:t.error}),this.requestRender())})}get animationStyle(){return!!this.isAnimationEnabled&&(this.config.animation??"slide")}detect(e){if(!this.config.autoDetect)return!1;const t=e,r=this.config.childrenField??function(e){if(!Array.isArray(e)||0===e.length)return null;const t=["children","items","nodes","subRows","nested"];for(const r of e)if(r&&"object"==typeof r)for(const e of t){const t=r[e];if(Array.isArray(t)&&t.length>0)return e}return null}(t)??"children";return x(t,r)}processRows(e){const t=this.grid?.getPluginByName?.("hierarchy");return t?t.process(e,this):h(e,[this])}beginHierarchy(e){const t=e;if(0===t.length||!x(t,this.config.childrenField??"children",this.config.hasChildren))return this.flattenedRows=[],this.#n=[],this.#i=0,this.rowKeyMap.clear(),this.previousVisibleKeys.clear(),this.#m(t),this.#b(),!1;const r="server"===this.grid?.query?.("datasource:sort-mode",null)?.[0];return this.#s=r?null:this.resolveEffectiveSortState(),this.config.defaultExpanded&&!this.initialExpansionDone&&(this.expandedKeys=y(t,this.config),this.initialExpansionDone=!0),this.#u=new WeakMap,!0}processSiblings(e,t){const{parentKey:r,depth:n}=t,i=this.config.childrenField??"children",s=this.config.hasChildren;for(let d=0;d<e.length;d++)this.#x(e[d].row,d,r);const o=this.#s,a=o?this.#v(e,o.field,o.direction):e;for(let d=0;d<a.length;d++){const e=a[d],t=e.row,o=this.#x(t,d,r),l=t[i],h=Array.isArray(l)&&l.length>0,c=h||!0!==t.__loading&&(s?s(t):null!=l&&!Array.isArray(l)&&!!l),u=this.expandedKeys.has(o);this.#u.set(t,{key:o,data:t,depth:n,hasChildren:c,isExpanded:u,parentKey:r,posInSet:d+1,setSize:a.length}),e.key=o,h&&u&&(e.expanded=!0,e.children=l.map(e=>({row:e})))}return a}endHierarchy(e,t){const r=this.#u;this.#u=new WeakMap,this.flattenedRows=[],this.rowKeyMap.clear(),this.keysToAnimate.clear();const n=new Set,i=new Array(e.length);let s=-1;for(let o=0;o<e.length;o++){const a=e[o],d=r.get(a);if(null===d?.parentKey&&s++,i[o]=s,!d)continue;const l=t(a)?.level;void 0!==l&&(d.depth=l-1),this.flattenedRows.push(d),this.rowKeyMap.set(d.key,d),this.#u.set(a,d),n.add(d.key),!this.previousVisibleKeys.has(d.key)&&d.depth>0&&this.keysToAnimate.add(d.key),this.#S(d)&&this.#R(d)}return this.previousVisibleKeys=n,this.#n=i,this.#i=s+1,this.#b(),!0}#b(){this.#g||(this.#g=!0,queueMicrotask(()=>{this.#g=!1,this.grid&&this.#p!==this.flattenedRows.length>0&&this.requestColumnsRender()}))}#x(e,t,r){if(void 0!==e.id){const t=String(e.id);return this.#c.set(e,t),t}const n=this.#c.get(e);if(void 0!==n)return n;const i=r?`${r}-${t}`:String(t);return this.#c.set(e,i),i}#v(e,t,r){const n=new Map;for(const i of e)n.set(i.row,i);return this.#k(e.map(e=>e.row),t,r).map(e=>n.get(e)??{row:e})}#k(e,r,n){const i=this.grid,s=i?._columns??[],o={field:r,direction:n},a=[...e],d=(i?.effectiveConfig?.sortHandler??t.builtInSort)(a,o,s);return d&&"function"==typeof d.then?(d.catch(()=>{}),(0,t.builtInSort)(a,o,s)):d}#f(e){return this.#c.get(e)??String(e.id??"?")}#y(e,t){e[this.config.childrenField??"children"]=t,this.#a.add(e)}#C(e){if(this.#a.has(e))return!1;const t=e[this.config.childrenField??"children"];if(Array.isArray(t)&&t.length>0)return!1;const r=this.config.hasChildren;return r?r(e):null!=t&&!Array.isArray(t)&&!!t}#S(e){if(!0===e.data.__loading)return!1;const t=this.#o.get(e.key);return t?t.row!==e.data:e.isExpanded&&e.hasChildren&&!this.#d.has(e.data)&&this.#C(e.data)}#m(e){if(0===this.#o.size)return;const t=new Map;for(let r=0;r<e.length;r++){const n=e[r];if(!0===n.__loading)continue;const i=void 0!==n.id?String(n.id):this.#c.get(n)??String(r);this.#o.has(i)&&t.set(i,n)}for(const[r,n]of t)this.#o.get(r)?.row!==n&&this.#R({key:r,data:n,depth:0,hasChildren:!1,isExpanded:!1,parentKey:null,posInSet:0,setSize:0},!0)}#R(e,t=!1){this.#h.push({meta:e,flat:t})>1||queueMicrotask(()=>{const e=this.#h;if(this.#h=[],!this.grid)return;let t=!1;for(const{meta:r,flat:n}of e){if(!n&&this.rowKeyMap.get(r.key)?.data!==r.data)continue;const e=this.#o.get(r.key);e&&e.row!==r.data&&this.#A(r.key),n||!this.expandedKeys.has(r.key)||this.#d.has(r.data)||(this.#d.add(r.data),t=this.requestLazyChildren(r)||t)}t&&this.requestRender()})}requestLazyChildren(e){const{key:t,depth:r,data:n}=e,i=this.#o.get(t);if(i?.row===n)return!1;if(!this.#C(n))return!1;const s=this.queryBoolean("datasource:is-active"),{loadChildren:o}=this.config;if(!s&&!o)return!1;i&&this.#A(t);const a=new AbortController;return this.#o.set(t,{row:n,depth:r,controller:a}),this.#d.add(n),this.#l.delete(n),this.broadcast("tree-load-start",{key:t,row:n,depth:r}),s?(this.grid.query("datasource:fetch-children",{context:{source:"tree",parentNode:n,nodePath:[t]},signal:a.signal}),!0):(this.#K(t,n,r,a),!0)}#q(e){return this.#o.get(e.key)?.row===e.data}#w(e,t){const r=this.#o.get(e);if(r?.row===t)return this.#o.delete(e),r}#A(e){const t=this.#o.get(e);t&&(this.#o.delete(e),this.#d.delete(t.row),this.#l.add(t.row),t.controller.abort(),this.broadcast("tree-load-end",{key:e,row:t.row,depth:t.depth,childCount:0,cancelled:!0}))}#K(e,t,r,n){const i=this.config.loadChildren,s=()=>!n.signal.aborted&&(this.#o.delete(e),!0),o=n=>{if(!s())return;const i=n??[];this.#y(t,i),this.broadcast("tree-load-end",{key:e,row:t,depth:r,childCount:i.length}),this.requestRender()},a=n=>{s()&&(this.broadcast("tree-load-error",{key:e,row:t,depth:r,error:n}),this.requestRender())};let d;try{d=i({row:t,key:e,depth:r,signal:n.signal})}catch(l){return void a(l)}if(d&&"function"==typeof d.subscribe){let e=!1,t=null;const r=()=>{e=!0,t?.unsubscribe(),t=null},i=d.subscribe({next:e=>{o(e),r()},error:e=>{a(e),r()}});return e?void i.unsubscribe():(t=i,void n.signal.addEventListener("abort",r,{once:!0}))}Promise.resolve(d).then(o,a)}resolveEffectiveSortState(){const e=this.grid?.query?.("sort:get-model",null);if(Array.isArray(e)&&e.length>0){const t=e[0];if(Array.isArray(t)&&t.length>0)return{field:t[0].field,direction:"desc"===t[0].direction?-1:1}}return this.sortState}processColumns(e){if(0===this.flattenedRows.length){const t=[...e];if(this.#p){const e=t.findIndex(e=>e.field===this.wrappedTreeColumnField);e>=0&&(t[e]={...t[e],renderer:"renderer"===this.originalTreeColumnRendererProperty?this.originalTreeColumnRenderer:void 0})}return this.#p=!1,this.originalTreeColumnRenderer=void 0,this.originalTreeColumnRendererProperty="viewRenderer",this.wrappedTreeColumnField=void 0,t}const t=[...e];if(0===t.length)return t;const{treeColumn:i}=this.config;let s=0;if(i){const e=t.findIndex(e=>e.field===i);e>=0&&(s=e)}const o=t[s],a=o.field;this.wrappedTreeColumnField!==a&&(this.originalTreeColumnRenderer=o.renderer||o.viewRenderer,this.originalTreeColumnRendererProperty=o.renderer?"renderer":"viewRenderer",this.wrappedTreeColumnField=a);const d=this.originalTreeColumnRenderer,h=()=>this.config,c=this.setIcon.bind(this);return t[s]={...o,renderer:e=>{const{row:t,value:i}=e,{showExpandIcons:s=!0,indentWidth:o}=h(),a=this.#u.get(t),u=a?.depth??0,p=document.createElement("span");if(p.className="tree-cell-wrapper",p.style.setProperty("--tbw-tree-depth",String(u)),void 0!==o&&p.style.setProperty("--tbw-tree-indent-width",`${o}px`),s)if(a&&this.#q(a)){const e=function(e){const t=document.createElement("div");return t.className=`tbw-spinner tbw-spinner--${e}`,t.setAttribute("role","progressbar"),t.setAttribute("aria-label","Loading"),t}("small");e.classList.add("tree-loading"),p.appendChild(e)}else if(a&&a.hasChildren){const e=document.createElement("span");e.className=`${n}${a.isExpanded?` ${r}`:""}`,c(e,a.isExpanded?"collapse":"expand"),e.setAttribute("data-tree-key",a.key),p.appendChild(e)}else{const e=document.createElement("span");e.className="tree-spacer",p.appendChild(e)}const g=document.createElement("span");if(g.className="tree-content",d){const t=d(e);t instanceof Node?g.appendChild(t):"string"==typeof t&&(f=g,(w=t)&&"string"==typeof w?-1!==w.indexOf("<")||-1!==w.indexOf("&")?f.replaceChildren(l(w)):f.textContent=w:f.replaceChildren())}else g.textContent=null!=i?String(i):"";var f,w;return p.appendChild(g),p}},this.#p=!0,t}onCellClick(e){const t=e.originalEvent?.target;if(!t?.classList.contains(n))return!1;const r=t.getAttribute("data-tree-key");if(!r)return!1;const i=this.rowKeyMap.get(r);return!!i&&(this.expandedKeys=w(this.expandedKeys,r),this.expandedKeys.has(r)&&this.requestLazyChildren(i),this.broadcast("tree-expand",{key:r,row:i.data,expanded:this.expandedKeys.has(r),depth:i.depth,expandedKeys:[...this.expandedKeys]}),this.requestRender(),!0)}onKeyDown(e){if(" "!==e.key)return;if(function(e){const t=e.composedPath?.()??[],r=t.length?t[0]:e.target;if(!(r instanceof HTMLElement))return!1;const n=r.tagName;return"INPUT"===n||"SELECT"===n||"TEXTAREA"===n||r.isContentEditable}(e))return;const t=this.grid._focusRow,r=this.#u.get(this.rows[t]);return r?.hasChildren?(e.preventDefault(),this.expandedKeys=w(this.expandedKeys,r.key),this.expandedKeys.has(r.key)&&this.requestLazyChildren(r),this.broadcast("tree-expand",{key:r.key,row:r.data,expanded:this.expandedKeys.has(r.key),depth:r.depth,expandedKeys:[...this.expandedKeys]}),this.requestRenderWithFocus(),!0):void 0}onHeaderClick(e){if(0===this.flattenedRows.length||!e.column.sortable)return!1;const t=this.grid?.query?.("sort:get-model",null);if(Array.isArray(t)&&t.length>0)return!1;const{field:r}=e.column;this.sortState&&this.sortState.field===r?1===this.sortState.direction?this.sortState={field:r,direction:-1}:this.sortState=null:this.sortState={field:r,direction:1};const n=this.grid;return void 0!==n._sortState&&(n._sortState=this.sortState?{...this.sortState}:null),this.broadcast("sort-change",{field:r,direction:this.sortState?.direction??0}),this.requestRender(),!0}afterRender(){const e=this.gridElement?.querySelector(".rows");if(!e)return;const t=this.animationStyle,r=!1!==t&&this.keysToAnimate.size>0,n="fade"===t?"tbw-tree-fade-in":"tbw-tree-slide-in";for(const i of e.querySelectorAll(".data-grid-row")){const e=this.#u.get(i.__rowDataRef);e&&(e.hasChildren?i.setAttribute("aria-expanded",String(e.isExpanded)):i.hasAttribute("aria-expanded")&&i.removeAttribute("aria-expanded"),i.classList.toggle("tbw-row-expanded",e.hasChildren&&e.isExpanded),this.#q(e)?i.setAttribute("aria-busy","true"):i.hasAttribute("aria-busy")&&!i.classList.contains("tbw-row-loading")&&i.removeAttribute("aria-busy"),r&&e.key&&this.keysToAnimate.has(e.key)&&(i.classList.add(n),i.addEventListener("animationend",()=>i.classList.remove(n),{once:!0})))}this.keysToAnimate.clear()}expand(e){this.expandedKeys.add(e);const t=this.rowKeyMap.get(e);t&&this.requestLazyChildren(t),this.requestRender()}collapse(e){this.expandedKeys.delete(e),this.requestRender()}toggle(e){this.expandedKeys=w(this.expandedKeys,e);const t=this.rowKeyMap.get(e);t?(this.expandedKeys.has(e)&&this.requestLazyChildren(t),this.broadcast("tree-expand",{key:e,row:t.data,expanded:this.expandedKeys.has(e),depth:t.depth,expandedKeys:[...this.expandedKeys]})):this.emitPluginEvent("tree-expand",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}expandAll(){this.expandedKeys=y(this.rows,this.config),this.emitPluginEvent("tree-expand",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}collapseAll(){this.expandedKeys=new Set,this.emitPluginEvent("tree-expand",{expandedKeys:[...this.expandedKeys]}),this.requestRender()}isExpanded(e){return this.expandedKeys.has(e)}getExpandedKeys(){return[...this.expandedKeys]}getFlattenedRows(){return[...this.flattenedRows]}getRowMeta(e){return this.#u.get(e)}getRowByKey(e){return this.rowKeyMap.get(e)?.data}expandToKey(e){this.expandedKeys=b(this.rows,e,this.config,this.expandedKeys),this.requestRender()}};e.TreePlugin=v});
|
|
2
2
|
//# sourceMappingURL=tree.umd.js.map
|