@toolbox-web/grid 3.8.3 → 3.9.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.d.ts +1 -0
- 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/plugin/base-plugin.d.ts +14 -0
- package/lib/core/plugin/plugin-manager.d.ts +5 -1
- package/lib/core/plugin/types.d.ts +2 -0
- package/lib/features/registry.js +1 -1
- package/lib/plugins/clipboard/index.js +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-columns/index.js +1 -1
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/grouping-rows-plugin.d.ts +20 -24
- package/lib/plugins/grouping-rows/grouping-rows.d.ts +9 -4
- package/lib/plugins/grouping-rows/index.d.ts +1 -1
- package/lib/plugins/grouping-rows/index.js +1 -1
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/grouping-rows/types.d.ts +32 -4
- package/lib/plugins/hierarchy/hierarchy-plugin.d.ts +79 -0
- package/lib/plugins/hierarchy/hierarchy.d.ts +28 -0
- package/lib/plugins/hierarchy/index.d.ts +9 -0
- package/lib/plugins/hierarchy/index.js +2 -0
- package/lib/plugins/hierarchy/index.js.map +1 -0
- package/lib/plugins/hierarchy/types.d.ts +92 -0
- 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 +1 -1
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/index.js +1 -1
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js +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/print/index.js +1 -1
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/responsive/index.js +1 -1
- package/lib/plugins/responsive/index.js.map +1 -1
- package/lib/plugins/row-drag-drop/index.js +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 +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/shell/index.js +1 -1
- package/lib/plugins/shell/index.js.map +1 -1
- package/lib/plugins/sticky-rows/index.js +1 -1
- package/lib/plugins/sticky-rows/index.js.map +1 -1
- package/lib/plugins/tooltip/index.js +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/tree/tree-datasource.d.ts +7 -10
- package/lib/plugins/tree/tree-plugin.d.ts +23 -3
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/visibility/index.js +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/grouping-rows.umd.js +1 -1
- package/umd/plugins/grouping-rows.umd.js.map +1 -1
- package/umd/plugins/hierarchy.umd.js +2 -0
- package/umd/plugins/hierarchy.umd.js.map +1 -0
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/defaults.ts","../../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../../libs/grid/src/lib/core/internal/pointer-modality.ts","../../../../../../libs/grid/src/lib/core/internal/utils.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../../libs/grid/src/lib/plugins/shared/drag-alternative-menu.ts","../../../../../../libs/grid/src/lib/plugins/selection/column-selection.ts","../../../../../../libs/grid/src/lib/plugins/selection/range-selection.ts","../../../../../../libs/grid/src/lib/plugins/selection/selection.css?inline","../../../../../../libs/grid/src/lib/core/internal/pointer-drag.ts","../../../../../../libs/grid/src/lib/plugins/selection/touch-selection.ts","../../../../../../libs/grid/src/lib/plugins/selection/selection-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * Runtime default values for grid configuration.\n *\n * Kept out of `core/types.ts` so that file stays type-only apart from the two\n * enum-like consts whose `type` aliases are derived from them via `typeof`.\n * Import types from `./types`; import the values that back them from here.\n */\n\nimport type { A11yMessages, AnimationConfig, GridIcons } from './types';\n\n/**\n * Default English announcement messages.\n * Used when no custom messages are provided via {@link A11yConfig.messages}.\n * @since 2.0.0\n */\nexport const DEFAULT_A11Y_MESSAGES: Required<A11yMessages> = {\n sortApplied: (column, direction) => `Sorted by ${column}, ${direction}`,\n sortCleared: () => 'Sort cleared',\n filterApplied: (column) => `Filter applied on ${column}`,\n filterCleared: (column) => `Filter cleared from ${column}`,\n allFiltersCleared: () => 'All filters cleared',\n groupExpanded: (name, count) => `Group ${name} expanded, ${count} rows`,\n groupCollapsed: (name) => `Group ${name} collapsed`,\n selectionChanged: (count) => `${count} rows selected`,\n selectAllRows: () => 'Select all rows',\n selectRow: (rowIndex) => `Select row ${rowIndex + 1}`,\n columnSelected: (label) => `Column ${label} selected`,\n columnSelectionChanged: (count) => `${count} columns selected`,\n columnSelectionCleared: () => 'Column selection cleared',\n selectionAxisChanged: (toAxis) =>\n toAxis === 'column'\n ? 'Row selection cleared, column selection active'\n : 'Column selection cleared, row selection active',\n rangeSpanSelected: (axis, count) =>\n count === 1 ? `${axis === 'row' ? 'Row' : 'Column'} selected` : `${count} ${axis}s selected`,\n editingStarted: (rowIndex) => `Editing row ${rowIndex + 1}`,\n editingCommitted: (rowIndex) => `Row ${rowIndex + 1} saved`,\n dataLoaded: (count) => `${count} rows loaded`,\n};\n\n/**\n * Default animation configuration\n * @since 0.2.7\n */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n/**\n * Default icons used when not overridden. Most entries are short text/emoji;\n * `filter` and `filterActive` are empty strings because the actual rendering\n * is driven by the `--tbw-icon-filter[-active]-mask` CSS custom properties\n * (see `core/styles/variables.css`). Userland that wants an HTML/SVG fallback\n * via `gridConfig.icons.filter = '<svg…>'` is unaffected — that path overrides this default.\n * @since 0.1.1\n */\nexport const DEFAULT_GRID_ICONS: Required<GridIcons> = {\n expand: '▶',\n collapse: '▼',\n sortAsc: '▲',\n sortDesc: '▼',\n sortNone: '⇅',\n submenuArrow: '▶',\n dragHandle: '⋮⋮',\n toolPanel: '☰',\n filter: '',\n filterActive: '',\n print: '🖨️',\n};\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","/**\n * Pointer modality detection — distinguishes coarse (touch/stylus) from fine\n * (mouse/precision trackpad) input devices via the `pointer: coarse` media query.\n *\n * This module is **internal only** and is NOT exported from `src/public.ts`.\n * Plugins and features use it to adapt hit-target sizing, long-press thresholds,\n * and gesture disambiguation at runtime without querying the media each call.\n *\n * Design decisions:\n * - A single `MediaQueryList` instance is shared across all subscribers to avoid\n * N OS-level listeners. Subscribers share one `change` handler.\n * - Guards for missing `globalThis.matchMedia` keep the module SSR/happy-dom safe.\n * - Legacy `addListener`/`removeListener` fallback for older Safari and happy-dom\n * environments that predate `MediaQueryList.addEventListener`.\n *\n * @since 3.5.0\n * @internal\n */\n\n// #region Types\n\n/**\n * The resolved pointer category for the primary input device.\n *\n * - `'fine'` — mouse or precision trackpad (pointer: fine).\n * - `'coarse'` — touch screen or low-precision stylus (pointer: coarse).\n *\n * @since 3.5.0\n */\nexport type PointerModality = 'fine' | 'coarse';\n\n// #endregion\n\n// #region Singleton MQL + subscriber registry\n\n/** Lazily-created shared `MediaQueryList` for `(pointer: coarse)`. */\nlet _mql: MediaQueryList | null = null;\n\n/** Active change subscribers. */\nconst _subscribers = new Set<(m: PointerModality) => void>();\n\n/**\n * Resolve the current modality from a `MediaQueryList` state.\n * Returns `'coarse'` when the query matches, `'fine'` otherwise.\n */\nfunction _resolve(mql: MediaQueryList): PointerModality {\n return mql.matches ? 'coarse' : 'fine';\n}\n\n/**\n * Shared MQL change handler — dispatches to all active subscribers.\n */\nfunction _onMqlChange(e: MediaQueryListEvent): void {\n const modality: PointerModality = e.matches ? 'coarse' : 'fine';\n _subscribers.forEach((cb) => cb(modality));\n}\n\n/**\n * Return (and lazily create) the shared MQL.\n * Returns `null` in environments without `matchMedia` (SSR / happy-dom).\n */\nfunction _getMql(): MediaQueryList | null {\n if (_mql) return _mql;\n if (typeof globalThis.matchMedia !== 'function') return null;\n _mql = globalThis.matchMedia('(pointer: coarse)');\n // Attach the single shared change handler via the modern or legacy API.\n if (typeof _mql.addEventListener === 'function') {\n _mql.addEventListener('change', _onMqlChange);\n } else if (typeof (_mql as { addListener?: unknown }).addListener === 'function') {\n // Legacy Safari / older environments\n (_mql as { addListener: (cb: (e: MediaQueryListEvent) => void) => void }).addListener(_onMqlChange);\n }\n return _mql;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Return the current pointer modality for the primary input device.\n *\n * Falls back to `'fine'` in SSR environments or browsers that do not\n * support `matchMedia`.\n *\n * @since 3.5.0\n */\nexport function getPrimaryPointer(): PointerModality {\n const mql = _getMql();\n if (!mql) return 'fine';\n return _resolve(mql);\n}\n\n/**\n * Subscribe to pointer-modality changes.\n *\n * The callback fires whenever the browser detects a switch between fine and\n * coarse primary input (e.g. user connects a mouse to a touch-screen device).\n *\n * Returns an **idempotent** unsubscribe function — calling it multiple times\n * is safe.\n *\n * In SSR / environments without `matchMedia` the callback is never invoked\n * and the returned unsubscribe is a no-op.\n *\n * @since 3.5.0\n */\nexport function onPointerModalityChange(cb: (m: PointerModality) => void): () => void {\n _getMql(); // ensure MQL is created and the shared handler is attached\n _subscribers.add(cb);\n let removed = false;\n return (): void => {\n if (removed) return;\n removed = true;\n _subscribers.delete(cb);\n };\n}\n\n// #endregion\n","// #region Environment Helpers\n\n/**\n * Check if we're running in a development environment.\n * Returns true for localhost or when NODE_ENV !== 'production'.\n * Used to show warnings only in development.\n */\nexport function isDevelopment(): boolean {\n // Check for localhost (browser environment)\n if (typeof window !== 'undefined' && window.location) {\n const hostname = window.location.hostname;\n if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {\n return true;\n }\n }\n // Check for NODE_ENV (build-time or SSR)\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Cell Rendering Helpers\n\n/**\n * Generate accessible HTML for a boolean cell.\n * Uses role=\"checkbox\" with proper aria attributes.\n */\nexport function booleanCellHTML(value: boolean): string {\n return `<span role=\"checkbox\" aria-checked=\"${value}\" aria-label=\"${value}\">${value ? '🗹' : '☐'}</span>`;\n}\n\n/**\n * Format a date value for display.\n * Handles Date objects, timestamps, and date strings.\n * Returns empty string for invalid dates.\n */\nexport function formatDateValue(value: unknown): string {\n if (value == null || value === '') return '';\n if (value instanceof Date) {\n return isNaN(value.getTime()) ? '' : value.toLocaleDateString();\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return isNaN(d.getTime()) ? '' : d.toLocaleDateString();\n }\n return '';\n}\n\n/**\n * Get the row index from a cell element's data-row attribute.\n * Falls back to calculating from parent row's DOM position if data-row is missing.\n * Returns -1 if no valid row index is found.\n */\nexport function getRowIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-row');\n if (attr) return parseInt(attr, 10);\n\n // Fallback: find the parent .data-grid-row and calculate index from siblings\n const rowEl = cell.closest('.data-grid-row');\n if (!rowEl) return -1;\n\n const parent = rowEl.parentElement;\n if (!parent) return -1;\n\n // Get all data-grid-row siblings and find this row's index\n const rows = parent.querySelectorAll(':scope > .data-grid-row');\n for (let i = 0; i < rows.length; i++) {\n if (rows[i] === rowEl) return i;\n }\n return -1;\n}\n\n/**\n * Get the column index from a cell element's data-col attribute.\n * Returns -1 if no valid column index is found.\n */\nexport function getColIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-col');\n return attr ? parseInt(attr, 10) : -1;\n}\n\n/**\n * Clear all cell-focus styling from a root element.\n * Used when changing focus or when selection plugin takes over focus management.\n */\nexport function clearCellFocus(root: Element | null): void {\n if (!root) return;\n root.querySelectorAll('.cell-focus').forEach((el) => el.classList.remove('cell-focus'));\n}\n// #endregion\n\n// #region RTL Helpers\n\n/** Text direction */\nexport type TextDirection = 'ltr' | 'rtl';\n\n/**\n * Get the text direction for an element.\n * Reads from the computed style, which respects the `dir` attribute on the element\n * or any ancestor, as well as CSS `direction` property.\n *\n * @param element - The element to check direction for\n * @returns 'ltr' or 'rtl'\n *\n * @example\n * ```typescript\n * // Detect grid's direction\n * const dir = getDirection(gridElement);\n * if (dir === 'rtl') {\n * // Handle RTL layout\n * }\n * ```\n */\nexport function getDirection(element: Element): TextDirection {\n // Try computed style first (works in real browsers)\n try {\n const computedDir = getComputedStyle(element).direction;\n if (computedDir === 'rtl') return 'rtl';\n } catch {\n // getComputedStyle may fail in some test environments\n }\n\n // Fallback: check dir attribute on element or ancestors\n // This handles test environments where getComputedStyle may not reflect dir attribute\n try {\n const dirAttr = element.closest?.('[dir]')?.getAttribute('dir');\n if (dirAttr === 'rtl') return 'rtl';\n } catch {\n // closest may not be available on mock elements\n }\n\n return 'ltr';\n}\n\n/**\n * Check if an element is in RTL mode.\n *\n * @param element - The element to check\n * @returns true if the element's text direction is right-to-left\n */\nexport function isRTL(element: Element): boolean {\n return getDirection(element) === 'rtl';\n}\n\n/**\n * Resolve a logical inline position to a physical position based on text direction.\n *\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n * - `'left'` / `'right'` → unchanged (physical values)\n *\n * @param position - Logical or physical position\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical position ('left' or 'right')\n *\n * @example\n * ```typescript\n * resolveInlinePosition('start', 'ltr'); // 'left'\n * resolveInlinePosition('start', 'rtl'); // 'right'\n * resolveInlinePosition('left', 'rtl'); // 'left' (unchanged)\n * ```\n */\nexport function resolveInlinePosition(\n position: 'left' | 'right' | 'start' | 'end',\n direction: TextDirection,\n): 'left' | 'right' {\n if (position === 'left' || position === 'right') {\n return position;\n }\n if (direction === 'rtl') {\n return position === 'start' ? 'right' : 'left';\n }\n return position === 'start' ? 'left' : 'right';\n}\n// #endregion\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__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'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n '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(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Base Grid Plugin Class\n *\n * All plugins extend this abstract class.\n * Plugins are instantiated per-grid and manage their own state.\n */\n\n// Injected by Vite at build time from package.json (same as grid.ts)\ndeclare const __GRID_VERSION__: string;\n\nimport type { HeaderContentDefinition, ToolPanelDefinition } from '../../plugins/shell/types';\nimport { DEFAULT_GRID_ICONS } from '../defaults';\nimport {\n type DiagnosticCode,\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n formatDiagnostic,\n gridPrefix,\n} from '../internal/diagnostics';\nimport { setSanitizedHTML } from '../internal/sanitize';\nimport type {\n ColumnConfig,\n ColumnState,\n GridConfig,\n GridIcons,\n GridPlugin,\n IconValue,\n PluginNameMap,\n Translate,\n} from '../types';\n\n// Re-export shared plugin types for convenience\nexport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellCoords,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n ContextMenuItem,\n ContextMenuParams,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n KeyboardModifiers,\n PluginCellRenderContext,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\n/**\n * Grid element interface for plugins.\n * Extends GridElementRef with plugin-specific methods.\n * Note: effectiveConfig is already available from GridElementRef.\n */\nexport interface GridElement extends GridElementRef {\n getPlugin<T extends GridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : BaseGridPlugin) | undefined;\n /**\n * Get a plugin's state by plugin name.\n * This is a loose-coupling method for plugins to access other plugins' state\n * without importing the plugin class directly.\n * @internal Plugin API\n */\n getPluginState?(name: string): unknown;\n\n // Focus Management\n /** Register an external DOM element as a logical focus container. @internal Plugin API */\n registerExternalFocusContainer?(el: Element): void;\n /** Unregister a previously registered external focus container. @internal Plugin API */\n unregisterExternalFocusContainer?(el: Element): void;\n /** Check whether focus is logically inside this grid (own DOM + external containers). @internal Plugin API */\n containsFocus?(node?: Node | null): boolean;\n}\n\n/**\n * Header render context for plugin header renderers.\n */\nexport interface PluginHeaderRenderContext {\n /** Column configuration */\n column: ColumnConfig;\n /** Column index */\n colIndex: number;\n}\n\n// ============================================================================\n// Plugin Dependency Types\n// ============================================================================\n\n/**\n * Declares a dependency on another plugin.\n *\n * @category Plugin Development\n * @example\n * ```typescript\n * export class UndoRedoPlugin extends BaseGridPlugin {\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true },\n * ];\n * }\n * ```\n * @since 0.4.1\n */\nexport interface PluginDependency {\n /**\n * The name of the required plugin (matches the plugin's `name` property).\n * Use string names for loose coupling - avoids static imports.\n */\n name: string;\n\n /**\n * Whether this dependency is required (hard) or optional (soft).\n * - `true`: Plugin cannot function without it. Throws error if missing.\n * - `false`: Plugin works with reduced functionality. Silent if missing\n * (set {@link severity} to `'warn'` or `'info'` to opt into a message).\n * @default true\n */\n required?: boolean;\n\n /**\n * Explicit severity when this dependency is missing. Overrides the default\n * derived from {@link required}.\n * - `'error'`: throw (halts grid setup). Default when `required` is not `false`.\n * - `'warn'`: log a console warning and continue (development only).\n * - `'info'`: log a verbose console.debug message and continue (development only).\n *\n * When omitted, a missing hard dependency throws and a missing soft\n * dependency is silent — preserving backward-compatible behavior.\n * @since 2.16.0\n */\n severity?: 'error' | 'warn' | 'info';\n\n /**\n * Optional predicate that gates whether this dependency applies, evaluated\n * against the depending plugin's own resolved configuration (defaults merged\n * with user config). Return `false` to skip the dependency entirely.\n *\n * Lets a plugin require/recommend another plugin only under certain config\n * — e.g. a plugin that needs a tool-panel host only when its tool panel is\n * enabled. When omitted, the dependency always applies.\n * @example when: (cfg: PivotConfig) => cfg.showToolPanel === true\n * @since 2.16.0\n */\n when?: (pluginConfig: unknown) => boolean;\n\n /**\n * Human-readable reason for this dependency.\n * Shown in error/info messages to help users understand why.\n * @example \"UndoRedoPlugin needs EditingPlugin to track cell edits\"\n */\n reason?: string;\n}\n\n/**\n * Declares an incompatibility between plugins.\n * When both plugins are loaded, a warning is logged to help users understand the conflict.\n *\n * @category Plugin Development\n */\nexport interface PluginIncompatibility {\n /**\n * The name of the incompatible plugin (matches the plugin's `name` property).\n */\n name: string;\n\n /**\n * Human-readable reason for the incompatibility.\n * Should explain why the plugins conflict and any workarounds.\n * @example \"Both transform the entire row model in different ways\"\n */\n reason: string;\n}\n\n// ============================================================================\n// Plugin Query & Event Definitions\n// ============================================================================\n\n/**\n * Defines a query that a plugin can handle.\n * Other plugins or the grid can send this query type to retrieve data.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * queries: [\n * {\n * type: 'canMoveColumn',\n * description: 'Check if a column can be moved/reordered',\n * },\n * ]\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * return this.canMoveColumn(query.context);\n * }\n * }\n * ```\n * @since 1.8.0\n */\nexport interface QueryDefinition {\n /**\n * The query type identifier (e.g., 'canMoveColumn', 'getContextMenuItems').\n * Should be unique across all plugins.\n */\n type: string;\n\n /**\n * Human-readable description of what the query does.\n */\n description?: string;\n}\n\n/**\n * Defines an event that a plugin can emit.\n * Other plugins can subscribe to these events via the Event Bus.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * events: [\n * {\n * type: 'filter-change',\n * description: 'Emitted when filter criteria change',\n * },\n * ]\n *\n * // In plugin class - emit\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // In another plugin - subscribe\n * this.on('filter-change', (detail) => console.log('Filter changed:', detail));\n * ```\n * @since 1.8.0\n */\nexport interface EventDefinition {\n /**\n * The event type identifier (e.g., 'filter-change', 'selection-change').\n * Used when emitting and subscribing to events.\n */\n type: string;\n\n /**\n * Human-readable description of what the event represents.\n */\n description?: string;\n\n /**\n * Whether this event is cancelable via `event.preventDefault()`.\n * @default false\n */\n cancelable?: boolean;\n}\n\n// ============================================================================\n// Plugin Manifest Types\n// ============================================================================\n\n/**\n * Defines a property that a plugin \"owns\" - used for configuration validation.\n * When this property is used without the owning plugin loaded, an error is thrown.\n *\n * @category Plugin Development\n */\nexport interface PluginPropertyDefinition {\n /** The property name on column or grid config */\n property: string;\n /** Whether this is a column-level or config-level property */\n level: 'column' | 'config';\n /** Human-readable description for error messages (e.g., 'the \"editable\" column property') */\n description: string;\n /** Import path hint for error messages (e.g., \"import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\") */\n importHint?: string;\n /** Custom check for whether property is considered \"used\" (default: truthy value check) */\n isUsed?: (value: unknown) => boolean;\n}\n\n/**\n * A configuration validation rule for detecting invalid/conflicting settings.\n * Plugins declare rules in their manifest; the validator executes them during grid initialization.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n */\nexport interface PluginConfigRule<TConfig = unknown> {\n /** Rule identifier for debugging (e.g., 'selection/range-dblclick') */\n id: string;\n /** Severity: 'error' throws, 'warn' logs warning */\n severity: 'error' | 'warn';\n /** Human-readable message shown when rule is violated */\n message: string;\n /** Predicate returning true if the rule is VIOLATED (i.e., config is invalid) */\n check: (pluginConfig: TConfig) => boolean;\n}\n\n/**\n * Hook names that can have execution priority configured.\n *\n * @category Plugin Development\n */\nexport type HookName =\n | 'processColumns'\n | 'processConfig'\n | 'processRows'\n | 'afterStructuralRender'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onHeaderClick'\n | 'onKeyDown'\n | 'onRowClick'\n | 'onScroll'\n | 'onScrollRender';\n\n/**\n * Static metadata about a plugin's capabilities and requirements.\n * Declared as a static property on plugin classes.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n *\n * @example\n * ```typescript\n * export class MyPlugin extends BaseGridPlugin<MyConfig> {\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'my-plugin/invalid-combo', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * readonly name = 'myPlugin';\n * }\n * ```\n * @since 1.1.0\n */\nexport interface PluginManifest<TConfig = unknown> {\n /**\n * Properties this plugin owns - validated by validate-config.ts.\n * If a user uses one of these properties without loading the plugin, an error is thrown.\n */\n ownedProperties?: PluginPropertyDefinition[];\n\n /**\n * Hook execution priority (higher = later, default 0).\n * Use negative values to run earlier, positive to run later.\n */\n hookPriority?: Partial<Record<HookName, number>>;\n\n /**\n * Configuration validation rules - checked during grid initialization.\n * Rules with severity 'error' throw, 'warn' logs to console.\n */\n configRules?: PluginConfigRule<TConfig>[];\n\n /**\n * Plugins that are incompatible with this plugin.\n * When both plugins are loaded together, a warning is shown.\n *\n * @example\n * ```typescript\n * incompatibleWith: [\n * { name: 'tree', reason: 'Both transform the entire row model in different ways' },\n * ],\n * ```\n */\n incompatibleWith?: PluginIncompatibility[];\n\n /**\n * Queries this plugin can handle.\n * Declares what query types this plugin responds to via `handleQuery()`.\n * This replaces the centralized PLUGIN_QUERIES approach with manifest-declared queries.\n *\n * @example\n * ```typescript\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * ```\n */\n queries?: QueryDefinition[];\n\n /**\n * Events this plugin can emit.\n * Declares what event types other plugins can subscribe to via `on()`.\n *\n * @example\n * ```typescript\n * events: [\n * { type: 'filter-change', description: 'Emitted when filter criteria change' },\n * ],\n * ```\n */\n events?: EventDefinition[];\n\n /**\n * Whether this plugin's `processRows` hook injects or removes rows\n * (group headers, tree nodes, pivot aggregates, placeholders, etc.).\n *\n * When true, the core sorting module delegates to the render scheduler\n * instead of sorting `_rows` in-place, because `_rows` may contain\n * plugin-generated marker objects that would be corrupted by a direct sort.\n *\n * Plugins that only **filter** or **reorder** existing rows (e.g. Filtering,\n * MultiSort) should leave this unset or set it to `false`.\n */\n modifiesRowStructure?: boolean;\n}\n\n/** Convert camelCase icon key to kebab-case data-icon attribute value. */\nexport function toIconAttr(key: string): string {\n return key.replace(/([A-Z])/g, '-$1').toLowerCase();\n}\n\n/**\n * Abstract base class for all grid plugins.\n *\n * @category Plugin Development\n * @template TConfig - Configuration type for the plugin\n * @since 0.1.1\n */\nexport abstract class BaseGridPlugin<TConfig = unknown> implements GridPlugin {\n /**\n * Plugin dependencies - declare other plugins this one requires.\n *\n * Dependencies are validated when the plugin is attached.\n * Required dependencies throw an error if missing. Optional dependencies are\n * silent by default; set a {@link PluginDependency.severity} of `'warn'` or\n * `'info'` to opt into a development-only log.\n *\n * @example\n * ```typescript\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },\n * { name: 'selection', required: false, reason: 'Enables selection-based undo' },\n * ];\n * ```\n */\n static readonly dependencies?: PluginDependency[];\n\n /**\n * Plugin manifest - declares owned properties, config rules, and hook priorities.\n *\n * This is read by the configuration validator to:\n * - Validate that required plugins are loaded when their properties are used\n * - Execute configRules to detect invalid/conflicting settings\n * - Order hook execution based on priority\n *\n * @example\n * ```typescript\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static readonly manifest?: PluginManifest<any>;\n\n /** Unique plugin identifier (derived from class name by default) */\n abstract readonly name: string;\n\n /**\n * Alternative names for backward compatibility.\n * `getPluginByName()` matches against both `name` and `aliases`.\n * @internal\n */\n readonly aliases?: readonly string[];\n\n /**\n * Plugin version - defaults to grid version for built-in plugins.\n * Third-party plugins can override with their own semver.\n */\n readonly version: string = typeof __GRID_VERSION__ !== 'undefined' ? __GRID_VERSION__ : 'dev';\n\n /** CSS styles to inject via `document.adoptedStyleSheets` */\n readonly styles?: string;\n\n /** Custom cell renderers keyed by type name */\n readonly cellRenderers?: Record<string, CellRenderer>;\n\n /** Custom header renderers keyed by type name */\n readonly headerRenderers?: Record<string, HeaderRenderer>;\n\n /** Custom cell editors keyed by type name */\n readonly cellEditors?: Record<string, CellEditor>;\n\n /** The grid instance this plugin is attached to */\n protected grid!: GridElement;\n\n /** Plugin configuration - merged with defaults in attach() */\n protected config!: TConfig;\n\n /** User-provided configuration from constructor */\n protected readonly userConfig: Partial<TConfig>;\n\n /**\n * Plugin-level AbortController for event listener cleanup.\n * Created fresh in attach(), aborted in detach().\n * This ensures event listeners are properly cleaned up when plugins are re-attached.\n */\n #abortController?: AbortController;\n\n /**\n * Default configuration - subclasses should override this getter.\n * Note: This must be a getter (not property initializer) for proper inheritance\n * since property initializers run after parent constructor.\n */\n protected get defaultConfig(): Partial<TConfig> {\n return {};\n }\n\n constructor(config: Partial<TConfig> = {}) {\n this.userConfig = config;\n }\n\n /**\n * Effective configuration (defaults merged with user config), resolved\n * eagerly so it is available **before** {@link attach} runs. Dependency\n * validation happens before `attach()` merges {@link config}, so config\n * -conditional dependency `when` predicates evaluate against this getter.\n *\n * While attached, returns the already-merged {@link config}; otherwise\n * recomputes `defaultConfig` + `userConfig` fresh. `config` is intentionally\n * **not** trusted when detached because {@link detach} does not clear it — a\n * re-validated plugin would otherwise see stale config from a prior\n * attachment. The {@link attach}-managed abort controller is the\n * attached/detached signal.\n * @internal Plugin infrastructure (used by `validatePluginDependencies`).\n */\n get resolvedConfig(): TConfig {\n return (this.#abortController ? this.config : { ...this.defaultConfig, ...this.userConfig }) as TConfig;\n }\n\n /**\n * Merge user-supplied configuration from other plugin instances into this\n * plugin's `userConfig`. Used by `PluginManager.attachAll()`'s alias-collapse\n * pre-pass: when a consumer instantiates the same plugin more than once (or\n * under an alias name), only one instance is attached, and the other\n * instances' configs are folded in via this method.\n *\n * Merge rules (shallow):\n *\n * - Same key, **same value** → silent.\n * - Same key, **different values** (incl. function vs function) → throws\n * a diagnostic naming the conflicting key. Conflicting callbacks\n * (`canDrag`, `canDrop`, etc.) cannot be reconciled automatically.\n * - **Disjoint keys** → merged cleanly.\n *\n * Subclasses may override to implement custom deep-merge logic, but should\n * call `super.mergeConfigsFrom(others)` to inherit the conflict-detection.\n *\n * @internal Plugin infrastructure (used by `PluginManager.attachAll`).\n * @throws Error with diagnostic code `TBW025` when configs conflict.\n */\n mergeConfigsFrom(others: readonly BaseGridPlugin<TConfig>[]): void {\n if (others.length === 0) return;\n const merged = { ...this.userConfig } as Record<string, unknown>;\n const sources: Record<string, BaseGridPlugin<TConfig>> = {};\n for (const key of Object.keys(merged)) sources[key] = this;\n for (const other of others) {\n const otherConfig = other.userConfig as Record<string, unknown>;\n for (const [key, value] of Object.entries(otherConfig)) {\n if (value === undefined) continue;\n if (!(key in merged)) {\n merged[key] = value;\n sources[key] = other;\n continue;\n }\n if (merged[key] === value) continue;\n // Conflict\n const ownerA = sources[key]?.constructor.name ?? this.constructor.name;\n const ownerB = other.constructor.name;\n const code = formatDiagnostic(\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n `Cannot merge plugin configs for \"${this.name}\": conflicting value for \"${key}\" supplied by both ${ownerA} and ${ownerB}. Pass the option on a single instance, or remove the duplicate.`,\n undefined,\n this.name,\n );\n throw new Error(code);\n }\n }\n // Mutate userConfig in place so re-attach picks it up via attach().\n // `userConfig` is declared `readonly` to prevent reassigning the binding,\n // but the object's own properties are intentionally mutable for this\n // alias-collapse pre-pass. A single cast to a record is enough — no\n // `as unknown as` double-cast needed.\n Object.assign(this.userConfig as Record<string, unknown>, merged);\n }\n\n /**\n * Replace this plugin's `userConfig` with a freshly-resolved sibling\n * instance's config. Used by the grid's per-grid feature-instance gate\n * (`#initializePlugins`): when a feature is re-resolved across a config\n * change, the **existing** instance is reused — so its accumulated state\n * survives — but it must adopt the latest `gridConfig.features.<name>`\n * value. Unlike {@link mergeConfigsFrom}, this is last-write-wins with no\n * conflict detection: the re-resolved instance is authoritative.\n *\n * `attach()` recomputes {@link config} from `userConfig`, so updating\n * `userConfig` here is enough for the reused instance to pick up the new\n * config on its next attach during the same `#initializePlugins` pass.\n *\n * @internal Plugin infrastructure (used by the grid feature gate).\n */\n refreshUserConfigFrom(other: BaseGridPlugin<TConfig>): void {\n const target = this.userConfig as Record<string, unknown>;\n // Snapshot the source BEFORE clearing `target`. A feature factory may\n // store the consumer's config object by reference (e.g. `new\n // ContextMenuPlugin(config)`), and a consumer that passes the *same* config\n // object across re-resolutions (common when `gridConfig` is a recomputed\n // Angular `computed()`) makes `this.userConfig === other.userConfig`.\n // Without this snapshot, the `delete` loop below would empty that shared\n // object first, then `Object.assign` would copy back nothing — silently\n // wiping the config and falling back to plugin defaults. (#contextMenu)\n const source = { ...(other.userConfig as Record<string, unknown>) };\n for (const key of Object.keys(target)) delete target[key];\n Object.assign(target, source);\n }\n\n /**\n * Called when the plugin is attached to a grid.\n * Override to set up event listeners, initialize state, etc.\n *\n * @example\n * ```ts\n * attach(grid: GridElement): void {\n * super.attach(grid);\n * // Set up document-level listeners with auto-cleanup\n * document.addEventListener('keydown', this.handleEscape, {\n * signal: this.disconnectSignal\n * });\n * }\n * ```\n */\n attach(grid: GridElement): void {\n // Abort any previous abort controller (in case of re-attach without detach)\n this.#abortController?.abort();\n // Create fresh abort controller for this attachment\n this.#abortController = new AbortController();\n\n this.grid = grid;\n // Merge config here (after subclass construction is complete)\n this.config = { ...this.defaultConfig, ...this.userConfig } as TConfig;\n }\n\n /**\n * Called when the plugin is detached from a grid.\n * Override to clean up event listeners, timers, etc.\n *\n * @example\n * ```ts\n * detach(): void {\n * // Clean up any state not handled by disconnectSignal\n * this.selectedRows.clear();\n * this.cache = null;\n * }\n * ```\n */\n detach(): void {\n // Abort the plugin's abort controller to clean up all event listeners\n // registered with { signal: this.disconnectSignal }\n this.#abortController?.abort();\n this.#abortController = undefined;\n // Override in subclass for additional cleanup\n }\n\n /**\n * Called when another plugin is attached to the same grid.\n * Use for inter-plugin coordination, e.g., to integrate with new plugins.\n *\n * @param name - The name of the plugin that was attached\n * @param plugin - The plugin instance (for direct access if needed)\n *\n * @example\n * ```ts\n * onPluginAttached(name: string, plugin: BaseGridPlugin): void {\n * if (name === 'selection') {\n * // Integrate with selection plugin\n * this.selectionPlugin = plugin as SelectionPlugin;\n * }\n * }\n * ```\n */\n onPluginAttached?(name: string, plugin: BaseGridPlugin): void;\n\n /**\n * Called when another plugin is detached from the same grid.\n * Use to clean up inter-plugin references.\n *\n * @param name - The name of the plugin that was detached\n *\n * @example\n * ```ts\n * onPluginDetached(name: string): void {\n * if (name === 'selection') {\n * this.selectionPlugin = undefined;\n * }\n * }\n * ```\n */\n onPluginDetached?(name: string): void;\n\n /**\n * Get another plugin instance from the same grid.\n * Use for inter-plugin communication.\n *\n * **Prefer {@link BaseGridPlugin.grid grid.getPluginByName()}** when you don't need the class import.\n *\n * @example\n * ```ts\n * // Preferred: by name\n * const selection = this.grid?.getPluginByName('selection');\n *\n * // Alternative: by class\n * const selection = this.getPlugin(SelectionPlugin);\n * if (selection) {\n * const selectedRows = selection.getSelectedRows();\n * }\n * ```\n */\n protected getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined {\n return this.grid?.getPlugin(PluginClass);\n }\n\n /**\n * Emit a custom event from the grid.\n */\n protected emit<T>(eventName: string, detail: T): void {\n this.grid?.dispatchEvent?.(new CustomEvent(eventName, { detail, bubbles: true }));\n }\n\n /**\n * Emit a cancelable custom event from the grid.\n * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise\n */\n protected emitCancelable<T>(eventName: string, detail: T): boolean {\n const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true });\n this.grid?.dispatchEvent?.(event);\n return event.defaultPrevented;\n }\n\n // =========================================================================\n // Event Bus - Plugin-to-Plugin Communication\n // =========================================================================\n\n /**\n * Subscribe to an event from another plugin.\n * The subscription is automatically cleaned up when this plugin is detached.\n *\n * @category Plugin Development\n * @param eventType - The event type to listen for (e.g., 'filter-change')\n * @param callback - The callback to invoke when the event is emitted\n *\n * @example\n * ```typescript\n * // In attach() or other initialization\n * this.on('filter-change', (detail) => {\n * console.log('Filter changed:', detail);\n * });\n * ```\n */\n protected on<T = unknown>(eventType: string, callback: (detail: T) => void): void {\n this.grid?._pluginManager?.subscribe(this, eventType, callback as (detail: unknown) => void);\n }\n\n /**\n * Unsubscribe from a plugin event.\n *\n * @category Plugin Development\n * @param eventType - The event type to stop listening for\n *\n * @example\n * ```typescript\n * this.off('filter-change');\n * ```\n */\n protected off(eventType: string): void {\n this.grid?._pluginManager?.unsubscribe(this, eventType);\n }\n\n /**\n * Emit an event to other plugins via the Event Bus.\n * This is for inter-plugin communication only; it does NOT dispatch DOM events.\n * Use `emit()` to dispatch DOM events that external consumers can listen to.\n *\n * @category Plugin Development\n * @param eventType - The event type to emit (should be declared in manifest.events)\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Emit to other plugins (not DOM)\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // For DOM events that consumers can addEventListener to:\n * this.emit('filter-change', { field: 'name', value: 'Alice' });\n * ```\n */\n protected emitPluginEvent<T>(eventType: string, detail: T): void {\n this.grid?._pluginManager?.emitPluginEvent(eventType, detail);\n }\n\n /**\n * Emit an event to **both** the plugin Event Bus (for inter-plugin communication)\n * **and** the DOM (for external consumers via `addEventListener`).\n *\n * Use this when a state change is relevant to both other plugins and external\n * consumers. For example, `sort-change` needs to invalidate Selection (plugin bus)\n * and notify the host application (DOM event).\n *\n * @category Plugin Development\n * @param eventType - The event type to broadcast\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Notify both plugins and consumers of a sort change\n * this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n * ```\n */\n protected broadcast<T>(eventType: string, detail: T): void {\n this.emitPluginEvent(eventType, detail);\n this.emit(eventType, detail);\n }\n\n /**\n * Run a yes/no plugin query and reduce the responses to a single boolean.\n *\n * `grid.query()` always returns an **array** of responses (empty when no\n * plugin handles the query), and `[]` is truthy in JavaScript — so testing\n * the raw result directly (`if (grid.query(...))`) is always `true` and\n * silently takes the \"some plugin said yes\" branch even when nothing\n * answered. Always route boolean queries through this helper.\n *\n * @category Plugin Development\n * @param type - The query type to dispatch\n * @param context - Optional query context passed to the handlers\n * @returns `true` only when at least one plugin answered truthily\n *\n * @example\n * ```typescript\n * if (this.queryBoolean('datasource:is-active')) {\n * // A ServerSide data source really is installed\n * }\n * ```\n * @since 3.4.0\n */\n protected queryBoolean(type: string, context: unknown = null): boolean {\n const responses = this.grid?.query?.(type, context);\n // Tolerate hosts/tests that hand back a bare value instead of the array.\n return Array.isArray(responses) ? responses.some(Boolean) : !!responses;\n }\n\n /**\n * Request a re-render of the grid.\n * Uses ROWS phase - does NOT trigger processColumns hooks.\n */\n protected requestRender(): void {\n this.grid?.requestRender?.();\n }\n\n /**\n * Request a columns re-render of the grid.\n * Uses COLUMNS phase - triggers processColumns hooks.\n * Use this when your plugin needs to reprocess column configuration.\n */\n protected requestColumnsRender(): void {\n (this.grid as { requestColumnsRender?: () => void })?.requestColumnsRender?.();\n }\n\n /**\n * Request a re-render and restore focus styling afterward.\n * Use this when a plugin action (like expand/collapse) triggers a render\n * but needs to maintain keyboard navigation focus.\n */\n protected requestRenderWithFocus(): void {\n this.grid?.requestRenderWithFocus?.();\n }\n\n /**\n * Request a lightweight style update without rebuilding DOM.\n * Use this instead of requestRender() when only CSS classes need updating.\n */\n protected requestAfterRender(): void {\n this.grid?.requestAfterRender?.();\n }\n\n /**\n * Re-render visible rows without rebuilding the row model or recalculating geometry.\n * Use this when row data has been updated in-place (e.g., server-side block loads)\n * and only the visible viewport needs to re-render.\n */\n protected requestVirtualRefresh(): void {\n this.grid?.requestVirtualRefresh?.();\n }\n\n /**\n * Get the current rows from the grid.\n */\n protected get rows(): any[] {\n return this.grid?.rows ?? [];\n }\n\n /**\n * Get the original unfiltered/unprocessed rows from the grid.\n * Use this when you need all source data regardless of active filters.\n */\n protected get sourceRows(): any[] {\n return this.grid?.sourceRows ?? [];\n }\n\n /**\n * Get the current columns from the grid.\n */\n protected get columns(): ColumnConfig[] {\n return this.grid?.columns ?? [];\n }\n\n /**\n * Get only visible columns from the grid (excludes hidden).\n * Use this for rendering that needs to match the grid template.\n */\n protected get visibleColumns(): ColumnConfig[] {\n return this.grid?._visibleColumns ?? [];\n }\n\n /**\n * Get the grid as an HTMLElement for direct DOM operations.\n * Use sparingly - prefer the typed GridElementRef API when possible.\n *\n * @example\n * ```ts\n * const width = this.gridElement.clientWidth;\n * this.gridElement.classList.add('my-plugin-active');\n * ```\n */\n protected get gridElement(): HTMLElement {\n return this.grid?._hostElement;\n }\n\n /**\n * Get the disconnect signal for event listener cleanup.\n * This signal is aborted when the grid disconnects from the DOM.\n * Use this when adding event listeners that should be cleaned up automatically.\n *\n * Best for:\n * - Document/window-level listeners added in attach()\n * - Listeners on the grid element itself\n * - Any listener that should persist across renders\n *\n * Not needed for:\n * - Listeners on elements created in afterRender() (removed with element)\n *\n * @example\n * element.addEventListener('click', handler, { signal: this.disconnectSignal });\n * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });\n */\n protected get disconnectSignal(): AbortSignal {\n // Return the plugin's own abort signal for proper cleanup on detach/re-attach\n // Falls back to grid's signal if plugin's controller not yet created\n return this.#abortController?.signal ?? this.grid?.disconnectSignal;\n }\n\n /**\n * Get the grid-level icons configuration.\n * Returns merged icons (user config + defaults).\n */\n protected get gridIcons(): Required<GridIcons> {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n /**\n * Resolve a user-visible string from {@link GridConfig.locale}, falling back to\n * the English default supplied at the call site.\n *\n * The fallback is passed inline rather than read from a shipped default map so\n * that a plugin's strings live in the plugin's own chunk.\n *\n * @param key - Namespaced message id, e.g. `'filter.apply'`.\n * @param fallback - English text used when the key is not overridden.\n * @since 3.5.0\n *\n * @example\n * ```ts\n * applyBtn.textContent = this.t('filter.apply', 'Apply');\n * ```\n */\n protected t(key: string, fallback: string): string {\n return this.grid?.effectiveConfig?.locale?.[key] ?? fallback;\n }\n\n /**\n * A bound `t()` suitable for handing to standalone renderer\n * helpers and public renderer callbacks.\n * @since 3.5.0\n */\n protected get translate(): Translate {\n return (key, fallback) => this.t(key, fallback);\n }\n\n // #region Animation Helpers\n\n /**\n * Check if animations are enabled at the grid level.\n * Respects gridConfig.animation.mode and the CSS variable set by the grid.\n *\n * Plugins should use this to skip animations when:\n * - Animation mode is 'off' or `false`\n * - User prefers reduced motion and mode is 'reduced-motion' (default)\n *\n * @example\n * ```ts\n * private get animationStyle(): 'slide' | 'fade' | false {\n * if (!this.isAnimationEnabled) return false;\n * return this.config.animation ?? 'slide';\n * }\n * ```\n */\n protected get isAnimationEnabled(): boolean {\n const mode = this.grid?.effectiveConfig?.animation?.mode ?? 'reduced-motion';\n\n // Explicit off = disabled\n if (mode === false || mode === 'off') return false;\n\n // Explicit on = always enabled\n if (mode === true || mode === 'on') return true;\n\n // reduced-motion: check CSS variable (set by grid based on media query)\n const host = this.gridElement;\n if (host) {\n const enabled = getComputedStyle(host).getPropertyValue('--tbw-animation-enabled').trim();\n return enabled !== '0';\n }\n\n return true; // Default to enabled\n }\n\n /**\n * Get the animation duration in milliseconds from CSS variable.\n * Falls back to 200ms if not set.\n *\n * Plugins can use this for their animation timing to stay consistent\n * with the grid-level animation.duration setting.\n *\n * @example\n * ```ts\n * element.animate(keyframes, { duration: this.animationDuration });\n * ```\n */\n protected get animationDuration(): number {\n const host = this.gridElement;\n if (host) {\n const durationStr = getComputedStyle(host).getPropertyValue('--tbw-animation-duration').trim();\n const parsed = parseInt(durationStr, 10);\n if (!isNaN(parsed)) return parsed;\n }\n return 200; // Default\n }\n\n // #endregion\n\n /**\n * Set an icon on an element using the CSS-first hybrid approach.\n *\n * Sets `data-icon` for CSS pseudo-element rendering and `data-expanded` for\n * expand/collapse toggle elements. Only injects DOM content when a JS override\n * is present (via `gridConfig.icons` or `pluginOverride`), which naturally\n * suppresses the CSS `::before` via the `:empty` selector.\n *\n * @param element - The element to set the icon on\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, iconKey: keyof GridIcons, pluginOverride?: IconValue): void {\n // Set data-icon attribute for CSS targeting (always)\n element.dataset.icon = toIconAttr(iconKey);\n\n // Set data-expanded for toggle elements (tree, group, master-detail)\n if (iconKey === 'collapse') {\n element.dataset.expanded = '';\n } else if (iconKey === 'expand') {\n delete element.dataset.expanded;\n }\n\n // Resolve JS-only icon (returns undefined when CSS default should be used)\n const jsIcon = this.#resolveJsIcon(iconKey, pluginOverride);\n\n if (jsIcon !== undefined) {\n // JS override: inject content (suppresses CSS ::before via :empty)\n if (typeof jsIcon === 'string') {\n setSanitizedHTML(element, jsIcon);\n } else if (jsIcon instanceof HTMLElement) {\n element.replaceChildren(jsIcon.cloneNode(true));\n }\n } else {\n // CSS path: ensure element is empty so ::before renders\n element.innerHTML = '';\n }\n }\n\n /**\n * Resolve JS-only icon override. Returns undefined if CSS default should render.\n * Only returns a value when the user explicitly set gridConfig.icons[key] or\n * the plugin passed an override.\n */\n #resolveJsIcon(iconKey: keyof GridIcons, pluginOverride?: IconValue): IconValue | undefined {\n if (pluginOverride !== undefined) return pluginOverride;\n return this.grid?.gridConfig?.icons?.[iconKey];\n }\n\n /**\n * Create or replace a sort indicator on a header cell.\n *\n * Handles the full lifecycle: removes any existing indicator, creates a new\n * `<span part=\"sort-indicator\" class=\"sort-indicator\">` with the appropriate\n * icon, sets `aria-sort` / `data-sort` attributes, and inserts it before the\n * filter button or resize handle.\n *\n * Plugins decide **which** cells and **when** — this method handles **how**.\n *\n * @param cell - The header cell element\n * @param direction - `'asc'` | `'desc'` for sorted, `null` for unsorted idle state\n * @returns The created indicator element (useful for appending badges, etc.)\n */\n protected updateSortIndicator(cell: Element, direction: 'asc' | 'desc' | null): HTMLElement {\n // Remove any existing indicator (core uses part, plugins use class)\n cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator')?.remove();\n\n const indicator = document.createElement('span');\n indicator.setAttribute('part', 'sort-indicator');\n indicator.className = 'sort-indicator';\n\n if (direction) {\n cell.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');\n cell.setAttribute('data-sort', direction);\n this.setIcon(indicator, direction === 'asc' ? 'sortAsc' : 'sortDesc');\n } else {\n cell.setAttribute('aria-sort', 'none');\n cell.removeAttribute('data-sort');\n this.setIcon(indicator, 'sortNone');\n }\n\n // Insert before filter button or resize handle to maintain DOM order\n const insertBefore = cell.querySelector('.tbw-filter-btn') ?? cell.querySelector('.resize-handle');\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n return indicator;\n }\n\n /**\n * Log a warning with an optional diagnostic code.\n *\n * When a diagnostic code is provided, the message is formatted with the code\n * and a link to the troubleshooting docs.\n *\n * @example\n * ```ts\n * this.warn('Something went wrong'); // plain\n * this.warn(MISSING_BREAKPOINT, 'Set a breakpoint'); // with code + docs link\n * ```\n */\n protected warn(message: string): void;\n protected warn(code: DiagnosticCode, message: string): void;\n protected warn(codeOrMessage: DiagnosticCode | string, message?: string): void {\n if (message !== undefined) {\n // Called with (code, message)\n console.warn(formatDiagnostic(codeOrMessage as DiagnosticCode, message, this.gridElement.id, this.name));\n } else {\n // Called with (message) — plain warning, no diagnostic code\n console.warn(`${gridPrefix(this.gridElement.id, this.name)} ${codeOrMessage}`);\n }\n }\n\n /**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\n protected throwDiagnostic(code: DiagnosticCode, message: string): never {\n throw new Error(formatDiagnostic(code, message, this.gridElement.id, this.name));\n }\n\n // #region Lifecycle Hooks\n\n /**\n * Transform rows before rendering.\n * Called during each render cycle before rows are rendered to the DOM.\n * Use this to filter, sort, or add computed properties to rows.\n *\n * @param rows - The current rows array (readonly to encourage returning a new array)\n * @returns The modified rows array to render\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Filter out hidden rows\n * return rows.filter(row => !row._hidden);\n * }\n * ```\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Add computed properties\n * return rows.map(row => ({\n * ...row,\n * _fullName: `${row.firstName} ${row.lastName}`\n * }));\n * }\n * ```\n */\n processRows?(rows: readonly any[]): any[];\n\n /**\n * Contribute to the grid's effective configuration during each config merge.\n * Called by {@link ConfigManager.merge} after the base config is assembled,\n * before it becomes `effectiveConfig`. Mutate `config` in place to inject\n * plugin-derived settings (e.g. the shell plugin folds its light-DOM /\n * API-registered state into `config.shell`).\n *\n * This is the config-pipeline counterpart to {@link processColumns} /\n * {@link processRows}: it keeps the core config merge free of any plugin\n * knowledge — each plugin owns the shape it contributes.\n *\n * @param config - The mutable base config being assembled into effectiveConfig.\n *\n * @example\n * ```ts\n * processConfig(config: GridConfig): void {\n * config.shell = { ...config.shell, header: { title: this.#title } };\n * }\n * ```\n */\n processConfig?(config: GridConfig): void;\n\n /**\n * Transform columns before rendering.\n * Called during each render cycle before column headers and cells are rendered.\n * Use this to add, remove, or modify column definitions.\n *\n * @param columns - The current columns array (readonly to encourage returning a new array)\n * @returns The modified columns array to render\n *\n * @example\n * ```ts\n * processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n * // Add a selection checkbox column\n * return [\n * { field: '_select', header: '', width: 40 },\n * ...columns\n * ];\n * }\n * ```\n */\n processColumns?(columns: readonly ColumnConfig[]): ColumnConfig[];\n\n /**\n * Called before each render cycle begins.\n * Use this to prepare state or cache values needed during rendering.\n *\n * **Note:** This hook is currently a placeholder for future implementation.\n * It is defined in the interface but not yet invoked by the grid's render pipeline.\n * If you need this functionality, please open an issue or contribute an implementation.\n *\n * @example\n * ```ts\n * beforeRender(): void {\n * this.visibleRowCount = this.calculateVisibleRows();\n * }\n * ```\n */\n beforeRender?(): void;\n\n /**\n * Called after each render cycle completes.\n * Use this for DOM manipulation, adding event listeners to rendered elements,\n * or applying visual effects like selection highlights.\n *\n * @example\n * ```ts\n * afterRender(): void {\n * // Apply selection styling to rendered rows\n * const rows = this.gridElement?.querySelectorAll('.data-grid-row');\n * rows?.forEach((row, i) => {\n * row.classList.toggle('selected', this.selectedRows.has(i));\n * });\n * }\n * ```\n */\n afterRender?(): void;\n\n /**\n * Called synchronously immediately after the grid (re)builds its root DOM\n * structure, before the browser has a chance to paint.\n *\n * Unlike {@link afterRender} (which runs in the scheduler's STYLE phase and\n * may be deferred), this hook fires inside the same task as the structural\n * DOM build. It is the correct place for plugins that need to wrap, relocate,\n * or augment the freshly built grid DOM without a flash of unstyled/unwrapped\n * content — e.g. a shell that moves the grid content into a chrome wrapper.\n *\n * The grid emits this hook every time it performs a full structural rebuild\n * (initial connect and structural changes), but NOT on the scroll/data hot\n * path. Implementations MUST be idempotent: a full rebuild discards any DOM a\n * previous invocation produced, so re-create it, and no-op when the desired\n * structure is already present.\n *\n * @example\n * ```ts\n * afterStructuralRender(): void {\n * const root = this.grid?._renderRoot;\n * if (!root || root.querySelector('.my-wrapper > .tbw-grid-content')) return;\n * // build wrapper, move .tbw-grid-content inside it\n * }\n * ```\n */\n afterStructuralRender?(): void;\n\n /**\n * Called synchronously by the grid immediately before it tears down and\n * rebuilds its root DOM structure (a structural re-render). Plugins that\n * relocated existing light-DOM nodes into their own chrome (e.g. a shell that\n * moves toolbar buttons into the header bar) use this to move those nodes back\n * to their original container so they survive the rebuild. Generic\n * counterpart to {@link afterStructuralRender}. MUST be idempotent.\n */\n beforeStructuralRender?(): void;\n\n /**\n * Called by the grid after a config merge to ask whether the change requires a\n * full structural re-render (rather than a cheaper in-place update). A plugin\n * returns `true` when its rendered DOM signature no longer matches the new\n * effective config — e.g. a shell header appearing/disappearing or a tool\n * panel's count/position/mode changing. The grid performs a structural\n * re-render if ANY plugin returns `true`. MUST be side-effect free.\n */\n needsStructuralRerender?(): boolean;\n\n /**\n * Called by the grid to let a plugin (re-)parse its own light-DOM\n * contributions into plugin state ahead of a config merge. Generic seam that\n * keeps core free of plugin-specific light-DOM tag knowledge (the plugin reads\n * `this.grid._hostElement`). MUST be idempotent.\n */\n parseLightDom?(): void;\n\n /**\n * Called by the grid (from `refreshColumns` and the light-DOM observer) to\n * let a plugin re-parse its light-DOM contributions AND refresh any\n * already-rendered chrome in place when something changed — e.g. a shell\n * title or tool buttons that a framework projected asynchronously after the\n * initial render. Distinct from {@link parseLightDom}, which only updates\n * plugin state. Generic seam — core holds no plugin-specific knowledge. MUST\n * be idempotent and a no-op when the plugin's chrome is not yet rendered.\n */\n syncLightDom?(): void;\n\n /**\n * Called after each cell is rendered.\n * This hook is more efficient than `afterRender()` for cell-level modifications\n * because you receive the cell context directly - no DOM queries needed.\n *\n * Use cases:\n * - Adding selection/highlight classes to specific cells\n * - Injecting badges or decorations\n * - Applying conditional styling based on cell value\n *\n * Performance note: Called for every visible cell during render. Keep implementation fast.\n * This hook is also called during scroll when cells are recycled.\n *\n * @param context - The cell render context with row, column, value, and elements\n *\n * @example\n * ```ts\n * afterCellRender(context: AfterCellRenderContext): void {\n * // Add selection class without DOM queries\n * if (this.isSelected(context.rowIndex, context.colIndex)) {\n * context.cellElement.classList.add('selected');\n * }\n *\n * // Add validation error styling\n * if (this.hasError(context.row, context.column.field)) {\n * context.cellElement.classList.add('has-error');\n * }\n * }\n * ```\n */\n afterCellRender?(context: AfterCellRenderContext): void;\n\n /**\n * Called after a row is fully rendered (all cells complete).\n * Use this for row-level decorations, styling, or ARIA attributes.\n *\n * Common use cases:\n * - Adding selection classes to entire rows (row-focus, selected)\n * - Setting row-level ARIA attributes\n * - Applying row validation highlighting\n * - Tree indentation styling\n *\n * Performance note: Called for every visible row during render. Keep implementation fast.\n * This hook is also called during scroll when rows are recycled.\n *\n * @param context - The row render context with row data and element\n *\n * @example\n * ```ts\n * afterRowRender(context: AfterRowRenderContext): void {\n * // Add row selection class without DOM queries\n * if (this.isRowSelected(context.rowIndex)) {\n * context.rowElement.classList.add('selected', 'row-focus');\n * }\n *\n * // Add validation error styling\n * if (this.rowHasErrors(context.row)) {\n * context.rowElement.classList.add('has-errors');\n * }\n * }\n * ```\n */\n afterRowRender?(context: AfterRowRenderContext): void;\n\n /**\n * Called after scroll-triggered row rendering completes.\n * This is a lightweight hook for applying visual state to recycled DOM elements.\n * Use this instead of afterRender when you need to reapply styling during scroll.\n *\n * Performance note: This is called frequently during scroll. Keep implementation fast.\n *\n * @example\n * ```ts\n * onScrollRender(): void {\n * // Reapply selection state to visible cells\n * this.applySelectionToVisibleCells();\n * }\n * ```\n */\n onScrollRender?(): void;\n\n /**\n * Get the height of a specific row.\n * Used for synthetic rows (group headers, detail panels, etc.) that have fixed heights.\n * Return undefined if this plugin does not manage the height for this row.\n *\n * This hook is called during position cache rebuilds for variable row height virtualization.\n * Plugins that create synthetic rows should implement this to provide accurate heights.\n *\n * @param row - The row data\n * @param index - The row index in the processed rows array\n * @returns The row height in pixels, or undefined if not managed by this plugin\n */\n getRowHeight?(row: unknown, index: number): number | undefined;\n\n /**\n * Adjust the virtualization start index to render additional rows before the visible range.\n * Use this when expanded content (like detail rows) needs its parent row to remain rendered\n * even when the parent row itself has scrolled above the viewport.\n *\n * @param start - The calculated start row index\n * @param scrollTop - The current scroll position\n * @param rowHeight - The height of a single row\n * @returns The adjusted start index (lower than or equal to original start)\n *\n * @example\n * ```ts\n * adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n * // If row 5 is expanded and scrolled partially, keep it rendered\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * const expandedRowTop = expandedRowIndex * rowHeight;\n * const expandedRowBottom = expandedRowTop + rowHeight + this.detailHeight;\n * if (expandedRowBottom > scrollTop && expandedRowIndex < start) {\n * return expandedRowIndex;\n * }\n * }\n * return start;\n * }\n * ```\n */\n adjustVirtualStart?(start: number, scrollTop: number, rowHeight: number): number;\n\n /**\n * Render a custom row, bypassing the default row rendering.\n * Use this for special row types like group headers, detail rows, or footers.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if the plugin handled rendering (prevents default), `false`/`void` for default rendering\n *\n * @example\n * ```ts\n * renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void {\n * if (row._isGroupHeader) {\n * rowEl.innerHTML = `<div class=\"group-header\">${row._groupLabel}</div>`;\n * return true; // Handled - skip default rendering\n * }\n * // Return void to let default rendering proceed\n * }\n * ```\n */\n renderRow?(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void;\n\n // #endregion\n\n // #region Inter-Plugin Communication\n\n /**\n * Handle queries from other plugins or the grid.\n *\n * Queries are declared in `manifest.queries` and dispatched via `grid.query()`.\n * This enables type-safe, discoverable inter-plugin communication.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * // In manifest\n * static override readonly manifest: PluginManifest = {\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * };\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * const column = query.context as ColumnConfig;\n * return !column.sticky; // Can't move sticky columns\n * }\n * }\n * ```\n */\n handleQuery?(query: PluginQuery): unknown;\n\n // #endregion\n\n // #region Interaction Hooks\n\n /**\n * Handle keyboard events on the grid.\n * Called when a key is pressed while the grid or a cell has focus.\n *\n * @param event - The native KeyboardEvent\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onKeyDown(event: KeyboardEvent): boolean | void {\n * // Handle Ctrl+A for select all\n * if (event.ctrlKey && event.key === 'a') {\n * this.selectAllRows();\n * return true; // Prevent default browser select-all\n * }\n * }\n * ```\n */\n onKeyDown?(event: KeyboardEvent): boolean | void;\n\n /**\n * Handle cell click events.\n * Called when a data cell is clicked (not headers).\n *\n * @param event - Cell click event with row/column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onCellClick(event: CellClickEvent): boolean | void {\n * if (event.field === '_select') {\n * this.toggleRowSelection(event.rowIndex);\n * return true; // Handled\n * }\n * }\n * ```\n */\n onCellClick?(event: CellClickEvent): boolean | void;\n\n /**\n * Handle row click events.\n * Called when any part of a data row is clicked.\n * Note: This is called in addition to onCellClick, not instead of.\n *\n * @param event - Row click event with row context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onRowClick(event: RowClickEvent): boolean | void {\n * if (this.config.mode === 'row') {\n * this.selectRow(event.rowIndex, event.originalEvent);\n * return true;\n * }\n * }\n * ```\n */\n onRowClick?(event: RowClickEvent): boolean | void;\n\n /**\n * Handle header click events.\n * Called when a column header is clicked. Commonly used for sorting.\n *\n * @param event - Header click event with column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onHeaderClick(event: HeaderClickEvent): boolean | void {\n * if (event.column.sortable !== false) {\n * this.toggleSort(event.field);\n * return true;\n * }\n * }\n * ```\n */\n onHeaderClick?(event: HeaderClickEvent): boolean | void;\n\n /**\n * Handle scroll events on the grid viewport.\n * Called during scrolling. Note: This may be called frequently; debounce if needed.\n *\n * @param event - Scroll event with scroll position and viewport dimensions\n *\n * @example\n * ```ts\n * onScroll(event: ScrollEvent): void {\n * // Update sticky column positions\n * this.updateStickyPositions(event.scrollLeft);\n * }\n * ```\n */\n onScroll?(event: ScrollEvent): void;\n\n /**\n * Handle cell mousedown events.\n * Used for initiating drag operations like range selection or column resize.\n *\n * @param event - Mouse event with cell context\n * @returns `true` to indicate drag started (prevents text selection), `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseDown(event: CellMouseEvent): boolean | void {\n * if (event.rowIndex !== undefined && this.config.mode === 'range') {\n * this.startDragSelection(event.rowIndex, event.colIndex);\n * return true; // Prevent text selection\n * }\n * }\n * ```\n */\n onCellMouseDown?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mousemove events during drag operations.\n * Only called when a drag is in progress (after mousedown returned true).\n *\n * @param event - Mouse event with current cell context\n * @returns `true` to continue handling the drag, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseMove(event: CellMouseEvent): boolean | void {\n * if (this.isDragging && event.rowIndex !== undefined) {\n * this.extendSelection(event.rowIndex, event.colIndex);\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseMove?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mouseup events to end drag operations.\n *\n * @param event - Mouse event with final cell context\n * @returns `true` if drag was finalized, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseUp(event: CellMouseEvent): boolean | void {\n * if (this.isDragging) {\n * this.finalizeDragSelection();\n * this.isDragging = false;\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseUp?(event: CellMouseEvent): boolean | void;\n\n // Note: Context menu items are provided via handleQuery('getContextMenuItems').\n // This keeps the core decoupled from the context-menu plugin specifics.\n\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Contribute plugin-specific state for a column.\n * Called by the grid when collecting column state for serialization.\n * Plugins can add their own properties to the column state.\n *\n * @param field - The field name of the column\n * @returns Partial column state with plugin-specific properties, or undefined if no state to contribute\n *\n * @example\n * ```ts\n * getColumnState(field: string): Partial<ColumnState> | undefined {\n * const filterModel = this.filterModels.get(field);\n * if (filterModel) {\n * // Uses module augmentation to add filter property to ColumnState\n * return { filter: filterModel } as Partial<ColumnState>;\n * }\n * return undefined;\n * }\n * ```\n */\n getColumnState?(field: string): Partial<ColumnState> | undefined;\n\n /**\n * Apply plugin-specific state to a column.\n * Called by the grid when restoring column state from serialized data.\n * Plugins should restore their internal state based on the provided state.\n *\n * @param field - The field name of the column\n * @param state - The column state to apply (may contain plugin-specific properties)\n *\n * @example\n * ```ts\n * applyColumnState(field: string, state: ColumnState): void {\n * // Check for filter property added via module augmentation\n * const filter = (state as any).filter;\n * if (filter) {\n * this.filterModels.set(field, filter);\n * this.applyFilter();\n * }\n * }\n * ```\n */\n applyColumnState?(field: string, state: ColumnState): void;\n\n // #endregion\n\n // #region Scroll Boundary Hooks\n\n /**\n * Report horizontal scroll boundary offsets for this plugin.\n * Plugins that obscure part of the scroll area (e.g., pinned/sticky columns)\n * should return how much space they occupy on each side.\n * The keyboard navigation uses this to ensure focused cells are fully visible.\n *\n * @param rowEl - The row element (optional, for calculating widths from rendered cells)\n * @param focusedCell - The currently focused cell element (optional, to determine if scrolling should be skipped)\n * @returns Object with left/right pixel offsets and optional skipScroll flag, or undefined if plugin has no offsets\n *\n * @example\n * ```ts\n * getHorizontalScrollOffsets(rowEl?: HTMLElement, focusedCell?: HTMLElement): { left: number; right: number; skipScroll?: boolean } | undefined {\n * // Calculate total width of left-pinned columns\n * const leftCells = rowEl?.querySelectorAll('.sticky-left') ?? [];\n * let left = 0;\n * leftCells.forEach(el => { left += (el as HTMLElement).offsetWidth; });\n * // Skip scroll if focused cell is pinned (always visible)\n * const skipScroll = focusedCell?.classList.contains('sticky-left');\n * return { left, right: 0, skipScroll };\n * }\n * ```\n */\n getHorizontalScrollOffsets?(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined;\n\n /**\n * Report vertical scroll boundary offsets for this plugin.\n * Plugins that overlay the top or bottom of the rows viewport (e.g. sticky\n * row clones) should return how much space they obscure on each edge, so\n * keyboard navigation can scroll a focused row clear of the overlay rather\n * than leaving it hidden underneath — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n *\n * Only report space that is painted *over* the scrollable rows. Content that\n * takes part in normal flow (pinned-row bars, the column header) already\n * shrinks the viewport and must NOT be reported here, or rows would scroll\n * twice as far as needed.\n *\n * @param focusedRowIndex - Index of the row being scrolled into view, so a plugin can report `skipScroll` when it already renders that row in a permanently visible overlay\n * @returns Object with top/bottom pixel offsets and optional skipScroll flag, or undefined if plugin obscures nothing\n *\n * @example\n * ```ts\n * getVerticalScrollOffsets(focusedRowIndex?: number): { top: number; bottom: number; skipScroll?: boolean } | undefined {\n * if (!this.overlayEl) return undefined;\n * // The row is already pinned in the overlay — it is visible where it is.\n * if (focusedRowIndex != null && this.stuckIndices.includes(focusedRowIndex)) {\n * return { top: 0, bottom: 0, skipScroll: true };\n * }\n * return { top: this.overlayEl.offsetHeight, bottom: 0 };\n * }\n * ```\n *\n * @since 3.6.0\n */\n getVerticalScrollOffsets?(\n focusedRowIndex?: number,\n ): { top: number; bottom: number; skipScroll?: boolean } | undefined;\n\n // #endregion\n\n // #region Shell Integration Hooks\n\n /**\n * Register a tool panel for this plugin.\n * Return undefined if plugin has no tool panel.\n * The shell will create a toolbar toggle button and render the panel content\n * when the user opens the panel.\n *\n * @returns Tool panel definition, or undefined if plugin has no panel\n *\n * @example\n * ```ts\n * getToolPanel(): ToolPanelDefinition | undefined {\n * return {\n * id: 'columns',\n * title: 'Columns',\n * icon: '☰',\n * tooltip: 'Show/hide columns',\n * order: 10,\n * render: (container) => {\n * this.renderColumnList(container);\n * return () => this.cleanup();\n * },\n * };\n * }\n * ```\n */\n getToolPanel?(): ToolPanelDefinition | undefined;\n\n /**\n * Register content for the shell header center section.\n * Return undefined if plugin has no header content.\n * Examples: search input, selection summary, status indicators.\n *\n * @returns Header content definition, or undefined if plugin has no header content\n *\n * @example\n * ```ts\n * getHeaderContent(): HeaderContentDefinition | undefined {\n * return {\n * id: 'quick-filter',\n * order: 10,\n * render: (container) => {\n * const input = document.createElement('input');\n * input.type = 'text';\n * input.placeholder = 'Search...';\n * input.addEventListener('input', this.handleInput);\n * container.appendChild(input);\n * return () => input.removeEventListener('input', this.handleInput);\n * },\n * };\n * }\n * ```\n */\n getHeaderContent?(): HeaderContentDefinition | undefined;\n\n // #endregion\n}\n","/**\n * Shared Expander Column Utilities\n *\n * Provides a fixed expander column for plugins that need expand/collapse icons\n * (MasterDetail, Tree, RowGrouping). The column is:\n * - Always first in the grid\n * - Cannot be reordered (lockPosition: true)\n * - Has no header (empty string)\n * - Has no right border (borderless styling)\n * - Narrow width (just fits the icon)\n */\n\nimport type { ColumnConfig } from '../types';\n\n/** Special field name for the expander column */\nexport const EXPANDER_COLUMN_FIELD = '__tbw_expander';\n\n/** Default width for the expander column (pixels) */\nexport const EXPANDER_COLUMN_WIDTH = 32;\n\n/**\n * Marker interface for expander column renderers.\n * Used to detect if expander column is already present.\n */\nexport interface ExpanderColumnRenderer {\n (ctx: any): HTMLElement;\n __expanderColumn?: true;\n /** Plugin name that created this expander */\n __expanderPlugin?: string;\n}\n\n/**\n * Check if a column is an expander column.\n */\nexport function isExpanderColumn(column: ColumnConfig): boolean {\n return column.field === EXPANDER_COLUMN_FIELD;\n}\n\n/**\n * Check if a column is a utility column (excluded from selection, clipboard, etc.).\n * Utility columns are non-data columns like expander columns.\n */\nexport function isUtilityColumn(column: ColumnConfig): boolean {\n return column.utility === true;\n}\n\n/**\n * Find an existing expander column in the column array.\n */\nexport function findExpanderColumn(columns: readonly ColumnConfig[]): ColumnConfig | undefined {\n return columns.find(isExpanderColumn);\n}\n\n/**\n * Create the base expander column config.\n * Plugins should add their own renderer to customize the expand icon behavior.\n *\n * @param pluginName - Name of the plugin creating the expander (for debugging)\n * @returns Base column config for the expander column\n */\nexport function createExpanderColumnConfig(pluginName: string): ColumnConfig {\n return {\n field: EXPANDER_COLUMN_FIELD as any,\n header: '', // No header text - visually merges with next column\n width: EXPANDER_COLUMN_WIDTH,\n resizable: false,\n sortable: false,\n filterable: false, // No filter button for expander column\n lockPosition: true,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n meta: {\n expanderColumn: true,\n expanderPlugin: pluginName,\n },\n };\n}\n","/**\n * Generic click-only action menu — the shared building block for WCAG 2.2\n * SC 2.5.7 \"Dragging Movements\" alternatives in plugins.\n *\n * Every drag affordance in the grid needs a path that works with plain clicks\n * or taps. A keyboard equivalent is **not** enough: SC 2.5.7 is specifically\n * about pointer input, and the W3C Understanding text calls out \"a pop-up menu\n * after tap\" as a conforming alternative. This module is that pop-up.\n *\n * Lives under `plugins/shared/` on purpose: each plugin bundle is built\n * self-contained, so this code is duplicated into the plugins that use it and\n * never lands in the core `index.js` budget.\n *\n * @since 3.6.0\n * @internal\n */\n\n/** Gap between the anchor element and the menu, in px. */\nconst ANCHOR_GAP_PX = 6;\n\n/** Runtime check — happy-dom and older browsers may lack the Popover API. */\nfunction supportsPopover(): boolean {\n return typeof HTMLElement.prototype?.showPopover === 'function';\n}\n\nlet hoverlessQuery: MediaQueryList | null | undefined;\n\n/** Whether the primary pointer cannot hover (touch, most switch devices). */\nfunction isHoverless(): boolean {\n if (hoverlessQuery === undefined) hoverlessQuery = globalThis.matchMedia?.('(hover: none)') ?? null;\n return hoverlessQuery?.matches ?? false;\n}\n\n/**\n * Whether a plugin should also expose its drag alternative as a dedicated\n * inline control, on top of the menu that is always available.\n *\n * Hover-less pointers get one unconditionally — a control revealed on hover is\n * unreachable when nothing can hover, and long-press is the wrong fallback for\n * the tremor and low-dexterity users SC 2.5.7 exists for.\n *\n * Driven by `a11y.dragAlternatives` on the grid config.\n */\nexport function prefersInlineDragAlternative(grid: {\n effectiveConfig?: { a11y?: { dragAlternatives?: 'menu' | 'inline' } };\n}): boolean {\n return grid.effectiveConfig?.a11y?.dragAlternatives === 'inline' || isHoverless();\n}\n\n/** One clickable entry in the menu. */\nexport interface DragAlternativeAction {\n /** Visible text, also the accessible name. */\n label: string;\n /** Render the entry but refuse the click (e.g. \"Move up\" on the first row). */\n disabled?: boolean;\n /** Invoked on click; the menu closes first so focus handling is predictable. */\n run(): void;\n}\n\nexport interface DragAlternativeMenu {\n /**\n * Show `actions` anchored to `anchor`, with `label` as the group's accessible\n * name. Mounted inside the anchor's grid host so it inherits theme tokens.\n */\n open(anchor: HTMLElement, label: string, actions: readonly DragAlternativeAction[]): void;\n /** Hide the menu. Safe to call when already closed. */\n close(): void;\n /** Remove the element and every document-level listener. */\n dispose(): void;\n /** The menu element, or `null` before the first {@link DragAlternativeMenu.open}. */\n readonly element: HTMLElement | null;\n}\n\n/**\n * Create a menu instance.\n *\n * @param id - DOM id for the menu element, unique per owning plugin.\n * @param className - Root class name, so each plugin can style its own menu.\n */\nexport function createDragAlternativeMenu(id: string, className: string): DragAlternativeMenu {\n let root: HTMLDivElement | null = null;\n let opened = false;\n let disposed = false;\n const abort = new AbortController();\n\n function close(): void {\n if (!opened || !root) return;\n opened = false;\n if (supportsPopover()) root.hidePopover();\n root.hidden = true;\n }\n\n function build(): HTMLDivElement {\n const el = document.createElement('div');\n el.id = id;\n el.className = className;\n el.setAttribute('popover', 'manual');\n el.setAttribute('role', 'group');\n // Without the Popover API there is no UA `display: none`, so hide it here.\n el.hidden = true;\n // Tabbing past either end dismisses instead of stranding the user — the\n // menu has no visible trigger to tab back to. A null `relatedTarget` (click\n // focus in Firefox/Safari, focus leaving the document) is ignored.\n el.addEventListener(\n 'focusout',\n (e) => {\n const next = (e as FocusEvent).relatedTarget;\n if (next instanceof Node && !el.contains(next)) close();\n },\n { signal: abort.signal },\n );\n return el;\n }\n\n /**\n * Position against the anchor in viewport coordinates. The menu lives in the\n * top layer, so its containing block is the viewport regardless of where the\n * anchor sits in the DOM.\n */\n function place(anchor: HTMLElement): void {\n if (!root) return;\n const a = anchor.getBoundingClientRect();\n const box = root.getBoundingClientRect();\n const left = Math.max(4, Math.min(a.left, window.innerWidth - box.width - 4));\n const below = a.bottom + ANCHOR_GAP_PX;\n const top = below + box.height <= window.innerHeight ? below : Math.max(4, a.top - ANCHOR_GAP_PX - box.height);\n root.style.left = `${Math.round(left)}px`;\n root.style.top = `${Math.round(top)}px`;\n }\n\n // Any pointer press outside the menu dismisses it. Capture phase so the press\n // that lands on a grid cell still reaches its own handler afterwards.\n document.addEventListener(\n 'pointerdown',\n (e) => {\n if (opened && root && !e.composedPath().includes(root)) close();\n },\n { capture: true, signal: abort.signal },\n );\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e as KeyboardEvent).key === 'Escape') close();\n },\n { capture: true, signal: abort.signal },\n );\n\n return {\n get element() {\n return root;\n },\n open(anchor, label, actions) {\n if (disposed || actions.length === 0) return;\n if (!root) root = build();\n // Mount inside the grid host (light DOM) so theme custom properties and\n // `color-scheme` cascade in; the top layer keeps it out of overflow.\n const host = anchor.closest<HTMLElement>('tbw-grid, [data-tbw-grid]') ?? document.body;\n if (root.parentNode !== host) host.appendChild(root);\n root.setAttribute('aria-label', label);\n root.replaceChildren();\n for (const action of actions) {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${className}-item`;\n btn.textContent = action.label;\n btn.disabled = action.disabled === true;\n btn.addEventListener('click', () => {\n close();\n action.run();\n });\n root.appendChild(btn);\n }\n root.hidden = false;\n if (!opened && supportsPopover()) root.showPopover();\n opened = true;\n place(anchor);\n root.querySelector<HTMLButtonElement>('button:not([disabled])')?.focus();\n },\n close,\n dispose() {\n disposed = true;\n close();\n abort.abort();\n root?.remove();\n root = null;\n },\n };\n}\n","/**\n * Column Selection Core Logic\n *\n * Pure functions for column selection: mode normalization, range expansion\n * across visible columns, and Ctrl+Shift+Arrow extension math. All grid-DOM\n * interaction lives in `SelectionPlugin.ts` — this module is dependency-free\n * and trivially unit-testable.\n *\n * @since 2.8.0\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SelectionMode } from './types';\n\n// #region Mode normalization\n\n/**\n * Normalized representation of `SelectionConfig.mode`. Splits the user-supplied\n * mode (single string OR array) into:\n *\n * - `primary`: the in-row axis (`'cell' | 'row' | 'range'`) — drives existing\n * click/drag/keyboard behavior. When the user configured column-only, this is\n * `'column'` and the in-row hooks treat it as inert.\n * - `columnEnabled`: whether the column axis is also active. Drives Ctrl+Click\n * on header, Ctrl+Space on focused cell, and the column-selection render pass.\n * - `bothAxes`: shorthand for \"column AND a non-column axis are configured\" —\n * this is the only state where the row↔column mutual-exclusion logic kicks in.\n */\nexport interface NormalizedModeConfig {\n primary: SelectionMode;\n columnEnabled: boolean;\n bothAxes: boolean;\n}\n\n/**\n * Normalize the user-supplied `mode` and validate it.\n *\n * Allowed shapes:\n * - Single: `'cell' | 'row' | 'column' | 'range'`\n * - Array: `['column', X]` or `[X, 'column']` where X is `'cell' | 'row' | 'range'`\n * - Single-element arrays are treated as the contained string for ergonomics.\n *\n * Throws on:\n * - Empty array\n * - Arrays of 3+ items\n * - Arrays without `'column'` (the only purpose of an array is to enable the\n * column axis alongside an in-row axis — `['row', 'cell']` etc. don't compose)\n * - Duplicate entries\n * - Unknown mode strings\n */\nexport function normalizeMode(mode: SelectionMode | SelectionMode[]): NormalizedModeConfig {\n const validModes: ReadonlySet<string> = new Set(['cell', 'row', 'column', 'range']);\n\n if (typeof mode === 'string') {\n if (!validModes.has(mode)) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode: \"${mode}\". Expected one of: 'cell' | 'row' | 'column' | 'range'.`,\n );\n }\n return {\n primary: mode,\n columnEnabled: mode === 'column',\n bothAxes: false,\n };\n }\n\n if (!Array.isArray(mode)) {\n throw new Error(`[SelectionPlugin] Invalid selection mode: expected string or array, got ${typeof mode}.`);\n }\n\n if (mode.length === 0) {\n throw new Error(`[SelectionPlugin] Invalid selection mode: array must contain at least one mode.`);\n }\n\n for (const m of mode) {\n if (!validModes.has(m)) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode in array: \"${m}\". Expected one of: 'cell' | 'row' | 'column' | 'range'.`,\n );\n }\n }\n\n // Reject duplicates\n if (new Set(mode).size !== mode.length) {\n throw new Error(`[SelectionPlugin] Invalid selection mode: array contains duplicate entries (${mode.join(', ')}).`);\n }\n\n // Single-element array → treat as plain string\n if (mode.length === 1) {\n return normalizeMode(mode[0]);\n }\n\n if (mode.length > 2) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode: arrays of more than 2 modes are not supported. Got [${mode.join(', ')}].`,\n );\n }\n\n // 2-element array: must contain 'column' + one of 'cell' | 'row' | 'range'\n const hasColumn = mode.includes('column');\n if (!hasColumn) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode: [${mode.join(', ')}]. ` +\n `Two-mode arrays must include 'column' (the only valid combinations are ['row','column'], ['cell','column'], ['range','column']). ` +\n `Other in-row modes don't compose meaningfully.`,\n );\n }\n\n const other = mode.find((m) => m !== 'column') as SelectionMode;\n if (other === 'column') {\n // Both entries are 'column' — already caught by duplicate check, but defend\n throw new Error(`[SelectionPlugin] Invalid selection mode: [${mode.join(', ')}].`);\n }\n\n return {\n primary: other,\n columnEnabled: true,\n bothAxes: true,\n };\n}\n\n// #endregion\n\n// #region Field/index helpers\n\n/**\n * Build the ordered list of selectable column fields from the grid's visible\n * columns. Utility columns (checkbox, expander, drag handle) are excluded —\n * they exist to support grid behavior, not to be selected.\n */\nexport function selectableColumnFields(visibleColumns: readonly ColumnConfig[]): string[] {\n const fields: string[] = [];\n for (const col of visibleColumns) {\n if ((col as { utility?: boolean }).utility === true) continue;\n if (typeof col.field === 'string' && col.field.length > 0) {\n fields.push(col.field);\n }\n }\n return fields;\n}\n\n/** Find the index of `field` within the selectable-fields list. -1 if not present. */\nexport function indexOfField(field: string, fields: readonly string[]): number {\n return fields.indexOf(field);\n}\n\n/**\n * Compute the inclusive field range from `anchor` to `target` along the\n * visible-column order. Returns the fields between the two (inclusive),\n * regardless of which one is to the left. Empty if either is missing.\n */\nexport function fieldsBetween(anchor: string, target: string, fields: readonly string[]): string[] {\n const a = indexOfField(anchor, fields);\n const b = indexOfField(target, fields);\n if (a === -1 || b === -1) return [];\n const start = Math.min(a, b);\n const end = Math.max(a, b);\n return fields.slice(start, end + 1);\n}\n\n// #endregion\n\n// #region Keyboard extension (Ctrl+Shift+Arrow)\n\n/**\n * Compute the new \"head\" field after pressing `ArrowLeft` or `ArrowRight`\n * during column-axis keyboard extension. `head` is the most recently focused\n * column edge (the one that moves); `anchor` stays put.\n *\n * Returns the new head field, or `null` if the move would go out of bounds\n * (caller should leave selection unchanged).\n */\nexport function computeKeyboardExtension(\n head: string | null,\n fields: readonly string[],\n direction: 'left' | 'right',\n): string | null {\n if (fields.length === 0) return null;\n if (head === null) return null;\n const headIdx = indexOfField(head, fields);\n if (headIdx < 0) return null;\n const next = direction === 'left' ? Math.max(0, headIdx - 1) : Math.min(fields.length - 1, headIdx + 1);\n if (next === headIdx) return null;\n return fields[next];\n}\n\n// #endregion\n","/**\n * Cell Range Selection Core Logic\n *\n * Pure functions for cell range selection operations.\n */\n\nimport type { InternalCellRange, CellRange } from './types';\n\n/**\n * Normalize a range so startRow/startCol are always <= endRow/endCol.\n * This handles cases where user drags from bottom-right to top-left.\n *\n * @param range - The range to normalize\n * @returns Normalized range with start <= end for both dimensions\n */\nexport function normalizeRange(range: InternalCellRange): InternalCellRange {\n return {\n startRow: Math.min(range.startRow, range.endRow),\n startCol: Math.min(range.startCol, range.endCol),\n endRow: Math.max(range.startRow, range.endRow),\n endCol: Math.max(range.startCol, range.endCol),\n };\n}\n\n/**\n * Convert an internal range to the public event format.\n *\n * @param range - The internal range to convert\n * @returns Public CellRange format with from/to coordinates\n */\nexport function toPublicRange(range: InternalCellRange): CellRange {\n const normalized = normalizeRange(range);\n return {\n from: { row: normalized.startRow, col: normalized.startCol },\n to: { row: normalized.endRow, col: normalized.endCol },\n };\n}\n\n/**\n * Convert multiple internal ranges to public format.\n *\n * @param ranges - Array of internal ranges\n * @returns Array of public CellRange format\n */\nexport function toPublicRanges(ranges: InternalCellRange[]): CellRange[] {\n return ranges.map(toPublicRange);\n}\n\n/**\n * Check if a cell is within a specific range.\n *\n * @param row - The row index to check\n * @param col - The column index to check\n * @param range - The range to check against\n * @returns True if the cell is within the range\n */\nexport function isCellInRange(row: number, col: number, range: InternalCellRange): boolean {\n const normalized = normalizeRange(range);\n return (\n row >= normalized.startRow && row <= normalized.endRow && col >= normalized.startCol && col <= normalized.endCol\n );\n}\n\n/**\n * Check if a cell is within any of the provided ranges.\n *\n * @param row - The row index to check\n * @param col - The column index to check\n * @param ranges - Array of ranges to check against\n * @returns True if the cell is within any range\n */\nexport function isCellInAnyRange(row: number, col: number, ranges: InternalCellRange[]): boolean {\n return ranges.some((range) => isCellInRange(row, col, range));\n}\n\n/**\n * Get all cells within a range as an array of {row, col} objects.\n *\n * @param range - The range to enumerate\n * @returns Array of all cell coordinates in the range\n */\nexport function getCellsInRange(range: InternalCellRange): Array<{ row: number; col: number }> {\n const cells: Array<{ row: number; col: number }> = [];\n const normalized = normalizeRange(range);\n\n for (let row = normalized.startRow; row <= normalized.endRow; row++) {\n for (let col = normalized.startCol; col <= normalized.endCol; col++) {\n cells.push({ row, col });\n }\n }\n\n return cells;\n}\n\n/**\n * Get all unique cells across multiple ranges.\n * Deduplicates cells that appear in overlapping ranges.\n *\n * @param ranges - Array of ranges to enumerate\n * @returns Array of unique cell coordinates\n */\nexport function getAllCellsInRanges(ranges: InternalCellRange[]): Array<{ row: number; col: number }> {\n const cellMap = new Map<string, { row: number; col: number }>();\n\n for (const range of ranges) {\n for (const cell of getCellsInRange(range)) {\n cellMap.set(`${cell.row},${cell.col}`, cell);\n }\n }\n\n return [...cellMap.values()];\n}\n\n/**\n * Merge overlapping or adjacent ranges into fewer ranges.\n * Simple implementation - returns ranges as-is for now.\n * More complex merging logic can be added later for optimization.\n *\n * @param ranges - Array of ranges to merge\n * @returns Merged array of ranges\n */\nexport function mergeRanges(ranges: InternalCellRange[]): InternalCellRange[] {\n // Simple implementation - more complex merging can be added later\n return ranges;\n}\n\n/**\n * Create a range from an anchor cell to a current cell position.\n * The range is not normalized - it preserves the direction of selection.\n *\n * @param anchor - The anchor cell (where selection started)\n * @param current - The current cell (where selection ends)\n * @returns An InternalCellRange from anchor to current\n */\nexport function createRangeFromAnchor(\n anchor: { row: number; col: number },\n current: { row: number; col: number }\n): InternalCellRange {\n return {\n startRow: anchor.row,\n startCol: anchor.col,\n endRow: current.row,\n endCol: current.col,\n };\n}\n\n/**\n * Calculate the number of cells in a range.\n *\n * @param range - The range to measure\n * @returns Total number of cells in the range\n */\nexport function getRangeCellCount(range: InternalCellRange): number {\n const normalized = normalizeRange(range);\n const rowCount = normalized.endRow - normalized.startRow + 1;\n const colCount = normalized.endCol - normalized.startCol + 1;\n return rowCount * colCount;\n}\n\n/**\n * Check if two ranges are equal (same boundaries).\n *\n * @param a - First range\n * @param b - Second range\n * @returns True if ranges have same boundaries after normalization\n */\nexport function rangesEqual(a: InternalCellRange, b: InternalCellRange): boolean {\n const normA = normalizeRange(a);\n const normB = normalizeRange(b);\n return (\n normA.startRow === normB.startRow &&\n normA.startCol === normB.startCol &&\n normA.endRow === normB.endRow &&\n normA.endCol === normB.endCol\n );\n}\n\n/**\n * Check if a range is a single cell (1x1).\n *\n * @param range - The range to check\n * @returns True if the range is exactly one cell\n */\nexport function isSingleCell(range: InternalCellRange): boolean {\n const normalized = normalizeRange(range);\n return normalized.startRow === normalized.endRow && normalized.startCol === normalized.endCol;\n}\n","/**\n * Selection Plugin Styles\n *\n * Scoped to tbw-grid using CSS nesting for light DOM compatibility.\n */\n@layer tbw-plugins {\n tbw-grid {\n /* Prevent text selection during range drag */\n &.selecting .data-grid-row > .cell {\n user-select: none;\n }\n\n /* Prevent text selection when any selection exists */\n &:has(.selection) {\n user-select: none;\n /* Disable cell-focus outline in row mode - row is the focus unit.\n Also disable row-focus outline - selection plugin uses ::after borders instead. */\n &[data-selection-mode='row'] .cell-focus,\n &[data-selection-mode='row'] .row-focus,\n &[data-selection-mode='range'] .cell-focus {\n outline: none;\n }\n }\n\n /* Row selection - use accent color for row focus (only when grid is focused) */\n .data-grid-row.row-focus {\n background-color: var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%));\n /* Replace outline with per-side pseudo-element borders that can collapse */\n outline: none;\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n border-width: 0;\n border-style: var(--tbw-selection-border-style, var(--tbw-border-style));\n border-color: var(--tbw-range-border-color, var(--tbw-color-accent));\n border-top-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n border-bottom-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n /* Must paint above sticky cells (.sticky-left/.sticky-right have\n z-index: 25 in core/styles/base.css) so the selection border is\n continuous across pinned columns. The border ::after has\n pointer-events: none, so it doesn't interfere with cell content. */\n z-index: 26;\n }\n\n /* Collapse borders between adjacent selected rows */\n + .data-grid-row.row-focus::after {\n border-top-width: 0;\n }\n &:has(+ .data-grid-row.row-focus)::after {\n border-bottom-width: 0;\n }\n\n /* Sticky cells (.sticky-left/.sticky-right) have an opaque\n background-color to hide unpinned content scrolling beneath them\n (see core/styles/rows.css). That opaque layer also hides the\n row-focus background-color set above. Re-paint sticky cells with\n the focus tint layered over panel-bg so the selection tint is\n visible across pinned columns. The selection BORDER is handled by\n the row's ::after with z-index above the sticky cells — no need\n to redraw it here. */\n > .cell.sticky-left,\n > .cell.sticky-right {\n background:\n linear-gradient(var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),\n var(--tbw-color-panel-bg);\n }\n }\n\n /* Non-selectable selected rows use the row-alt background as the muted\n indicator — keep sticky cells in sync so pinned columns don't flash\n the accent tint on rows that aren't actually selectable. */\n .data-grid-row.row-focus[data-selectable='false'] {\n > .cell.sticky-left,\n > .cell.sticky-right {\n background: var(--tbw-color-row-alt);\n }\n }\n\n /* Selection cell styles - for range mode */\n .data-grid-row > .cell.selected {\n background-color: var(--tbw-range-selection-bg);\n position: relative;\n\n /* Pseudo-element draws the selection range border inside the cell's\n padding box. Uses inset: 0 to stay within overflow: hidden bounds.\n The cell's own borders (row divider, column border) remain visible\n outside the selection border — this is intentional and consistent\n across all themes regardless of border width or visibility. */\n &::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n border: 0 var(--tbw-selection-border-style, var(--tbw-border-style)) var(--tbw-range-border-color);\n z-index: 1;\n }\n\n &.top::after {\n border-top-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n &.bottom::after {\n border-bottom-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n &.first::after {\n border-left-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n &.last::after {\n border-right-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n }\n\n /* Non-selectable row styling (row mode).\n The row is dimmed to signal \"not selectable\", but its *data* is still\n content the user has to read, so the exception WCAG 1.4.3 grants inactive\n UI components does not apply. 0.6 pushed the text to 4.24:1 on white;\n 0.65 is the lowest value that clears AA (5.02:1). */\n .data-grid-row[data-selectable='false'] {\n cursor: not-allowed;\n opacity: var(--tbw-selection-disabled-opacity, 0.65);\n\n /* Muted selection indicator */\n &.row-focus {\n background-color: var(--tbw-color-row-alt);\n }\n }\n\n /* Non-selectable cell styling (cell/range mode) */\n .data-grid-row > .cell[data-selectable='false'] {\n cursor: not-allowed;\n opacity: var(--tbw-selection-disabled-opacity, 0.65);\n\n /* Prevent selection highlight on non-selectable cells */\n &.selected {\n background-color: var(--tbw-selection-warning-bg, rgba(from var(--tbw-color-error) r g b / 50%));\n }\n }\n\n /* Selection summary in header */\n .tbw-selection-summary {\n font-size: var(--tbw-font-size-sm, 0.8125rem);\n color: var(--tbw-color-fg-muted);\n white-space: nowrap;\n }\n\n /* Checkbox column styles */\n .data-grid-row > .cell[data-field='__tbw_checkbox'],\n .header-row > .cell[data-field='__tbw_checkbox'] {\n text-align: center;\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n /* Row checkboxes are visual indicators - clicks pass through to cell */\n .tbw-select-row-checkbox {\n pointer-events: none;\n margin: 0;\n cursor: pointer;\n }\n\n /* Header select-all checkbox container */\n .tbw-checkbox-header {\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100%;\n cursor: pointer;\n }\n\n .tbw-select-all-checkbox {\n margin: 0;\n cursor: pointer;\n }\n\n /* SC 2.5.8: the row checkbox is not itself a target — the row click selects,\n so the whole cell accepts the pointer. Select-all has its own handler, and\n gets a cell-sized target from the `<label>` wrapper instead of a 24px box.\n Coarse pointers still grow the boxes, which is a comfort choice. */\n @media (pointer: coarse) {\n .tbw-select-row-checkbox,\n .tbw-select-all-checkbox {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n\n /* ---- Column-axis selection ----------------------------------------- */\n /* Background tint for selected columns. Applied to the matching header\n cell and every data cell whose data-col resolves to a selected field.\n The tint uses --tbw-column-selection-bg (overridable) defaulting to\n a 12 % accent overlay — visually distinct from row-focus but uses the\n same palette so themes stay coherent. */\n .header-row > .cell.column-selected,\n .data-grid-row > .cell.column-selected {\n background:\n linear-gradient(var(--tbw-column-selection-bg, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),\n var(--tbw-color-panel-bg);\n }\n\n /* Pinned columns repaint with sticky background; layer the column tint\n on top of the panel-bg fill so the indicator survives pinning. */\n .data-grid-row > .cell.column-selected.sticky-left,\n .data-grid-row > .cell.column-selected.sticky-right,\n .header-row > .cell.column-selected.sticky-left,\n .header-row > .cell.column-selected.sticky-right {\n background:\n linear-gradient(var(--tbw-column-selection-bg, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),\n var(--tbw-color-panel-bg);\n }\n\n /* ========== Touch selection mode (#304) ========== */\n\n /* Sticky action bar shown while touch selection mode is active. Rendered\n only on coarse-pointer devices, so the sizing targets a finger. */\n .tbw-selection-toolbar {\n position: sticky;\n top: 0;\n z-index: 30;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 8px;\n background: var(--tbw-color-panel-bg);\n border-bottom: 1px solid var(--tbw-color-border);\n font-size: 13px;\n }\n\n .tbw-selection-toolbar-count {\n font-weight: 600;\n white-space: nowrap;\n }\n\n .tbw-selection-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n /* `--tbw-touch-target-min` is owned by the touch-target sub-issue (#305);\n the literal fallback keeps this file readable on its own. */\n .tbw-selection-toolbar-btn {\n min-height: var(--tbw-touch-target-min, 24px);\n padding: 0 10px;\n border: 0;\n /* Themes set radii for much larger surfaces (Material uses 12px), which\n turns a 24px control into a lozenge. */\n border-radius: min(var(--tbw-border-radius, 4px), 6px);\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n\n &:hover,\n &:focus-visible {\n background: var(--tbw-color-row-hover, light-dark(#f0f0f0, #2a2a2a));\n }\n\n &[data-action='done'] {\n font-weight: 600;\n color: var(--tbw-color-accent-text, var(--tbw-color-accent));\n }\n }\n\n /* Draggable corner dots on an active cell range (iOS Numbers idiom).\n Additive — a mouse user gets them too. The visible dot is smaller than\n the hit area, and `touch-action` is off because the drag runs on pointer\n capture. */\n .tbw-range-handle {\n position: absolute;\n z-index: 25;\n width: var(--tbw-touch-target-min, 24px);\n height: var(--tbw-touch-target-min, 24px);\n translate: -50% -50%;\n background: radial-gradient(circle, var(--tbw-color-accent) 0 5px, transparent 5px);\n cursor: crosshair;\n touch-action: none;\n\n &[hidden] {\n display: none;\n }\n\n &.dragging {\n scale: 1.3;\n }\n\n /* Tapped rather than dragged: the handle is waiting for the cell that\n will receive this corner (WCAG 2.2 SC 2.5.7). */\n &.armed {\n scale: 1.3;\n outline: 2px solid var(--tbw-color-accent);\n outline-offset: -4px;\n border-radius: 50%;\n }\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Generic pointer-drag helper — converts a `pointerdown` event into a\n * deterministic drag lifecycle with pointer capture, threshold / long-press\n * promotion, and clean teardown.\n *\n * ## Usage\n * ```ts\n * element.addEventListener('pointerdown', (e) => {\n * startPointerDrag(e, element, {\n * onMove: (pe) => resize(pe.clientX),\n * onEnd: (pe) => commit(),\n * });\n * });\n * ```\n *\n * ## Fine vs coarse pointer promotion\n * For **fine** pointers (mouse / precision trackpad): set `longPressDuration`\n * to `0` (or omit it) so the drag is promoted immediately on first move.\n *\n * For **coarse** pointers (touch / stylus): set `longPressDuration` to e.g.\n * `400` ms so the drag is only promoted after the user holds still long\n * enough. Movement exceeding `longPressSlop` px before the timer fires\n * **cancels** the drag — this lets native scroll proceed uninterrupted.\n *\n * Branch example at call site:\n * ```ts\n * const isCoarse = e.pointerType === 'touch' || e.pointerType === 'pen';\n * startPointerDrag(e, el, handlers, { longPressDuration: isCoarse ? 400 : 0 });\n * ```\n *\n * ## Pointer capture\n * `setPointerCapture` / `releasePointerCapture` are called on `captureTarget`\n * only — **never** `document` / `window` listeners. Capture is claimed at\n * **promotion**, not on `pointerdown`: a captured pointer makes the browser\n * retarget the compatibility mouse events (`mouseup`, `click`, `dblclick`) to\n * the capture element, which would break click-driven grid features.\n *\n * ## Re-entrancy\n * Simultaneous drags on the same element with the same `pointerId` are\n * silently ignored (returns a no-op cancel function).\n *\n * ## DnD plugins\n * HTML5 drag-and-drop plugins (`RowDragDropPlugin`, `ReorderPlugin`, etc.)\n * are expected to consume this module in the future (#228). The API is\n * intentionally general.\n *\n * This module is **internal only** — NOT exported from `src/public.ts`.\n *\n * @since 3.5.0\n * @internal\n */\n\n// #region Types\n\n/**\n * Callbacks supplied to {@link startPointerDrag}.\n *\n * @since 3.5.0\n */\nexport interface PointerDragHandlers {\n /**\n * Called on each `pointermove` after the drag has been promoted.\n * For immediate drags (no threshold / long-press) this fires on the very\n * first move. For long-press promotion it fires only after the delay.\n */\n onMove(e: PointerEvent): void;\n\n /**\n * Called when the pointer is released after promotion (`pointerup`).\n * Not called if the drag was cancelled before promotion.\n */\n onEnd(e: PointerEvent): void;\n\n /**\n * Called once when the drag is promoted — when the movement threshold is\n * crossed or the long-press timer elapses. Fires before the first `onMove`.\n *\n * Long-press call sites use this to enter a mode (e.g. cell range-paint) and\n * give feedback at the moment the press is recognised, even if the pointer\n * never subsequently moves.\n */\n onPromote?(): void;\n\n /**\n * Called when the drag is cancelled — by `pointercancel`, `lostpointercapture`,\n * an `Escape` keydown, or the returned `cancel()` function.\n * Also called when the pointer is released *before* promotion (tap / short press).\n */\n onCancel?(): void;\n}\n\n/**\n * Options for {@link startPointerDrag}.\n *\n * @since 3.5.0\n */\nexport interface PointerDragOptions {\n /**\n * Minimum movement in px before the drag is promoted.\n * `0` (default) promotes immediately on the first `pointermove`.\n */\n threshold?: number;\n\n /**\n * Long-press duration in milliseconds.\n *\n * `0` (default) = no long-press requirement; the drag is promoted as soon\n * as the pointer moves (subject to `threshold`).\n *\n * When `> 0` the drag is promoted only after this many ms have elapsed\n * without the pointer exceeding `longPressSlop`. If the slop is exceeded\n * before the timer fires the entire drag is cancelled so native scroll can\n * proceed.\n *\n * Use this for **coarse** (touch/stylus) pointers to disambiguate a tap\n * or a scroll gesture from an intentional drag-to-paint interaction.\n */\n longPressDuration?: number;\n\n /**\n * Maximum pointer movement (px) permitted during the long-press wait.\n * If this distance is exceeded the drag is cancelled.\n * Default: `8`.\n */\n longPressSlop?: number;\n}\n\n// #endregion\n\n// #region Re-entrancy guard\n\n/**\n * Tracks active pointer IDs per capture target.\n * Prevents two simultaneous drags on the same element with the same pointer.\n */\nconst _activePointers = new WeakMap<Element, Set<number>>();\n\nfunction _claimPointer(el: Element, id: number): boolean {\n let set = _activePointers.get(el);\n if (!set) {\n set = new Set();\n _activePointers.set(el, set);\n }\n if (set.has(id)) return false;\n set.add(id);\n return true;\n}\n\nfunction _releasePointer(el: Element, id: number): void {\n _activePointers.get(el)?.delete(id);\n}\n\n// #endregion\n\n// #region startPointerDrag\n\n/**\n * Begin a pointer drag on `captureTarget`.\n *\n * Attaches `pointermove`, `pointerup`, `pointercancel`, and\n * `lostpointercapture` listeners directly to `captureTarget` (never to\n * `document` / `window`). `setPointerCapture` is called when the drag is\n * promoted so events continue to route here regardless of DOM changes; an\n * unpromoted press never captures and therefore leaves `click` / `dblclick`\n * targeting the element actually under the pointer.\n *\n * Returns a `cancel()` dispose function. Calling it is idempotent.\n *\n * @param startEvent - The originating `pointerdown` event.\n * @param captureTarget - Element on which to set pointer capture and attach\n * pointer listeners. Must be in a connected document.\n * @param handlers - `onMove`, `onEnd`, and optional `onCancel` callbacks.\n * @param options - Threshold, long-press, and slop configuration.\n *\n * @since 3.5.0\n */\nexport function startPointerDrag(\n startEvent: PointerEvent,\n captureTarget: Element,\n handlers: PointerDragHandlers,\n options?: PointerDragOptions,\n): () => void {\n const { onMove, onEnd, onCancel, onPromote } = handlers;\n const threshold = options?.threshold ?? 0;\n const longPressDuration = options?.longPressDuration ?? 0;\n const longPressSlop = options?.longPressSlop ?? 8;\n const pointerId = startEvent.pointerId;\n\n // Re-entrancy guard — silently ignore duplicate drags on same element/pointer\n if (!_claimPointer(captureTarget, pointerId)) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const startX = startEvent.clientX;\n const startY = startEvent.clientY;\n\n // Promotion: true when the drag has passed threshold/long-press requirements\n let promoted = threshold === 0 && longPressDuration === 0;\n let done = false;\n let longPressTimer: ReturnType<typeof setTimeout> | null = null;\n let captureFailed = false;\n\n // #region Internal helpers\n\n /**\n * Claim pointer capture — called at promotion, never at `pointerdown`.\n *\n * While an element holds pointer capture the browser retargets the\n * compatibility mouse events (`mouseup`, `click`, `dblclick`) to that element\n * instead of the element actually under the pointer. Capturing on press would\n * therefore break every click-driven feature in the grid (cell editing,\n * header sort, row click) because those handlers resolve their target with\n * `event.target.closest(...)`. Promotion is the point where the gesture is\n * known to be a drag, so capturing there keeps plain clicks intact.\n */\n function acquireCapture(): boolean {\n try {\n captureTarget.setPointerCapture(pointerId);\n return true;\n } catch {\n // setPointerCapture can fail in unit-test environments without full\n // browser APIs, or if the element was detached mid-gesture. Callers\n // typically flip UI state (cursor, user-select, `isResizing`) *before*\n // calling us, so report the failure as a cancellation — otherwise that\n // state is never rolled back and the UI sticks mid-drag.\n captureFailed = true;\n return false;\n }\n }\n\n /** Promote the gesture to a drag: take capture, then notify the caller. */\n function promote(): boolean {\n promoted = true;\n if (!acquireCapture()) {\n cancel();\n return false;\n }\n onPromote?.();\n return true;\n }\n\n function teardown(): void {\n if (done) return;\n done = true;\n if (longPressTimer !== null) {\n clearTimeout(longPressTimer);\n longPressTimer = null;\n }\n captureTarget.removeEventListener('pointermove', onPointerMove);\n captureTarget.removeEventListener('pointerup', onPointerUp);\n captureTarget.removeEventListener('pointercancel', onPointerCancel);\n captureTarget.removeEventListener('lostpointercapture', onLostCapture);\n document.removeEventListener('keydown', onEscapeKey, true);\n _releasePointer(captureTarget, pointerId);\n try {\n if (captureTarget.hasPointerCapture(pointerId)) {\n captureTarget.releasePointerCapture(pointerId);\n }\n } catch {\n // Ignore — element may have been removed from DOM\n }\n }\n\n function cancel(): void {\n if (done) return;\n teardown();\n onCancel?.();\n }\n\n // #endregion\n\n // #region Event handlers\n\n function onPointerMove(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n\n const dx = e.clientX - startX;\n const dy = e.clientY - startY;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n if (!promoted) {\n if (longPressDuration > 0) {\n // Cancel entire drag if movement exceeds slop while waiting for long-press\n if (dist > longPressSlop) {\n cancel();\n return;\n }\n // Long-press not yet elapsed — swallow event, no onMove\n return;\n }\n // Threshold promotion only (no long-press)\n if (threshold > 0 && dist < threshold) return;\n if (!promote()) return;\n }\n\n onMove(e);\n }\n\n function onPointerUp(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n const wasPromoted = promoted;\n teardown();\n if (wasPromoted) {\n onEnd(e);\n } else {\n onCancel?.();\n }\n }\n\n function onPointerCancel(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n cancel();\n }\n\n function onLostCapture(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n cancel();\n }\n\n function onEscapeKey(event: Event): void {\n const e = event as KeyboardEvent;\n if (e.key === 'Escape' && !done) cancel();\n }\n\n // #endregion\n\n // Attach listeners before starting the long-press timer\n captureTarget.addEventListener('pointermove', onPointerMove);\n captureTarget.addEventListener('pointerup', onPointerUp);\n captureTarget.addEventListener('pointercancel', onPointerCancel);\n captureTarget.addEventListener('lostpointercapture', onLostCapture);\n document.addEventListener('keydown', onEscapeKey, true);\n\n // Drags with neither a threshold nor a long-press are promoted up front.\n if (promoted) {\n promote();\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n if (captureFailed) return () => {};\n }\n\n // Start long-press timer after capture so the countdown is accurate\n if (longPressDuration > 0) {\n longPressTimer = setTimeout(() => {\n longPressTimer = null;\n if (done) return;\n promote();\n }, longPressDuration);\n }\n\n return cancel;\n}\n\n// #endregion\n","/**\n * Touch selection mode — long-press to enter, tap to toggle, tap \"Done\" to exit.\n *\n * This module owns the *presentation* half of the touch selection idiom\n * (#304): the selection-mode toolbar and the cell-range corner handles.\n * The state machine itself lives in `SelectionPlugin`, which drives these two\n * classes from its `afterRender()` hook.\n *\n * @module\n * @since 3.5.0\n */\n\nimport { startPointerDrag } from '../../core/internal/pointer-drag';\n\n// #region Types\n\n/**\n * What happens to the selection when touch selection mode is exited.\n *\n * - `'transient'` (default) — leaving selection mode clears the selection, the\n * Gmail / Google Photos behaviour. The mode *is* the selection.\n * - `'sticky'` — the selection survives, so a subsequent tap-to-toggle round\n * can start from what was already selected.\n *\n * @since 3.5.0\n */\nexport type TouchSelectionMode = 'sticky' | 'transient';\n\n/** Callbacks the toolbar invokes; all are supplied by `SelectionPlugin`. */\nexport interface SelectionToolbarCallbacks {\n /** \"Select all\" pressed. */\n selectAll(): void;\n /** \"Clear\" pressed. */\n clear(): void;\n /** \"Done\" pressed — exit selection mode. */\n done(): void;\n /** \"More…\" pressed. Receives the button so a menu can be anchored to it. */\n more(anchor: HTMLElement): void;\n}\n\n/** A normalized rectangle in (row, visible-column) space. */\nexport interface RangeRect {\n startRow: number;\n startCol: number;\n endRow: number;\n endCol: number;\n}\n\n/** Which corner of the range rectangle a handle controls. */\nexport type RangeCorner = 'start' | 'end';\n\n/** Callbacks the corner handles invoke while being dragged. */\nexport interface RangeHandleCallbacks {\n /** Resolve the cell under a viewport coordinate, or `null` if there is none. */\n cellAt(clientX: number, clientY: number): { row: number; col: number } | null;\n /** The dragged corner moved to a new cell. */\n resize(corner: RangeCorner, row: number, col: number): void;\n /** The drag finished (pointerup) or was aborted (Escape / pointercancel). */\n commit(): void;\n /**\n * The handle was pressed and released without moving — a tap, not a drag.\n * The non-dragging path required by WCAG 2.2 SC 2.5.7: the plugin arms this\n * corner and the next cell tap places it.\n */\n arm(corner: RangeCorner): void;\n}\n\n// #endregion\n\n// #region Selection-mode toolbar\n\n/** CSS class of the toolbar root. Also used by `selection.css`. */\nconst TOOLBAR_CLASS = 'tbw-selection-toolbar';\n\n/**\n * The sticky \"N selected · Select all · Clear · More… · Done\" bar shown while\n * touch selection mode is active.\n *\n * Mounted lazily on first {@link SelectionToolbar.show} and torn down by\n * {@link SelectionToolbar.destroy}. Buttons are `type=\"button\"` so the bar can\n * safely live inside a host form.\n */\nexport class SelectionToolbar {\n #root: HTMLElement | null = null;\n #count: HTMLElement | null = null;\n #more: HTMLButtonElement | null = null;\n #callbacks: SelectionToolbarCallbacks | null = null;\n\n /** True while the toolbar is in the DOM. */\n get mounted(): boolean {\n return this.#root !== null;\n }\n\n /** The toolbar root element, or `null` when not mounted. Test seam. */\n get element(): HTMLElement | null {\n return this.#root;\n }\n\n /**\n * Mount (if needed) and update the toolbar.\n *\n * @param container Element the toolbar is prepended to — normally `.tbw-grid-root`.\n * @param selectedCount Number of selected items, rendered as \"N selected\".\n * @param showMore Whether the \"More…\" overflow button is rendered. Only true\n * when a `ContextMenuPlugin` is registered, so touch users keep parity with\n * the right-click menu.\n */\n show(container: HTMLElement, selectedCount: number, showMore: boolean, callbacks: SelectionToolbarCallbacks): void {\n this.#callbacks = callbacks;\n if (!this.#root) this.#mount(container);\n if (this.#count) {\n this.#count.textContent = `${selectedCount} selected`;\n }\n this.#root?.setAttribute('data-selected-count', String(selectedCount));\n if (this.#more) this.#more.hidden = !showMore;\n }\n\n /** Remove the toolbar from the DOM. Safe to call when not mounted. */\n destroy(): void {\n this.#root?.remove();\n this.#root = null;\n this.#count = null;\n this.#more = null;\n this.#callbacks = null;\n }\n\n #mount(container: HTMLElement): void {\n const root = container.ownerDocument.createElement('div');\n root.className = TOOLBAR_CLASS;\n root.setAttribute('role', 'toolbar');\n root.setAttribute('aria-label', 'Selection');\n\n const count = container.ownerDocument.createElement('span');\n count.className = `${TOOLBAR_CLASS}-count`;\n count.setAttribute('aria-live', 'polite');\n root.appendChild(count);\n\n const actions = container.ownerDocument.createElement('div');\n actions.className = `${TOOLBAR_CLASS}-actions`;\n root.appendChild(actions);\n\n actions.appendChild(this.#button(container, 'select-all', 'Select all', () => this.#callbacks?.selectAll()));\n actions.appendChild(this.#button(container, 'clear', 'Clear', () => this.#callbacks?.clear()));\n const more = this.#button(container, 'more', 'More…', (btn) => this.#callbacks?.more(btn));\n more.setAttribute('aria-haspopup', 'menu');\n actions.appendChild(more);\n actions.appendChild(this.#button(container, 'done', 'Done', () => this.#callbacks?.done()));\n\n container.prepend(root);\n this.#root = root;\n this.#count = count;\n this.#more = more;\n }\n\n #button(\n container: HTMLElement,\n action: string,\n label: string,\n onClick: (btn: HTMLButtonElement) => void,\n ): HTMLButtonElement {\n const btn = container.ownerDocument.createElement('button');\n btn.type = 'button';\n btn.className = `${TOOLBAR_CLASS}-btn`;\n btn.dataset.action = action;\n btn.textContent = label;\n btn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n onClick(btn);\n });\n return btn;\n }\n}\n\n// #endregion\n\n// #region Cell-range corner handles\n\n/** CSS class of a corner handle. Also used by `selection.css`. */\nconst HANDLE_CLASS = 'tbw-range-handle';\n\n/**\n * The two draggable dots at the top-left and bottom-right corners of an active\n * cell range (the iOS Numbers / Google Sheets idiom).\n *\n * They are additive rather than touch-only: a mouse user can drag them too.\n * Whether they are rendered at all is decided by `SelectionPlugin`, which only\n * asks for them when the range was started by a coarse pointer.\n * Positioning is recomputed on every render because the grid recycles row DOM\n * during virtualization, so a handle's anchor cell may not exist at all after\n * a scroll — in that case the handle is simply hidden.\n */\nexport class RangeCornerHandles {\n #start: HTMLElement | null = null;\n #end: HTMLElement | null = null;\n #callbacks: RangeHandleCallbacks | null = null;\n #armed: RangeCorner | null = null;\n\n /**\n * Render (or hide) the handles for `rect`.\n *\n * @param container Positioned ancestor the handles are absolutely placed in.\n * @param rect The normalized range, or `null` to hide the handles.\n * @param cellFor Resolves the DOM cell for a (row, col) pair, or `undefined`\n * when that cell is outside the rendered window.\n */\n render(\n container: HTMLElement,\n rect: RangeRect | null,\n cellFor: (row: number, col: number) => HTMLElement | null,\n callbacks: RangeHandleCallbacks,\n ): void {\n this.#callbacks = callbacks;\n if (!rect) {\n this.#hide();\n return;\n }\n\n this.#start ??= this.#createHandle(container, 'start');\n this.#end ??= this.#createHandle(container, 'end');\n\n this.#place(container, this.#start, cellFor(rect.startRow, rect.startCol), 'start');\n this.#place(container, this.#end, cellFor(rect.endRow, rect.endCol), 'end');\n this.setArmed(this.#armed);\n }\n\n /**\n * Mark one corner as waiting for the tap that will place it, or clear the\n * marking with `null`. Purely presentational — the plugin owns the state.\n */\n setArmed(corner: RangeCorner | null): void {\n this.#armed = corner;\n for (const [handle, name] of [\n [this.#start, 'start'],\n [this.#end, 'end'],\n ] as const) {\n if (!handle) continue;\n const on = corner === name;\n handle.classList.toggle('armed', on);\n handle.setAttribute('aria-pressed', String(on));\n }\n }\n\n /** Remove both handles from the DOM. Safe to call when not rendered. */\n destroy(): void {\n this.#start?.remove();\n this.#end?.remove();\n this.#start = null;\n this.#end = null;\n this.#callbacks = null;\n this.#armed = null;\n }\n\n #hide(): void {\n if (this.#start) this.#start.hidden = true;\n if (this.#end) this.#end.hidden = true;\n }\n\n #place(container: HTMLElement, handle: HTMLElement | null, cell: HTMLElement | null, corner: RangeCorner): void {\n if (!handle) return;\n if (!cell) {\n handle.hidden = true;\n return;\n }\n handle.hidden = false;\n // getBoundingClientRect is unavailable/zeroed in some test environments;\n // the handle still renders (and is assertable) with a zero offset.\n const cellRect = cell.getBoundingClientRect?.();\n const hostRect = container.getBoundingClientRect?.();\n if (!cellRect || !hostRect) return;\n const x = corner === 'start' ? cellRect.left - hostRect.left : cellRect.right - hostRect.left;\n const y = corner === 'start' ? cellRect.top - hostRect.top : cellRect.bottom - hostRect.top;\n handle.style.left = `${x}px`;\n handle.style.top = `${y}px`;\n }\n\n #createHandle(container: HTMLElement, corner: RangeCorner): HTMLElement {\n const el = container.ownerDocument.createElement('div');\n el.className = `${HANDLE_CLASS} ${HANDLE_CLASS}-${corner}`;\n el.dataset.corner = corner;\n // A real button, not decoration: it is tappable as well as draggable, so it\n // needs a name and a pressed state for the armed (tap-to-place) mode.\n el.setAttribute('role', 'button');\n el.setAttribute('tabindex', '-1');\n el.setAttribute('aria-pressed', 'false');\n el.setAttribute(\n 'aria-label',\n corner === 'start'\n ? 'Selection start corner — drag, or activate to place it with a tap'\n : 'Selection end corner — drag, or activate to place it with a tap',\n );\n el.addEventListener('pointerdown', (e) => this.#onPointerDown(e as PointerEvent, el, corner));\n container.appendChild(el);\n return el;\n }\n\n #onPointerDown(e: PointerEvent, handle: HTMLElement, corner: RangeCorner): void {\n if (e.button !== 0 && e.pointerType === 'mouse') return;\n e.preventDefault();\n e.stopPropagation();\n const callbacks = this.#callbacks;\n if (!callbacks) return;\n\n handle.classList.add('dragging');\n // `startPointerDrag` promotes immediately with these options, so `onMove`\n // firing at least once is the only reliable signal that this was a drag.\n let moved = false;\n const finish = (): void => {\n handle.classList.remove('dragging');\n callbacks.commit();\n };\n startPointerDrag(e, handle, {\n onMove: (moveEvent) => {\n moved = true;\n const target = callbacks.cellAt(moveEvent.clientX, moveEvent.clientY);\n if (target) callbacks.resize(corner, target.row, target.col);\n },\n onEnd: () => {\n handle.classList.remove('dragging');\n if (moved) callbacks.commit();\n else callbacks.arm(corner);\n },\n // An aborted drag still leaves the range wherever the last move put it,\n // so the plugin must re-render — but it is never a tap.\n onCancel: finish,\n });\n }\n}\n\n// #endregion\n","/**\n * Selection Plugin (Class-based)\n *\n * Provides selection functionality for tbw-grid.\n * Supports three modes:\n * - 'cell': Single cell selection (default). No border, just focus highlight.\n * - 'row': Row selection. Clicking a cell selects the entire row.\n * - 'range': Range selection. Shift+click or drag to select rectangular cell ranges.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { getPrimaryPointer } from '../../core/internal/pointer-modality';\nimport { clearCellFocus, getRowIndexFromCell } from '../../core/internal/utils';\nimport type { GridElement, HeaderClickEvent, PluginManifest, PluginQuery } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin, CellClickEvent, CellMouseEvent } from '../../core/plugin/base-plugin';\nimport { isExpanderColumn, isUtilityColumn } from '../../core/plugin/expander-column';\nimport type { ColumnConfig } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport {\n createDragAlternativeMenu,\n type DragAlternativeAction,\n type DragAlternativeMenu,\n} from '../shared/drag-alternative-menu';\nimport {\n computeKeyboardExtension,\n fieldsBetween,\n normalizeMode,\n selectableColumnFields,\n type NormalizedModeConfig,\n} from './column-selection';\nimport {\n createRangeFromAnchor,\n getAllCellsInRanges,\n isCellInAnyRange,\n normalizeRange,\n rangesEqual,\n toPublicRanges,\n} from './range-selection';\nimport styles from './selection.css?inline';\nimport { RangeCornerHandles, SelectionToolbar, type RangeCorner, type RangeRect } from './touch-selection';\nimport type {\n CellRange,\n InternalCellRange,\n SelectionAxis,\n SelectionChangeDetail,\n SelectionConfig,\n SelectionMode,\n SelectionResult,\n} from './types';\n\n/**\n * Resolve the primary in-row mode from a config that may be a single string\n * or an array. Used by `configRules` (which run before `attach()` populates\n * the cached normalized mode). Falls back to `'cell'` on invalid input —\n * `attach()` will throw the proper error message later.\n */\nfunction primaryModeOf(mode: SelectionMode | SelectionMode[] | undefined): SelectionMode {\n if (typeof mode === 'string') return mode;\n if (Array.isArray(mode)) {\n const other = mode.find((m) => m !== 'column');\n if (other) return other;\n if (mode.includes('column')) return 'column';\n }\n return 'cell';\n}\n\n/** Special field name for the selection checkbox column */\nconst CHECKBOX_COLUMN_FIELD = '__tbw_checkbox';\n\n/**\n * Order band for the range-extension item contributed to the context menu.\n * `insertGroupSeparators` groups by the tens digit, so 60 keeps it in its own\n * group below column move (50-53).\n */\nconst EXTEND_ITEM_ORDER = 60;\n\n/** Keys that move grid focus — selection reacts to these in cell/range mode. */\nfunction isFormField(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || target.isContentEditable;\n}\n\nconst NAV_KEYS: readonly string[] = [\n 'ArrowUp',\n 'ArrowDown',\n 'ArrowLeft',\n 'ArrowRight',\n 'Tab',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n];\n\n/**\n * Build the selection change event detail for the current state.\n *\n * `axis` decides which axis \"won\" — when both row and column are populated\n * (which can only happen in transient states inside mutual-exclusion handling),\n * `axis` is the source of truth for which one to report.\n */\nfunction buildSelectionEvent(\n configuredMode: SelectionMode | SelectionMode[],\n axis: SelectionAxis,\n primary: SelectionMode,\n state: {\n selectedCell: { row: number; col: number } | null;\n selected: Set<number>;\n ranges: InternalCellRange[];\n selectedColumns: Set<string>;\n },\n colCount: number,\n visibleColumnFieldsInOrder: readonly string[],\n): SelectionChangeDetail {\n // Column axis active → ignore in-row state, report column field names in\n // visible-column order. WHY: `Set` insertion order reflects toggle history\n // (so the same selection emits different orderings depending on how the\n // user reached it), which makes `selection-change.detail.selectedColumns`\n // non-deterministic for consumers. Filtering the visible field list keeps\n // the event detail stable and consistent with `getSelectedColumns()`.\n if (axis === 'column' && state.selectedColumns.size > 0) {\n return {\n mode: configuredMode,\n activeAxis: 'column',\n ranges: [],\n selectedColumns: visibleColumnFieldsInOrder.filter((f) => state.selectedColumns.has(f)),\n };\n }\n\n if (primary === 'cell' && state.selectedCell) {\n return {\n mode: configuredMode,\n activeAxis: 'cell',\n ranges: [\n {\n from: { row: state.selectedCell.row, col: state.selectedCell.col },\n to: { row: state.selectedCell.row, col: state.selectedCell.col },\n },\n ],\n selectedColumns: [],\n };\n }\n\n if (primary === 'row' && state.selected.size > 0) {\n // Sort rows and merge contiguous indices into minimal ranges\n const sorted = [...state.selected].sort((a, b) => a - b);\n const ranges: CellRange[] = [];\n let start = sorted[0];\n let end = start;\n for (let i = 1; i < sorted.length; i++) {\n if (sorted[i] === end + 1) {\n end = sorted[i];\n } else {\n ranges.push({ from: { row: start, col: 0 }, to: { row: end, col: colCount - 1 } });\n start = sorted[i];\n end = start;\n }\n }\n ranges.push({ from: { row: start, col: 0 }, to: { row: end, col: colCount - 1 } });\n return { mode: configuredMode, activeAxis: 'row', ranges, selectedColumns: [] };\n }\n\n if (primary === 'range' && state.ranges.length > 0) {\n return {\n mode: configuredMode,\n activeAxis: 'range',\n ranges: toPublicRanges(state.ranges),\n selectedColumns: [],\n };\n }\n\n return { mode: configuredMode, activeAxis: 'none', ranges: [], selectedColumns: [] };\n}\n\n/**\n * Selection Plugin for tbw-grid\n *\n * Adds cell, row, and range selection capabilities to the grid with full keyboard support.\n * Whether you need simple cell highlighting or complex multi-range selections, this plugin has you covered.\n *\n * ## Installation\n *\n * ```ts\n * import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';\n * ```\n *\n * ## Selection Modes\n *\n * Configure the plugin with one of three modes via {@link SelectionConfig}:\n *\n * - **`'cell'`** - Single cell selection (default). Click cells to select individually.\n * - **`'row'`** - Full row selection. Click anywhere in a row to select the entire row.\n * - **`'range'`** - Rectangular selection. Click and drag or Shift+Click to select ranges.\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Arrow Keys` | Move selection |\n * | `Shift + Arrow` | Extend selection (range mode) |\n * | `Ctrl/Cmd + Click` | Toggle selection (multi-select) |\n * | `Shift + Click` | Extend to clicked cell/row |\n * | `Ctrl/Cmd + A` | Select all (range mode) |\n * | `Escape` | Clear selection |\n *\n * > **Note:** When `multiSelect: false`, Ctrl/Shift modifiers are ignored —\n * > clicks always select a single item.\n *\n * ## CSS Custom Properties\n *\n * | Property | Description |\n * |----------|-------------|\n * | `--tbw-focus-background` | Focused row background |\n * | `--tbw-range-selection-bg` | Range selection fill |\n * | `--tbw-range-border-color` | Range selection border |\n *\n * @example Basic row selection\n * ```ts\n * grid.gridConfig = {\n * columns: [...],\n * plugins: [new SelectionPlugin({ mode: 'row' })],\n * };\n * ```\n *\n * @example Range selection with event handling\n * ```ts\n * grid.gridConfig = {\n * plugins: [new SelectionPlugin({ mode: 'range' })],\n * };\n *\n * grid.on('selection-change', ({ mode, ranges }) => {\n * console.log(`Selected ${ranges.length} ranges in ${mode} mode`);\n * });\n * ```\n *\n * @example Programmatic selection control\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n *\n * // Get current selection\n * const selection = plugin.getSelection();\n * console.log(selection.ranges);\n *\n * // Set selection programmatically\n * plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: 5, col: 3 } }]);\n *\n * // Clear all selection\n * plugin.clearSelection();\n * ```\n *\n * @see {@link SelectionMode} for detailed mode descriptions\n * @see {@link SelectionConfig} for configuration options\n * @see {@link SelectionResult} for the selection result structure\n * @see {@link SelectionConfig} for interactive examples in the docs site\n * @since 0.1.1\n */\nexport class SelectionPlugin extends BaseGridPlugin<SelectionConfig> {\n /**\n * Plugin manifest - declares queries and configuration validation rules.\n * @internal\n */\n static override readonly manifest: PluginManifest<SelectionConfig> = {\n queries: [\n { type: 'getSelection', description: 'Get the current selection state' },\n { type: 'selectRows', description: 'Select specific rows by index (row mode only)' },\n { type: 'getSelectedRowIndices', description: 'Get sorted array of selected row indices' },\n { type: 'getSelectedRows', description: 'Get actual row objects for the current selection (works in all modes)' },\n { type: 'getSelectedColumns', description: 'Get field names of selected columns (column mode only)' },\n { type: 'selectColumns', description: 'Select specific columns by field name (column mode only)' },\n {\n type: 'getContextMenuItems',\n description: 'Contribute the non-dragging range-extension action (WCAG 2.2 SC 2.5.7)',\n },\n ],\n configRules: [\n {\n id: 'selection/range-dblclick',\n severity: 'warn',\n message:\n `\"triggerOn: 'dblclick'\" has no effect when mode is \"range\".\\n` +\n ` → Range selection uses drag interaction (mousedown → mousemove), not click events.\\n` +\n ` → The \"triggerOn\" option only affects \"cell\" and \"row\" selection modes.`,\n check: (config) => primaryModeOf(config.mode) === 'range' && config.triggerOn === 'dblclick',\n },\n {\n id: 'selection/column-checkbox',\n severity: 'warn',\n message:\n `\"checkbox: true\" only renders in row mode.\\n` +\n ` → Column selection has no checkbox UI; activate columns via Ctrl+Click on a header or Ctrl+Space on a focused cell.`,\n check: (config) => !!config.checkbox && primaryModeOf(config.mode) !== 'row',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'selection';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<SelectionConfig> {\n return {\n mode: 'cell',\n triggerOn: 'click',\n enabled: true,\n multiSelect: true,\n touchMode: 'transient',\n };\n }\n\n // #region Touch Selection State (#304)\n\n /**\n * True while touch selection mode is active — long-press entered it, plain\n * taps now toggle instead of replacing, and the selection toolbar is shown.\n *\n * Read-only for consumers; drive it with {@link exitTouchSelection}.\n *\n * @since 3.5.0\n */\n get touchSelectionActive(): boolean {\n return this.#touchActive;\n }\n\n #touchActive = false;\n /**\n * Whether the *current* range was started by a finger/stylus. Gates the\n * corner handles: on desktop they would sit inside other ranges (multi-range\n * is mouse-only) and get in the way of normal drag-selection.\n */\n #rangeFromCoarsePointer = false;\n readonly #toolbar = new SelectionToolbar();\n readonly #cornerHandles = new RangeCornerHandles();\n\n // #endregion\n\n // #region Non-dragging range alternatives (WCAG 2.2 SC 2.5.7)\n\n /** Corner waiting for the tap that will place it, set by tapping its handle. */\n #armedCorner: RangeCorner | null = null;\n /**\n * Last cell the user deliberately anchored with a *primary* interaction.\n * Kept separate from {@link cellAnchor} because a right-click outside the\n * range re-anchors that one so the menu targets the clicked cell — which\n * would leave \"Extend selection to here\" with nothing to extend from.\n */\n #extendAnchor: { row: number; col: number } | null = null;\n #extendMenu: DragAlternativeMenu | null = null;\n\n // #endregion\n\n // #region Internal State\n /** Row selection state (row mode) */\n private selected = new Set<number>();\n private lastSelected: number | null = null;\n private anchor: number | null = null;\n\n /** Range selection state (range mode) */\n private ranges: InternalCellRange[] = [];\n private activeRange: InternalCellRange | null = null;\n private cellAnchor: { row: number; col: number } | null = null;\n private isDragging = false;\n\n /** Pending keyboard navigation update (processed in afterRender) */\n private pendingKeyboardUpdate: { shiftKey: boolean } | null = null;\n\n /** Pending row-mode keyboard update (processed in afterRender) */\n private pendingRowKeyUpdate: { shiftKey: boolean } | null = null;\n\n /** Cell selection state (cell mode) */\n private selectedCell: { row: number; col: number } | null = null;\n\n /**\n * Column selection state (column mode / `['row','column']` array mode).\n * Stored as field-name strings so selection survives column pinning,\n * reordering, and virtualization recycling.\n */\n private selectedColumns = new Set<string>();\n /** Anchor column for Ctrl+Shift+click range extension. */\n private columnAnchor: string | null = null;\n /** Head column for Ctrl+Shift+Arrow keyboard extension. */\n private columnHead: string | null = null;\n /** Which axis last won. Drives `SelectionChangeDetail.activeAxis` and mutual exclusion. */\n private activeAxis: SelectionAxis = 'none';\n\n /**\n * Normalized mode config, computed once in `attach()`. All mode checks in this\n * plugin go through `this.#mode` rather than `this.config.mode` so the\n * single-string vs. array distinction is resolved in exactly one place.\n */\n #mode: NormalizedModeConfig = { primary: 'cell', columnEnabled: false, bothAxes: false };\n\n /** Last synced focus row — used to detect when grid focus moves so selection follows */\n private lastSyncedFocusRow = -1;\n /** Last synced focus col (cell mode) */\n private lastSyncedFocusCol = -1;\n\n /** Debounce timer for selection announcements */\n private announceTimer: ReturnType<typeof setTimeout> | null = null;\n /** Set by Shift/Ctrl+Space so the next announcement names the rows/columns selected. */\n #pendingSpanAnnouncement: { axis: 'row' | 'column'; count: number } | null = null;\n /** Range built by Shift/Ctrl+Space; Shift+Arrow keeps its spanned axis full while it is still `activeRange`. */\n #spanRange: { axis: 'row' | 'column'; range: InternalCellRange } | null = null;\n\n /** True when selection was explicitly set (click/keyboard) — prevents #syncSelectionToFocus from overwriting */\n private explicitSelection = false;\n\n // #endregion\n\n // #region Private Helpers - Selection Enabled Check\n\n /**\n * Check if selection is enabled at the grid level.\n * Grid-wide `selectable: false` or plugin's `enabled: false` disables all selection.\n */\n private isSelectionEnabled(): boolean {\n // Check plugin config first\n if (this.config.enabled === false) return false;\n // Check grid-level config\n return this.grid.effectiveConfig?.selectable !== false;\n }\n\n // #endregion\n\n // #region Private Helpers - Selectability\n\n /**\n * Check if a row/cell is selectable.\n * Returns true if selectable, false if not.\n */\n private checkSelectable(rowIndex: number, colIndex?: number): boolean {\n const { isSelectable } = this.config;\n if (!isSelectable) return true; // No callback = all selectable\n\n const row = this.rows[rowIndex];\n if (!row) return false;\n\n // colIndex is a visible-column index (from data-col), so use visibleColumns\n const column = colIndex !== undefined ? this.visibleColumns[colIndex] : undefined;\n return isSelectable(row, rowIndex, column, colIndex);\n }\n\n /**\n * Check if an entire row is selectable (for row mode).\n */\n private isRowSelectable(rowIndex: number): boolean {\n return this.checkSelectable(rowIndex);\n }\n\n /**\n * Check if a cell is selectable (for cell/range modes).\n */\n private isCellSelectable(rowIndex: number, colIndex: number): boolean {\n return this.checkSelectable(rowIndex, colIndex);\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Resolve the user-supplied mode (string OR array) into a single normalized\n // shape. Throws on invalid combinations (e.g. ['row', 'cell']).\n this.#mode = normalizeMode(this.config.mode);\n\n // Subscribe to events that invalidate selection\n // When rows change due to filtering/grouping/tree/sort operations, selection indices become invalid\n this.on('filter-change', () => this.clearSelectionSilent());\n this.on('group-toggle', () => this.clearSelectionSilent());\n this.on('tree-expand', () => this.clearSelectionSilent());\n this.on('sort-change', () => this.clearSelectionSilent());\n\n // Auto-select the row currently being edited so consumers of getSelectedRows()\n // / selectedRows() always see the row the user is actually working with.\n // Issue #284: editing and selection were independent, so a row could be in\n // edit mode while selectedRows() returned a stale (or different) row.\n // Only meaningful in row mode — cell mode tracks single-cell focus, range\n // mode is for bulk selection. We listen to `edit-open` (broadcast by\n // EditingPlugin) — `edit-close` is intentionally ignored so existing\n // selections are preserved when the user finishes editing.\n this.on<{ rowIndex: number; row: unknown }>('edit-open', ({ rowIndex, row }) => {\n if (!this.isSelectionEnabled()) return;\n if (row == null || rowIndex < 0) return;\n if (this.#mode.primary !== 'row') return;\n if (!this.isRowSelectable(rowIndex)) return;\n if (this.selected.has(rowIndex)) return;\n // multiSelect: false → replace; otherwise add to existing set so\n // multi-selection is preserved when the user enters edit on one row.\n if (this.config.multiSelect === false) {\n this.selected.clear();\n }\n this.selected.add(rowIndex);\n this.lastSelected = rowIndex;\n this.anchor = rowIndex;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n });\n\n // Source-row collection replaced from outside (host swapped `[rows]`).\n // `data-change` also fires for in-place cell edits, so gate on sourceRowCount\n // changing — the only signal that the source collection actually grew/shrank.\n // Without this, stored row indices resolve against a different array and\n // getSelectedRows() silently returns the wrong rows.\n let lastSourceRowCount = -1;\n grid.addEventListener(\n 'data-change',\n ((event: CustomEvent<{ sourceRowCount: number }>) => {\n const { sourceRowCount } = event.detail;\n const hasSelection = this.selected.size > 0 || this.ranges.length > 0 || this.selectedCell !== null;\n if (lastSourceRowCount !== -1 && sourceRowCount !== lastSourceRowCount && hasSelection) {\n this.clearSelectionSilent();\n }\n lastSourceRowCount = sourceRowCount;\n }) as EventListener,\n { signal: this.disconnectSignal },\n );\n\n // SC 2.5.7 fallback: without a ContextMenuPlugin there is no menu to\n // contribute \"Extend selection to here\" to, so host a bare one. Delegated\n // rather than per-cell — cells are recycled on every render.\n grid.addEventListener(\n 'contextmenu',\n ((event: MouseEvent) => {\n if (this.#contextMenuPlugin()) return; // the real menu owns this gesture\n const cell = (event.target as Element | null)?.closest?.<HTMLElement>('.cell[data-row][data-col]');\n if (!cell) return;\n const row = parseInt(cell.getAttribute('data-row') ?? '-1', 10);\n const col = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n const actions = this.#extendActions(row, col);\n if (!actions) return;\n event.preventDefault();\n this.#openExtendMenu(cell, actions);\n }) as EventListener,\n { signal: this.disconnectSignal },\n );\n }\n\n /**\n * Handle queries from other plugins.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getSelection') {\n return this.getSelection();\n }\n if (query.type === 'getSelectedRowIndices') {\n return this.getSelectedRowIndices();\n }\n if (query.type === 'getSelectedRows') {\n return this.getSelectedRows();\n }\n if (query.type === 'selectRows') {\n this.selectRows(query.context as number[]);\n return true;\n }\n if (query.type === 'getSelectedColumns') {\n return this.getSelectedColumns();\n }\n if (query.type === 'selectColumns') {\n const fields = query.context as string[];\n this.#setColumnSelection(fields);\n return true;\n }\n if (query.type === 'getContextMenuItems') {\n const params = query.context as ContextMenuParams | undefined;\n if (!params || params.isHeader) return undefined;\n const actions = this.#extendActions(params.rowIndex, params.columnIndex);\n if (!actions) return undefined;\n return actions.map((action, i): HeaderContextMenuItem => ({\n id: `selection-extend-${i}`,\n label: action.label,\n disabled: action.disabled,\n action: () => action.run(),\n order: EXTEND_ITEM_ORDER + i,\n }));\n }\n return undefined;\n }\n\n /** @internal */\n override detach(): void {\n // Clear aria-multiselectable that we set on the role=grid element.\n // Other lifecycle teardown happens below.\n const rowsBodyEl = this.gridElement?.querySelector('.rows-body');\n rowsBodyEl?.removeAttribute('aria-multiselectable');\n\n this.#toolbar.destroy();\n this.#cornerHandles.destroy();\n this.#extendMenu?.dispose();\n this.#extendMenu = null;\n this.#armedCorner = null;\n this.#extendAnchor = null;\n this.#touchActive = false;\n\n this.selected.clear();\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.isDragging = false;\n this.selectedCell = null;\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n this.activeAxis = 'none';\n this.pendingKeyboardUpdate = null;\n this.pendingRowKeyUpdate = null;\n this.lastSyncedFocusRow = -1;\n this.lastSyncedFocusCol = -1;\n // Cancel the debounced screen-reader announcement so it can't fire after\n // teardown (the callback reaches `announce()` → `requestAnimationFrame`,\n // which is undefined once the host/DOM env is gone — surfaces as an\n // \"Unhandled Error: requestAnimationFrame is not defined\" in tests).\n if (this.announceTimer) {\n clearTimeout(this.announceTimer);\n this.announceTimer = null;\n }\n }\n\n /**\n * Clear selection without emitting an event.\n * Used when selection is invalidated by external changes (filtering, grouping, etc.)\n *\n * Column selection is intentionally PRESERVED here — it tracks field names,\n * not row indices, so it stays valid across filter/sort/group changes. Use\n * {@link clearSelection} (public) or {@link #clearAllAxesSilent} for a full wipe.\n */\n private clearSelectionSilent(): void {\n this.selected.clear();\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.selectedCell = null;\n this.lastSelected = null;\n this.anchor = null;\n this.lastSyncedFocusRow = -1;\n this.lastSyncedFocusCol = -1;\n if (this.activeAxis !== 'column') {\n this.activeAxis = 'none';\n }\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Touch Selection Mode (#304)\n\n /**\n * True when a press originated from a finger or stylus.\n *\n * Prefers the per-event `pointerType` over the `(pointer: coarse)` media\n * query so that a hybrid device (e.g. a Surface with both a touchscreen and\n * a mouse) keeps mouse chords working while touch gets the long-press idiom.\n */\n #isCoarseEvent(e: MouseEvent): boolean {\n const pointerType = (e as PointerEvent).pointerType;\n if (pointerType === 'touch' || pointerType === 'pen') return true;\n if (pointerType === 'mouse') return false;\n return getPrimaryPointer() === 'coarse';\n }\n\n /**\n * Enter touch selection mode, seeding it with `rowIndex`.\n *\n * Long-pressing a row while already in the mode range-extends from the\n * anchor instead — that branch is handled by the caller.\n */\n #enterTouchSelection(rowIndex: number): boolean {\n if (!this.isRowSelectable(rowIndex)) return false;\n this.#touchActive = true;\n this.selected.clear();\n this.selected.add(rowIndex);\n this.anchor = rowIndex;\n this.lastSelected = rowIndex;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /**\n * Leave touch selection mode.\n *\n * With the default `touchMode: 'transient'` this also clears the selection —\n * the mode *is* the selection, as in Gmail. With `touchMode: 'sticky'` the\n * selection survives so a later round can build on it.\n *\n * No-op when the mode is not active.\n *\n * @since 3.5.0\n */\n exitTouchSelection(): void {\n if (!this.#touchActive) return;\n this.#touchActive = false;\n if ((this.config.touchMode ?? 'transient') === 'transient') {\n this.selected.clear();\n this.anchor = null;\n this.lastSelected = null;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n this.requestAfterRender();\n }\n\n /** Range-extend from the touch anchor to `rowIndex` (second long-press). */\n #extendTouchSelection(rowIndex: number): void {\n const anchor = this.anchor ?? rowIndex;\n const start = Math.min(anchor, rowIndex);\n const end = Math.max(anchor, rowIndex);\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) this.selected.add(i);\n }\n this.lastSelected = rowIndex;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /** The `ContextMenuPlugin` instance, when one is registered. */\n #contextMenuPlugin(): { showMenu(x: number, y: number, params: Record<string, unknown>): void } | undefined {\n const plugin = this.grid?.getPluginByName?.('contextMenu') ?? this.grid?.getPluginByName?.('context-menu');\n const candidate = plugin as { showMenu?: unknown } | undefined;\n return typeof candidate?.showMenu === 'function'\n ? (candidate as { showMenu(x: number, y: number, params: Record<string, unknown>): void })\n : undefined;\n }\n\n /** Resolve the rendered cell element for a (row, visible-col) pair. */\n #cellFor(row: number, col: number): HTMLElement | null {\n return this.gridElement?.querySelector<HTMLElement>(`.cell[data-row=\"${row}\"][data-col=\"${col}\"]`) ?? null;\n }\n\n /** Resolve the (row, col) of the cell under a viewport coordinate. */\n #cellAtPoint(clientX: number, clientY: number): { row: number; col: number } | null {\n const host = this.gridElement;\n const doc = host?.ownerDocument;\n const el = doc?.elementFromPoint?.(clientX, clientY);\n const cell = (el as Element | null)?.closest?.('.cell[data-row][data-col]');\n // `elementFromPoint` is document-wide: when several grids share a page the\n // drag can wander over a *different* grid's cells. Only accept cells that\n // belong to this grid instance.\n if (!cell || !host?.contains?.(cell)) return null;\n const row = parseInt(cell.getAttribute('data-row') ?? '-1', 10);\n const col = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n return row >= 0 && col >= 0 ? { row, col } : null;\n }\n\n /** Move one corner of the active range while its handle is dragged. */\n #resizeRangeCorner(corner: RangeCorner, row: number, col: number): void {\n const current = this.ranges[this.ranges.length - 1];\n if (!current) return;\n const normalized = normalizeRange(current);\n const anchor =\n corner === 'start'\n ? { row: normalized.endRow, col: normalized.endCol }\n : { row: normalized.startRow, col: normalized.startCol };\n const next = createRangeFromAnchor(anchor, { row, col });\n if (rangesEqual(current, next)) return;\n this.ranges[this.ranges.length - 1] = next;\n this.activeRange = next;\n this.cellAnchor = anchor;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n // #endregion\n // #region Non-dragging range alternatives (WCAG 2.2 SC 2.5.7)\n\n /**\n * The single-pointer alternative to paint-dragging a range: click the first\n * cell as usual, then open the context menu on the opposite corner and pick\n * \"Extend selection to here\".\n *\n * Returns `null` — not an empty array — when the action does not apply, so\n * the plugin contributes nothing rather than a dead entry.\n */\n #extendActions(rowIndex: number, colIndex: number): DragAlternativeAction[] | null {\n if (this.#mode.primary !== 'range') return null;\n if (!this.isSelectionEnabled()) return null;\n if (rowIndex < 0 || colIndex < 0) return null;\n if (!this.isCellSelectable(rowIndex, colIndex)) return null;\n const anchor = this.#extendAnchor;\n if (!anchor) return null;\n if (anchor.row === rowIndex && anchor.col === colIndex) return null;\n return [\n {\n label: 'Extend selection to here',\n run: () => this.#extendSelectionTo(rowIndex, colIndex),\n },\n ];\n }\n\n /** Replace the active range with the rectangle from {@link #extendAnchor} to (row, col). */\n #extendSelectionTo(row: number, col: number): void {\n const anchor = this.#extendAnchor;\n if (!anchor) return;\n const next = createRangeFromAnchor(anchor, { row, col });\n this.ranges = [next];\n this.activeRange = next;\n this.cellAnchor = anchor;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Fallback menu for when no `ContextMenuPlugin` is registered. The alternative\n * to a drag must exist whether or not an optional plugin is installed, so the\n * shared bare menu stands in — but it must never stack on top of the real one.\n */\n #openExtendMenu(anchor: HTMLElement, actions: DragAlternativeAction[]): void {\n this.#extendMenu ??= createDragAlternativeMenu('tbw-range-extend-menu', 'tbw-range-extend-menu');\n this.#extendMenu.open(anchor, 'Selection', actions);\n }\n\n /**\n * Arm (or disarm) a range corner after its handle was tapped rather than\n * dragged. The next cell click places the corner.\n */\n #armRangeCorner(corner: RangeCorner): void {\n this.#armedCorner = this.#armedCorner === corner ? null : corner;\n this.#cornerHandles.setArmed(this.#armedCorner);\n if (this.gridElement) {\n announce(\n this.gridElement,\n this.#armedCorner ? 'Select a cell to place the selection corner' : 'Selection corner placement cancelled',\n );\n }\n }\n\n /** Clear any armed corner. Returns `true` when something was actually cleared. */\n #disarmRangeCorner(): boolean {\n if (!this.#armedCorner) return false;\n this.#armedCorner = null;\n this.#cornerHandles.setArmed(null);\n return true;\n }\n\n // #endregion\n // #region Touch chrome\n\n /**\n * Render (or tear down) the touch chrome: the selection-mode toolbar and the\n * range corner handles. Called from `afterRender()`.\n *\n * The toolbar follows `#touchActive`, which can only have been set by a\n * coarse-pointer long-press — the modality check happens once, per event, at\n * entry. Re-testing the `(pointer: coarse)` media query here would break\n * hybrid devices (a Surface reports a fine primary pointer even while the\n * user is touching the screen). The corner handles follow the same rule via\n * `#rangeFromCoarsePointer`: they exist because touch has no Shift/Ctrl, so a\n * mouse-started range never gets them.\n */\n #renderTouchChrome(container: HTMLElement | null): void {\n if (!container) return;\n\n if (this.#touchActive) {\n this.#toolbar.show(container, this.selected.size, this.#contextMenuPlugin() !== undefined, {\n selectAll: () => this.selectAll(),\n clear: () => this.clearSelection(),\n done: () => this.exitTouchSelection(),\n more: (btn) => this.#openOverflowMenu(btn),\n });\n } else {\n this.#toolbar.destroy();\n }\n\n const active =\n this.#rangeFromCoarsePointer && this.#mode.primary === 'range' && this.ranges.length > 0\n ? this.ranges[this.ranges.length - 1]\n : null;\n const rect: RangeRect | null = active ? normalizeRange(active) : null;\n if (!rect) this.#disarmRangeCorner();\n this.#cornerHandles.render(container, rect, (row, col) => this.#cellFor(row, col), {\n cellAt: (x, y) => this.#cellAtPoint(x, y),\n resize: (corner, row, col) => this.#resizeRangeCorner(corner, row, col),\n commit: () => this.requestAfterRender(),\n arm: (corner) => this.#armRangeCorner(corner),\n });\n }\n\n /**\n * Surface the `ContextMenuPlugin` items from the toolbar's \"More…\" button, so\n * touch users don't lose the actions a mouse user reaches by right-clicking.\n */\n #openOverflowMenu(anchorEl: HTMLElement): void {\n const menu = this.#contextMenuPlugin();\n if (!menu) return;\n const rect = anchorEl.getBoundingClientRect?.();\n const rowIndex = this.lastSelected ?? -1;\n menu.showMenu(rect?.left ?? 0, rect?.bottom ?? 0, {\n row: rowIndex >= 0 ? this.rows[rowIndex] : null,\n rowIndex,\n // ContextMenuParams.selectedRows is a list of row *indices*, not row objects.\n selectedRows: this.getSelectedRowIndices(),\n });\n }\n\n // #endregion\n // #region Event Handlers\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return false;\n\n // Skip if event type doesn't match configured trigger.\n // This allows dblclick mode to only select on double-click.\n const { triggerOn = 'click' } = this.config;\n if (event.originalEvent.type !== triggerOn) return false;\n\n switch (this.#mode.primary) {\n case 'cell':\n return this.#clickSelectCell(event);\n case 'row':\n return this.#clickSelectRow(event);\n case 'range':\n return this.#clickSelectRange(event);\n default:\n return false;\n }\n }\n\n /**\n * CELL MODE: single-cell selection. Skips utility columns and non-selectable\n * cells, and only emits when the selection actually changed.\n */\n #clickSelectCell(event: CellClickEvent): boolean {\n const { rowIndex, colIndex } = event;\n\n // Check if this is a utility column (expander columns, etc.)\n // event.column is already resolved from _visibleColumns in the event builder.\n // Allow the event to propagate, but don't select utility cells.\n if (event.column && isUtilityColumn(event.column)) return false;\n if (!this.isCellSelectable(rowIndex, colIndex)) return false;\n\n const currentCell = this.selectedCell;\n // Same cell already selected\n if (currentCell && currentCell.row === rowIndex && currentCell.col === colIndex) return false;\n\n this.selectedCell = { row: rowIndex, col: colIndex };\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return false;\n }\n\n /** ROW MODE: multi-select with Shift/Ctrl, checkbox toggle, or single select. */\n #clickSelectRow(event: CellClickEvent): boolean {\n const { rowIndex, originalEvent, column } = event;\n if (!this.isRowSelectable(rowIndex)) return false;\n\n const multiSelect = this.config.multiSelect !== false;\n const shiftKey = originalEvent.shiftKey && multiSelect;\n // While touch selection mode is active a plain tap toggles, exactly as if\n // Ctrl were held — that is the whole point of the mode. Mouse chords are\n // untouched, so a hybrid device supports both at once.\n const ctrlKey = (originalEvent.ctrlKey || originalEvent.metaKey || this.#touchActive) && multiSelect;\n const isCheckbox = column?.checkboxColumn === true;\n\n if (shiftKey && this.anchor !== null) {\n // Shift+Click: Range select from anchor to clicked row\n const start = Math.min(this.anchor, rowIndex);\n const end = Math.max(this.anchor, rowIndex);\n if (!ctrlKey) this.selected.clear();\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) this.selected.add(i);\n }\n } else if (ctrlKey || (isCheckbox && multiSelect)) {\n // Ctrl+Click or checkbox click: Toggle individual row\n if (this.selected.has(rowIndex)) this.selected.delete(rowIndex);\n else this.selected.add(rowIndex);\n this.anchor = rowIndex;\n } else {\n // Plain click (or any click when multiSelect is false): select only clicked row.\n // Same row already selected → nothing to do.\n if (this.selected.size === 1 && this.selected.has(rowIndex)) return false;\n this.selected.clear();\n this.selected.add(rowIndex);\n this.anchor = rowIndex;\n }\n\n this.lastSelected = rowIndex;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return false;\n }\n\n /** RANGE MODE: Shift+click extends the selection, plain click starts a new one. */\n #clickSelectRange(event: CellClickEvent): boolean {\n const { rowIndex, colIndex, originalEvent } = event;\n\n // Skip utility columns and non-selectable cells - don't start selection from them\n if (event.column && isUtilityColumn(event.column)) return false;\n if (!this.isCellSelectable(rowIndex, colIndex)) return false;\n\n // A tapped (not dragged) corner handle is waiting for a destination — this\n // click places it instead of starting a new selection (SC 2.5.7).\n if (this.#armedCorner) {\n const corner = this.#armedCorner;\n this.#disarmRangeCorner();\n this.#resizeRangeCorner(corner, rowIndex, colIndex);\n return true;\n }\n\n // A tap never reaches `onCellMouseDown` (coarse presses only dispatch it\n // after a long-press), so the handle gate has to be updated here too.\n this.#rangeFromCoarsePointer = this.#isCoarseEvent(originalEvent);\n\n const shiftKey = originalEvent.shiftKey;\n const ctrlKey = (originalEvent.ctrlKey || originalEvent.metaKey) && this.config.multiSelect !== false;\n\n if (shiftKey && this.cellAnchor) {\n // Extend selection from anchor\n const newRange = createRangeFromAnchor(this.cellAnchor, { row: rowIndex, col: colIndex });\n\n // Check if range actually changed\n const currentRange = this.ranges.length > 0 ? this.ranges[this.ranges.length - 1] : null;\n if (currentRange && rangesEqual(currentRange, newRange)) return false;\n\n if (!ctrlKey) this.ranges = [newRange];\n else if (this.ranges.length > 0) this.ranges[this.ranges.length - 1] = newRange;\n else this.ranges.push(newRange);\n this.activeRange = newRange;\n } else {\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n if (ctrlKey) {\n this.ranges.push(newRange);\n } else {\n // Plain click - only emit if the same single-cell range isn't already selected\n if (this.ranges.length === 1 && rangesEqual(this.ranges[0], newRange)) return false;\n this.ranges = [newRange];\n }\n this.activeRange = newRange;\n this.cellAnchor = { row: rowIndex, col: colIndex };\n this.#extendAnchor = { row: rowIndex, col: colIndex };\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return false;\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return false;\n\n if (this.#mode.columnEnabled && this.#keyColumnAxis(event)) return true;\n\n const mode = this.#mode.primary;\n\n // Escape unwinds one step at a time: an armed range corner first, then\n // touch selection mode, then the selection itself.\n if (event.key === 'Escape' && this.#disarmRangeCorner()) return true;\n\n // Escape leaves touch selection mode first (an external keyboard is the\n // documented escape hatch), and only clears selection on a second press.\n if (event.key === 'Escape' && this.#touchActive) {\n this.exitTouchSelection();\n return true;\n }\n\n // Escape clears selection in all modes\n if (event.key === 'Escape') return this.#keyClearSelection(mode);\n\n const isNavKey = NAV_KEYS.includes(event.key);\n switch (mode) {\n case 'cell':\n return isNavKey ? this.#keyNavCellMode() : false;\n case 'row':\n return this.#keyRowMode(event);\n case 'range':\n return this.#keyRangeMode(event, isNavKey);\n default:\n return false;\n }\n }\n\n /**\n * Column-axis keyboard chords (only reachable when `columnEnabled`):\n * - **Ctrl/⌘+Space** — WAI-ARIA Grid: toggle column selection for the focused\n * column. Per spec the key is `' '`; some older hosts report `'Spacebar'`.\n * - **Ctrl/⌘+Shift+ArrowLeft/Right** — extend column selection along the\n * visible columns.\n *\n * @returns `true` when the chord was consumed.\n */\n #keyColumnAxis(event: KeyboardEvent): boolean {\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\n if (!ctrlOrMeta) return false;\n // Editors keep their own Ctrl+Space / Ctrl+Shift+Arrow (word selection) behaviour.\n if (isFormField(event.target) || this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n\n if (event.key === ' ' || event.key === 'Spacebar') {\n const column = this.visibleColumns[this.grid._focusCol];\n if (!column || isUtilityColumn(column) || typeof column.field !== 'string') return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectColumn(column.field, { toggle: true });\n return true;\n }\n\n if (\n event.shiftKey &&\n this.activeAxis === 'column' &&\n this.config.multiSelect !== false &&\n (event.key === 'ArrowLeft' || event.key === 'ArrowRight')\n ) {\n const fields = selectableColumnFields(this.visibleColumns);\n const direction = event.key === 'ArrowLeft' ? 'left' : 'right';\n const newHead = computeKeyboardExtension(this.columnHead, fields, direction);\n if (newHead === null || this.columnAnchor === null) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectColumn(newHead, { range: true });\n return true;\n }\n\n return false;\n }\n\n /** Escape: clear the active axis. Defers to EditingPlugin while an edit is open. */\n #keyClearSelection(mode: SelectionMode): boolean {\n // If editing is active, let the EditingPlugin cancel the active edit first\n if (this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n\n // Column axis — clear it only; a single Escape clears the *active* axis when\n // both axes are configured.\n if (this.activeAxis === 'column') {\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n this.activeAxis = 'none';\n } else if (mode === 'cell') {\n this.selectedCell = null;\n } else if (mode === 'row') {\n this.selected.clear();\n this.anchor = null;\n } else if (mode === 'range') {\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /** CELL MODE: selection follows focus (but respects selectability). */\n #keyNavCellMode(): boolean {\n // Use queueMicrotask so grid's handler runs first and updates focusRow/focusCol\n queueMicrotask(() => {\n const focusRow = this.grid._focusRow;\n const focusCol = this.grid._focusCol;\n // Only select if the cell is selectable; clear when navigating to a non-selectable cell\n this.selectedCell = this.isCellSelectable(focusRow, focusCol) ? { row: focusRow, col: focusCol } : null;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n });\n return false; // Let grid handle navigation\n }\n\n /** ROW MODE: Arrow/Page/Ctrl+Home/End move selection, Shift extends, Ctrl+A selects all. */\n #keyRowMode(event: KeyboardEvent): boolean {\n const multiSelect = this.config.multiSelect !== false;\n const isRowNavKey =\n event.key === 'ArrowUp' ||\n event.key === 'ArrowDown' ||\n event.key === 'PageUp' ||\n event.key === 'PageDown' ||\n ((event.ctrlKey || event.metaKey) && (event.key === 'Home' || event.key === 'End'));\n\n if (isRowNavKey) {\n const shiftKey = event.shiftKey && multiSelect;\n\n // Set anchor SYNCHRONOUSLY before grid moves focus\n if (shiftKey && this.anchor === null) this.anchor = this.grid._focusRow;\n\n // Mark explicit selection SYNCHRONOUSLY so #syncSelectionToFocus\n // won't overwrite the anchor if afterRender fires before our update\n this.explicitSelection = true;\n\n // Store pending update — processed in afterRender when grid has updated focusRow\n this.pendingRowKeyUpdate = { shiftKey };\n\n // Schedule afterRender (grid's refreshVirtualWindow(false) may skip it)\n queueMicrotask(() => this.requestAfterRender());\n return false; // Let grid handle navigation\n }\n\n return this.#keySelectAll(event, multiSelect);\n }\n\n /**\n * RANGE MODE: Shift+Arrow extends, plain Arrow resets, Ctrl+A selects all.\n * Tab always navigates without extending (even with Shift).\n */\n #keyRangeMode(event: KeyboardEvent, isNavKey: boolean): boolean {\n if (!isNavKey) return this.#keyRangeSpan(event) || this.#keySelectAll(event, this.config.multiSelect !== false);\n\n const shouldExtend = event.shiftKey && event.key !== 'Tab';\n\n // Capture anchor BEFORE grid moves focus (synchronous)\n // This ensures the anchor is the starting point, not the destination\n if (shouldExtend && !this.cellAnchor) {\n this.cellAnchor = { row: this.grid._focusRow, col: this.grid._focusCol };\n }\n\n // Mark pending update - will be processed in afterRender when grid updates focus\n this.pendingKeyboardUpdate = { shiftKey: shouldExtend };\n\n // Schedule afterRender to run after grid's keyboard handler completes.\n // Grid's refreshVirtualWindow(false) skips afterRender for performance,\n // so we explicitly request it to process pendingKeyboardUpdate.\n queueMicrotask(() => this.requestAfterRender());\n\n return false; // Let grid handle navigation\n }\n\n /**\n * WAI-ARIA APG grid chords in range mode:\n * - **Shift+Space** — select the row(s) spanned by the active range (or the focused row).\n * - **Ctrl/⌘+Space** — select the spanned column(s); only when the column axis is not\n * configured (otherwise {@link #keyColumnAxis} owns it).\n *\n * Skipped for form-field targets and while an edit is open so editors still type spaces.\n */\n #keyRangeSpan(event: KeyboardEvent): boolean {\n if (event.key !== ' ' && event.key !== 'Spacebar') return false;\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\n let axis: 'row' | 'column';\n if (event.shiftKey && !ctrlOrMeta) axis = 'row';\n else if (ctrlOrMeta && !event.shiftKey && !this.#mode.columnEnabled) axis = 'column';\n else return false;\n if (event.altKey) return false;\n if (isFormField(event.target)) return false;\n if (this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n\n const rowCount = this.rows.length;\n const colCount = this.visibleColumns.length;\n if (rowCount === 0 || colCount === 0) return false;\n\n const focus = { row: this.grid._focusRow, col: this.grid._focusCol };\n const span = this.activeRange\n ? normalizeRange(this.activeRange)\n : { startRow: focus.row, startCol: focus.col, endRow: focus.row, endCol: focus.col };\n const range: InternalCellRange =\n axis === 'row'\n ? { startRow: span.startRow, startCol: 0, endRow: span.endRow, endCol: colCount - 1 }\n : { startRow: 0, startCol: span.startCol, endRow: rowCount - 1, endCol: span.endCol };\n\n event.preventDefault();\n event.stopPropagation();\n\n // Anchor on the corner farthest from focus so a following Shift+Arrow keeps the far edge.\n const anchor = {\n row: Math.abs(focus.row - range.startRow) >= Math.abs(focus.row - range.endRow) ? range.startRow : range.endRow,\n col: Math.abs(focus.col - range.startCol) >= Math.abs(focus.col - range.endCol) ? range.startCol : range.endCol,\n };\n this.ranges = [range];\n this.activeRange = range;\n this.#spanRange = { axis, range };\n this.cellAnchor = anchor;\n this.#extendAnchor = anchor;\n this.#pendingSpanAnnouncement = {\n axis,\n count: axis === 'row' ? range.endRow - range.startRow + 1 : range.endCol - range.startCol + 1,\n };\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /** Ctrl/⌘+A: select all (skipped while editing, and when single-select). */\n #keySelectAll(event: KeyboardEvent, multiSelect: boolean): boolean {\n if (!multiSelect || event.key !== 'a' || !(event.ctrlKey || event.metaKey)) return false;\n if (this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectAll();\n return true;\n }\n\n /** @internal */\n override onCellMouseDown(event: CellMouseEvent): boolean | void {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n // ROW MODE — touch selection entry (#304).\n //\n // `event-delegation.ts` only dispatches `mousedown` for a coarse pointer\n // *after* a 400 ms long-press has elapsed, so reaching here with a coarse\n // event already means \"the user held the row down\". Header presses are\n // excluded — they belong to the column header menu (#270).\n if (this.#mode.primary === 'row') {\n if (event.rowIndex === undefined || event.rowIndex < 0) return;\n if (this.config.multiSelect === false) return;\n if (!this.#isCoarseEvent(event.originalEvent)) return;\n if (event.column && isUtilityColumn(event.column)) return;\n if (this.#touchActive) {\n this.#extendTouchSelection(event.rowIndex);\n return true;\n }\n return this.#enterTouchSelection(event.rowIndex) || undefined;\n }\n\n if (this.#mode.primary !== 'range') return;\n if (event.rowIndex === undefined || event.colIndex === undefined) return;\n if (event.rowIndex < 0) return; // Header\n\n // A tapped corner handle owns the next press: swallow it so no drag starts\n // and the range is not reset before `onCellClick` places the corner.\n if (this.#armedCorner) return true;\n\n this.#rangeFromCoarsePointer = this.#isCoarseEvent(event.originalEvent);\n\n // Right-click (secondary button) inside an existing range must NOT clear the\n // selection: this lets the context menu act on the whole range without the\n // user having to hold Ctrl. Outside any range, fall through to the normal\n // behavior (clear + select the clicked cell so the menu targets it).\n if (event.originalEvent.button === 2 && isCellInAnyRange(event.rowIndex, event.colIndex, this.ranges)) {\n return; // keep selection, don't start a drag\n }\n\n // Skip utility columns (expander columns, etc.)\n // event.column is already resolved from _visibleColumns in the event builder\n if (event.column && isUtilityColumn(event.column)) {\n return; // Don't start selection on utility columns\n }\n\n // Skip non-selectable cells - don't start drag from them\n if (!this.isCellSelectable(event.rowIndex, event.colIndex)) {\n return;\n }\n\n // Let onCellClick handle shift+click for range extension\n if (event.originalEvent.shiftKey && this.cellAnchor) {\n return;\n }\n\n // Start drag selection\n this.isDragging = true;\n const rowIndex = event.rowIndex;\n const colIndex = event.colIndex;\n\n // When multiSelect is false, Ctrl+click starts a new single range instead of adding\n const ctrlKey = (event.originalEvent.ctrlKey || event.originalEvent.metaKey) && this.config.multiSelect !== false;\n\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n\n // Check if selection is actually changing (for non-Ctrl clicks)\n if (!ctrlKey && this.ranges.length === 1 && rangesEqual(this.ranges[0], newRange)) {\n // Same cell already selected, just update anchor for potential drag\n this.cellAnchor = { row: rowIndex, col: colIndex };\n this.#setExtendAnchor(event, rowIndex, colIndex);\n return true;\n }\n\n this.cellAnchor = { row: rowIndex, col: colIndex };\n this.#setExtendAnchor(event, rowIndex, colIndex);\n\n if (!ctrlKey) {\n this.ranges = [];\n }\n\n this.ranges.push(newRange);\n this.activeRange = newRange;\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /**\n * Remember where a *primary* press anchored the range. A secondary press is\n * deliberately ignored: right-clicking outside the range re-anchors\n * {@link cellAnchor} so the menu targets the clicked cell, and reusing that\n * would make \"Extend selection to here\" extend from itself.\n */\n #setExtendAnchor(event: CellMouseEvent, row: number, col: number): void {\n if (event.originalEvent.button === 2) return;\n this.#extendAnchor = { row, col };\n }\n\n /** @internal */\n override onCellMouseMove(event: CellMouseEvent): boolean | void {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n if (this.#mode.primary !== 'range') return;\n if (!this.isDragging || !this.cellAnchor) return;\n if (event.rowIndex === undefined || event.colIndex === undefined) return;\n if (event.rowIndex < 0) return;\n\n // When dragging, clamp to first data column (skip utility columns)\n // colIndex from events is a visible-column index (from data-col)\n let targetCol = event.colIndex;\n const column = this.visibleColumns[targetCol];\n if (column && isUtilityColumn(column)) {\n // Find the first non-utility visible column\n const firstDataCol = this.visibleColumns.findIndex((col) => !isUtilityColumn(col));\n if (firstDataCol >= 0) {\n targetCol = firstDataCol;\n }\n }\n\n const newRange = createRangeFromAnchor(this.cellAnchor, { row: event.rowIndex, col: targetCol });\n\n // Only update and emit if the range actually changed\n const currentRange = this.ranges.length > 0 ? this.ranges[this.ranges.length - 1] : null;\n if (currentRange && rangesEqual(currentRange, newRange)) {\n return true; // Range unchanged, no need to update\n }\n\n if (this.ranges.length > 0) {\n this.ranges[this.ranges.length - 1] = newRange;\n } else {\n this.ranges.push(newRange);\n }\n this.activeRange = newRange;\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /** @internal */\n override onCellMouseUp(_event: CellMouseEvent): boolean | void {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n if (this.#mode.primary !== 'range') return;\n if (this.isDragging) {\n this.isDragging = false;\n return true;\n }\n }\n\n /**\n * Header click handler — drives column-axis selection.\n *\n * - Plain click (and plain Shift+click): defer to MultiSort / core sort by\n * returning `false`. Header-click sort behavior is unchanged.\n * - **Ctrl/Cmd+click**: toggle selection of the clicked column (or replace,\n * when `multiSelect: false`).\n * - **Ctrl/Cmd+Shift+click**: extend selection from the column anchor to the\n * clicked column. Avoids the plain `Shift+click` chord owned by MultiSort.\n *\n * No-ops when column selection isn't enabled or when the clicked column is\n * a utility column (`utility: true`).\n * @internal\n */\n override onHeaderClick(event: HeaderClickEvent): boolean | void {\n if (!this.isSelectionEnabled()) return false;\n if (!this.#mode.columnEnabled) return false;\n if (event.column && isUtilityColumn(event.column)) return false;\n\n const e = event.originalEvent;\n const ctrlKey = e.ctrlKey || e.metaKey;\n if (!ctrlKey) return false; // Plain / Shift click → sort path\n\n const field = event.field;\n if (!field) return false;\n\n e.preventDefault();\n if (typeof e.stopPropagation === 'function') e.stopPropagation();\n\n const range = e.shiftKey === true && this.config.multiSelect !== false && this.columnAnchor !== null;\n this.selectColumn(field, range ? { range: true } : { toggle: true });\n return true; // Handled — suppress sort\n }\n\n // #region Checkbox Column\n\n /**\n * Inject checkbox column when `checkbox: true` and mode is `'row'`.\n * @internal\n */\n override processColumns(columns: ColumnConfig[]): ColumnConfig[] {\n if (this.config.checkbox && this.#mode.primary === 'row') {\n // Check if checkbox column already exists\n if (columns.some((col) => col.field === CHECKBOX_COLUMN_FIELD)) {\n return columns;\n }\n const checkboxCol = this.#createCheckboxColumn();\n // Insert after expander column if present, otherwise first\n const expanderIdx = columns.findIndex(isExpanderColumn);\n const insertAt = expanderIdx >= 0 ? expanderIdx + 1 : 0;\n return [...columns.slice(0, insertAt), checkboxCol, ...columns.slice(insertAt)];\n }\n return columns;\n }\n\n /**\n * Create the checkbox utility column configuration.\n */\n #createCheckboxColumn(): ColumnConfig {\n return {\n field: CHECKBOX_COLUMN_FIELD,\n header: '',\n width: 32,\n resizable: false,\n sortable: false,\n lockPosition: true,\n utility: true,\n checkboxColumn: true,\n headerRenderer: () => {\n // A label, not a div: it forwards the pointer to the checkbox natively,\n // so the target is the whole 32px cell without inflating the box (SC 2.5.8).\n const container = document.createElement('label');\n container.className = 'tbw-checkbox-header';\n // Hide \"select all\" checkbox in single-select mode\n if (this.config.multiSelect === false) return container;\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-select-all-checkbox';\n // The wrapping label names the checkbox implicitly AND gives the\n // otherwise-empty `columnheader` cell screen-reader-visible text.\n const labelText = document.createElement('span');\n labelText.className = 'tbw-sr-only';\n labelText.textContent = getA11yMessage(this.gridElement, 'selectAllRows');\n checkbox.addEventListener('click', (e) => {\n e.stopPropagation(); // Prevent header sort\n if ((e.target as HTMLInputElement).checked) {\n this.selectAll();\n } else {\n this.clearSelection();\n }\n });\n // The label's own click keeps bubbling after it forwards to the\n // checkbox; stop it so the header cell sees nothing.\n container.addEventListener('click', (e) => e.stopPropagation());\n container.append(checkbox, labelText);\n return container;\n },\n renderer: (ctx) => {\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-select-row-checkbox';\n // Set initial checked state from current selection\n const cellEl = ctx.cellEl;\n const rowIndex = cellEl ? parseInt(cellEl.getAttribute('data-row') ?? '-1', 10) : -1;\n if (rowIndex >= 0) {\n checkbox.checked = this.selected.has(rowIndex);\n }\n checkbox.setAttribute('aria-label', getA11yMessage(this.gridElement, 'selectRow', Math.max(rowIndex, 0)));\n return checkbox;\n },\n };\n }\n\n /**\n * Update checkbox checked states to reflect current selection.\n * Called from #applySelectionClasses.\n */\n #updateCheckboxStates(gridEl: HTMLElement): void {\n // Update row checkboxes\n const rowCheckboxes = gridEl.querySelectorAll('.tbw-select-row-checkbox') as NodeListOf<HTMLInputElement>;\n rowCheckboxes.forEach((checkbox) => {\n const cell = checkbox.closest('.cell');\n const rowIndex = cell ? getRowIndexFromCell(cell) : -1;\n if (rowIndex >= 0) {\n checkbox.checked = this.selected.has(rowIndex);\n }\n });\n\n // Update header select-all checkbox\n const headerCheckbox = gridEl.querySelector('.tbw-select-all-checkbox') as HTMLInputElement | null;\n if (headerCheckbox) {\n const rowCount = this.rows.length;\n let selectableCount = 0;\n if (this.config.isSelectable) {\n for (let i = 0; i < rowCount; i++) {\n if (this.isRowSelectable(i)) selectableCount++;\n }\n } else {\n selectableCount = rowCount;\n }\n const allSelected = selectableCount > 0 && this.selected.size >= selectableCount;\n const someSelected = this.selected.size > 0;\n headerCheckbox.checked = allSelected;\n headerCheckbox.indeterminate = someSelected && !allSelected;\n }\n }\n\n // #endregion\n\n /**\n * True when focus is currently inside the grid host (data-has-focus is set on focusin).\n * Used to determine whether selection should follow the focus cursor.\n */\n #gridHasFocus(): boolean {\n return this.gridElement?.hasAttribute('data-has-focus') ?? false;\n }\n\n /**\n * Sync selection state to the grid's current focus position.\n * In row mode, keeps `selected` in sync with `_focusRow`.\n * In cell mode, keeps `selectedCell` in sync with `_focusRow`/`_focusCol`.\n * Only updates when the focus has changed since the last sync.\n * Skips when `explicitSelection` is set (click/keyboard set selection directly).\n *\n * Issue #392: selection must be a deliberate user action. The grid's focus\n * cursor defaults to (0,0) even before focus enters the grid, so following it\n * unconditionally auto-selects row 0 / cell 0,0 on first render. Only follow\n * focus when focus is genuinely inside the grid. Always advance the synced\n * markers so the resting position is treated as already-synced (tabbing in and\n * landing on the default cell must not select it).\n */\n #syncSelectionToFocus(mode: string): void {\n const focusRow = this.grid._focusRow;\n const focusCol = this.grid._focusCol;\n\n // Only follow focus when the grid actually has focus. Otherwise, keep the\n // synced markers in step so the resting position is not retroactively selected\n // when focus enters the grid later.\n if (!this.#gridHasFocus()) {\n this.lastSyncedFocusRow = focusRow;\n this.lastSyncedFocusCol = focusCol;\n return;\n }\n\n if (mode === 'row') {\n // Skip auto-sync when selection was explicitly set (Shift/Ctrl click, keyboard)\n if (this.explicitSelection) {\n this.explicitSelection = false;\n this.lastSyncedFocusRow = focusRow;\n return;\n }\n\n if (focusRow !== this.lastSyncedFocusRow) {\n this.lastSyncedFocusRow = focusRow;\n if (this.isRowSelectable(focusRow)) {\n if (!this.selected.has(focusRow) || this.selected.size !== 1) {\n this.selected.clear();\n this.selected.add(focusRow);\n this.lastSelected = focusRow;\n this.anchor = focusRow;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n }\n }\n }\n\n if (mode === 'cell') {\n if (this.explicitSelection) {\n this.explicitSelection = false;\n this.lastSyncedFocusRow = focusRow;\n this.lastSyncedFocusCol = focusCol;\n return;\n }\n\n if (focusRow !== this.lastSyncedFocusRow || focusCol !== this.lastSyncedFocusCol) {\n this.lastSyncedFocusRow = focusRow;\n this.lastSyncedFocusCol = focusCol;\n if (this.isCellSelectable(focusRow, focusCol)) {\n const cur = this.selectedCell;\n if (!cur || cur.row !== focusRow || cur.col !== focusCol) {\n this.selectedCell = { row: focusRow, col: focusCol };\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n }\n }\n }\n }\n\n /**\n * Apply CSS selection classes to row/cell elements.\n * Shared by afterRender and onScrollRender.\n */\n #applySelectionClasses(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const mode = this.#mode.primary;\n const columnEnabled = this.#mode.columnEnabled;\n const hasSelectableCallback = !!this.config.isSelectable;\n\n // Reflect multi-select capability on the role=grid element so screen readers\n // announce the grid as multi-selectable. WAI-ARIA requires aria-multiselectable\n // on the element carrying role=\"grid\" — that's `.rows-body` in our render tree,\n // not the host. `multiSelect` defaults to true; only `false` opts out.\n const rowsBodyEl = gridEl.querySelector('.rows-body');\n if (rowsBodyEl) {\n const multi = this.config.multiSelect !== false;\n rowsBodyEl.setAttribute('aria-multiselectable', multi ? 'true' : 'false');\n }\n\n // Clear all selection classes first (including column-selected).\n // Also reset stale aria-selected on cells that previously carried a\n // selection marker — range/column passes set aria-selected=\"true\" on\n // selected cells, and core only updates the attribute when focus\n // changes — so without an explicit reset a cell that lost selection\n // (axis flipped, range shrank, virtualization recycled the node into a\n // non-selected position) would keep the stale value. We MUST scope the\n // reset to cells that had `.selected` / `.column-selected` BEFORE the\n // class wipe — clearing aria-selected unconditionally would also blow\n // away the focused-cell marker that core's keyboard handler / row\n // renderer set for the active cell, regressing keyboard a11y.\n // Header aria-selected is handled in the columnEnabled block below.\n const allCells = gridEl.querySelectorAll('.cell');\n allCells.forEach((cell) => {\n const hadSelectionMarker =\n cell.classList.contains(GridClasses.SELECTED) || cell.classList.contains('column-selected');\n cell.classList.remove(GridClasses.SELECTED, 'top', 'bottom', 'first', 'last', 'column-selected');\n if (hadSelectionMarker) {\n cell.removeAttribute('aria-selected');\n }\n // Clear selectable attribute - will be re-applied below\n if (hasSelectableCallback) {\n cell.removeAttribute('data-selectable');\n }\n });\n\n const allRows = gridEl.querySelectorAll('.data-grid-row');\n allRows.forEach((row) => {\n row.classList.remove(GridClasses.SELECTED, 'row-focus');\n row.setAttribute('aria-selected', 'false');\n // Clear selectable attribute - will be re-applied below\n if (hasSelectableCallback) {\n row.removeAttribute('data-selectable');\n }\n });\n\n // Clear column-selected from header cells too\n if (columnEnabled) {\n const headerCells = gridEl.querySelectorAll('.header-row > .cell');\n headerCells.forEach((cell) => {\n cell.classList.remove('column-selected');\n cell.removeAttribute('aria-selected');\n });\n }\n\n // ROW MODE: Add row-focus class to selected rows, disable cell-focus, update checkboxes\n if (mode === 'row') {\n // In row mode, disable ALL cell-focus styling - row selection takes precedence\n clearCellFocus(gridEl);\n\n allRows.forEach((row) => {\n const firstCell = row.querySelector('.cell[data-row]');\n const rowIndex = getRowIndexFromCell(firstCell);\n if (rowIndex >= 0) {\n // Mark non-selectable rows\n if (hasSelectableCallback && !this.isRowSelectable(rowIndex)) {\n row.setAttribute('data-selectable', 'false');\n }\n if (this.selected.has(rowIndex)) {\n row.classList.add(GridClasses.SELECTED, 'row-focus');\n row.setAttribute('aria-selected', 'true');\n }\n }\n });\n\n // Update checkbox states if checkbox column is enabled\n if (this.config.checkbox) {\n this.#updateCheckboxStates(gridEl);\n }\n }\n\n // CELL/RANGE MODE: Mark non-selectable cells\n if ((mode === 'cell' || mode === 'range') && hasSelectableCallback) {\n const cells = gridEl.querySelectorAll('.cell[data-row][data-col]');\n cells.forEach((cell) => {\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) {\n if (!this.isCellSelectable(rowIndex, colIndex)) {\n cell.setAttribute('data-selectable', 'false');\n }\n }\n });\n }\n\n // RANGE MODE: Add selected and edge classes to cells\n // Uses neighbor-based edge detection for correct multi-range borders\n if (mode === 'range' && this.ranges.length > 0) {\n // Clear all cell-focus first - selection plugin manages focus styling in range mode\n clearCellFocus(gridEl);\n\n // Pre-normalize ranges for efficient neighbor checks\n const normalizedRanges = this.ranges.map(normalizeRange);\n\n // Fast selection check against pre-normalized ranges\n const isInSelection = (r: number, c: number): boolean => {\n for (const range of normalizedRanges) {\n if (r >= range.startRow && r <= range.endRow && c >= range.startCol && c <= range.endCol) {\n return true;\n }\n }\n return false;\n };\n\n const cells = gridEl.querySelectorAll('.cell[data-row][data-col]');\n cells.forEach((cell) => {\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) {\n // Skip utility columns entirely - don't add any selection classes\n // colIndex from data-col is a visible-column index\n const column = this.visibleColumns[colIndex];\n if (column && isUtilityColumn(column)) {\n return;\n }\n\n if (isInSelection(rowIndex, colIndex)) {\n cell.classList.add(GridClasses.SELECTED);\n cell.setAttribute('aria-selected', 'true');\n\n // Edge detection: add border class where neighbor is not selected\n // This handles single ranges, multi-range, and irregular selections correctly\n if (!isInSelection(rowIndex - 1, colIndex)) cell.classList.add('top');\n if (!isInSelection(rowIndex + 1, colIndex)) cell.classList.add('bottom');\n if (!isInSelection(rowIndex, colIndex - 1)) cell.classList.add('first');\n if (!isInSelection(rowIndex, colIndex + 1)) cell.classList.add('last');\n }\n }\n });\n }\n\n // CELL MODE: Let the grid's native .cell-focus styling handle cell highlighting\n // No additional action needed - the grid already manages focus styling\n\n // COLUMN AXIS: Apply column-selected class + aria-selected to the header cell\n // and every data cell in the matching column. Identifies columns by their\n // visible-index (data-col matches the visibleColumns position) so it works\n // regardless of pinning / reordering — selectedColumns stores fields, not\n // indices, so the rendering layer resolves them per-render.\n if (columnEnabled && this.selectedColumns.size > 0) {\n // Build visible-index → field map once for O(1) lookups.\n const colFieldByIndex: (string | undefined)[] = this.visibleColumns.map((c) =>\n typeof c.field === 'string' ? c.field : undefined,\n );\n\n // Header cells — the grid renders header cells with data-col attributes.\n const headerCells = gridEl.querySelectorAll<HTMLElement>('.header-row > .cell[data-col]');\n headerCells.forEach((cell) => {\n const colIndex = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n const field = colIndex >= 0 ? colFieldByIndex[colIndex] : undefined;\n if (field && this.selectedColumns.has(field)) {\n cell.classList.add('column-selected');\n cell.setAttribute('aria-selected', 'true');\n }\n });\n\n // Data cells.\n const cells = gridEl.querySelectorAll<HTMLElement>('.cell[data-col]:not(.header-row .cell)');\n cells.forEach((cell) => {\n // Skip header cells (filter above isn't reliable for nested .header-row scoping)\n if (cell.closest('.header-row')) return;\n const colIndex = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n if (colIndex < 0) return;\n const column = this.visibleColumns[colIndex];\n if (!column || isUtilityColumn(column)) return;\n const field = colFieldByIndex[colIndex];\n if (field && this.selectedColumns.has(field)) {\n cell.classList.add('column-selected');\n cell.setAttribute('aria-selected', 'true');\n }\n });\n }\n }\n\n /** @internal */\n override afterRender(): void {\n // Skip rendering selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const container = gridEl.querySelector('.tbw-grid-root');\n const mode = this.#mode.primary;\n\n // Process pending row keyboard navigation update (row mode)\n // This runs AFTER the grid has updated focusRow\n if (this.pendingRowKeyUpdate && mode === 'row') {\n const { shiftKey } = this.pendingRowKeyUpdate;\n this.pendingRowKeyUpdate = null;\n\n const focusRow = this.grid._focusRow;\n\n if (shiftKey && this.anchor !== null) {\n // Shift+nav: Extend selection from anchor to new focus\n this.selected.clear();\n const start = Math.min(this.anchor, focusRow);\n const end = Math.max(this.anchor, focusRow);\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) {\n this.selected.add(i);\n }\n }\n } else {\n // Plain nav: Single select\n if (this.isRowSelectable(focusRow)) {\n this.selected.clear();\n this.selected.add(focusRow);\n this.anchor = focusRow;\n } else {\n this.selected.clear();\n }\n }\n\n this.lastSelected = focusRow;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n\n // Process pending keyboard navigation update (range mode)\n // This runs AFTER the grid has updated focusRow/focusCol\n if (this.pendingKeyboardUpdate && mode === 'range') {\n const { shiftKey } = this.pendingKeyboardUpdate;\n this.pendingKeyboardUpdate = null;\n\n const currentRow = this.grid._focusRow;\n const currentCol = this.grid._focusCol;\n\n if (shiftKey && this.cellAnchor) {\n // Extend selection from anchor to current focus\n let newRange = createRangeFromAnchor(this.cellAnchor, { row: currentRow, col: currentCol });\n const span = this.#spanRange;\n if (span && span.range === this.activeRange) {\n newRange =\n span.axis === 'row'\n ? { ...newRange, startCol: 0, endCol: this.visibleColumns.length - 1 }\n : { ...newRange, startRow: 0, endRow: this.rows.length - 1 };\n span.range = newRange;\n }\n this.ranges = [newRange];\n this.activeRange = newRange;\n } else if (!shiftKey) {\n // Without shift, clear selection (cell-focus will show instead)\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = { row: currentRow, col: currentCol }; // Reset anchor to current position\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n\n // Sync selection to grid's focus position.\n // This ensures selection follows keyboard navigation (Tab, arrows, etc.)\n // regardless of which plugin moved the focus.\n this.#syncSelectionToFocus(mode);\n\n // Set data attribute on host for CSS variable scoping\n this.gridElement.setAttribute('data-selection-mode', mode);\n\n // Toggle .selecting class during drag to prevent text selection\n if (container) {\n container.classList.toggle('selecting', this.isDragging);\n }\n\n this.#applySelectionClasses();\n this.#renderTouchChrome(container as HTMLElement | null);\n }\n\n /**\n * Called after scroll-triggered row rendering.\n * Reapplies selection classes to recycled DOM elements.\n * @internal\n */\n override onScrollRender(): void {\n // Skip rendering selection classes if disabled\n if (!this.isSelectionEnabled()) return;\n\n this.#applySelectionClasses();\n }\n\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current selection as a unified result.\n * Works for all selection modes and always returns ranges.\n *\n * @example\n * ```ts\n * const selection = plugin.getSelection();\n * if (selection.ranges.length > 0) {\n * const { from, to } = selection.ranges[0];\n * // For cell mode: from === to (single cell)\n * // For row mode: from.col = 0, to.col = lastCol (full row)\n * // For range mode: rectangular selection\n * }\n * ```\n */\n getSelection(): SelectionResult {\n const event = this.#buildEvent();\n return {\n mode: this.config.mode,\n activeAxis: event.activeAxis,\n ranges: event.ranges,\n selectedColumns: event.selectedColumns,\n anchor: this.cellAnchor,\n };\n }\n\n /**\n * Get all selected cells across all ranges.\n */\n getSelectedCells(): Array<{ row: number; col: number }> {\n return getAllCellsInRanges(this.ranges);\n }\n\n /**\n * Check if a specific cell is in range selection.\n */\n isCellSelected(row: number, col: number): boolean {\n return isCellInAnyRange(row, col, this.ranges);\n }\n\n /**\n * Select all selectable rows (row mode) or all cells (range mode).\n *\n * In row mode, selects every row where `isSelectable` returns true (or all rows if no callback).\n * In range mode, creates a single range spanning all rows and visible columns.\n * Array modes resolve to their row/range axis. Has no effect in cell mode.\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * plugin.selectAll(); // Selects everything in current mode\n * ```\n */\n selectAll(): void {\n const mode = this.#mode.primary;\n\n // Single-select mode: selectAll is a no-op\n if (this.config.multiSelect === false) return;\n\n if (mode === 'row') {\n this.selected.clear();\n for (let i = 0; i < this.rows.length; i++) {\n if (this.isRowSelectable(i)) {\n this.selected.add(i);\n }\n }\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n } else if (mode === 'range') {\n const rowCount = this.rows.length;\n const colCount = this.visibleColumns.length;\n if (rowCount > 0 && colCount > 0) {\n const allRange: InternalCellRange = {\n startRow: 0,\n startCol: 0,\n endRow: rowCount - 1,\n endCol: colCount - 1,\n };\n this.ranges = [allRange];\n this.activeRange = allRange;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n }\n }\n\n /**\n * Select specific rows by index (row mode only).\n * Replaces the current selection with the provided row indices.\n * Indices that are out of bounds or fail the `isSelectable` check are ignored.\n *\n * @param indices - Array of row indices to select\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * plugin.selectRows([0, 2, 4]); // Select rows 0, 2, and 4\n * ```\n */\n selectRows(indices: number[]): void {\n if (this.#mode.primary !== 'row') return;\n // In single-select mode, only use the last index\n const effectiveIndices =\n this.config.multiSelect === false && indices.length > 1 ? [indices[indices.length - 1]] : indices;\n this.selected.clear();\n for (const idx of effectiveIndices) {\n if (idx >= 0 && idx < this.rows.length && this.isRowSelectable(idx)) {\n this.selected.add(idx);\n }\n }\n this.anchor = effectiveIndices.length > 0 ? effectiveIndices[effectiveIndices.length - 1] : null;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Get the indices of all selected rows (convenience for row mode).\n * Returns indices sorted in ascending order.\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * const rows = plugin.getSelectedRowIndices(); // [0, 2, 4]\n * ```\n */\n getSelectedRowIndices(): number[] {\n return [...this.selected].sort((a, b) => a - b);\n }\n\n /**\n * Get the actual row objects for the current selection.\n *\n * Works across all selection modes:\n * - **Row mode**: Returns the row objects for all selected rows.\n * - **Cell mode**: Returns the single row containing the selected cell, or `[]`.\n * - **Range mode**: Returns the unique row objects that intersect any selected range.\n *\n * Row objects are resolved from the grid's processed (sorted/filtered) row array,\n * so they always reflect the current visual order.\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * const selected = plugin.getSelectedRows(); // [{ id: 1, name: 'Alice' }, ...]\n * ```\n */\n getSelectedRows<T = unknown>(): T[] {\n const mode = this.#mode.primary;\n const rows = this.rows;\n\n if (mode === 'row') {\n return this.getSelectedRowIndices()\n .filter((i) => i >= 0 && i < rows.length)\n .map((i) => rows[i]) as T[];\n }\n\n if (mode === 'cell' && this.selectedCell) {\n const { row } = this.selectedCell;\n return row >= 0 && row < rows.length ? [rows[row] as T] : [];\n }\n\n if (mode === 'range' && this.ranges.length > 0) {\n // Collect unique row indices across all ranges\n const rowIndices = new Set<number>();\n for (const range of this.ranges) {\n const minRow = Math.max(0, Math.min(range.startRow, range.endRow));\n const maxRow = Math.min(rows.length - 1, Math.max(range.startRow, range.endRow));\n for (let r = minRow; r <= maxRow; r++) {\n rowIndices.add(r);\n }\n }\n return [...rowIndices].sort((a, b) => a - b).map((i) => rows[i]) as T[];\n }\n\n return [];\n }\n\n /**\n * Replace the entire column-axis selection with `fields` in a single\n * batched operation. Filters out unknown / utility / hidden fields against\n * the current visible columns, updates `selectedColumns` once, sets the\n * anchor/head to the last accepted field (or null when empty), emits\n * `selection-change` exactly once, and schedules a single render.\n *\n * Used by the `selectColumns` plugin query so external callers wiring\n * many fields at once don't pay N×emit + N×requestAfterRender.\n */\n #setColumnSelection(fields: readonly string[]): void {\n if (!this.#mode.columnEnabled) return;\n\n const visible = selectableColumnFields(this.visibleColumns);\n const allowed = new Set(visible);\n // Preserve caller order but de-duplicate and filter unknowns/utility cols.\n const accepted: string[] = [];\n const seen = new Set<string>();\n for (const f of fields) {\n if (!allowed.has(f) || seen.has(f)) continue;\n seen.add(f);\n accepted.push(f);\n }\n\n this.#enforceMutualExclusion('column');\n\n this.selectedColumns.clear();\n for (const f of accepted) this.selectedColumns.add(f);\n const last = accepted.length > 0 ? accepted[accepted.length - 1] : null;\n this.columnAnchor = last;\n this.columnHead = last;\n this.activeAxis = this.selectedColumns.size > 0 ? 'column' : 'none';\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Toggle, add, or replace a column in the column-axis selection.\n *\n * Column selection is identified by **field name**, so it survives column\n * pinning, reordering, and virtualization recycling. The column must be\n * present in the grid's visible columns and must not be a utility column.\n *\n * Only available when `mode` includes `'column'`. With `multiSelect: false`,\n * `range`/`toggle` options are ignored and the call always replaces the\n * current selection with the single column.\n *\n * @param field - The column field name to select.\n * @param options.range - When true and a `columnAnchor` exists, selects\n * every column from anchor to `field` inclusive (Ctrl+Shift+Click semantics).\n * @param options.toggle - When true, removes `field` if already selected;\n * otherwise adds it. Without `toggle`, plain calls replace the selection.\n * @since 2.8.0\n */\n selectColumn(field: string, options: { range?: boolean; toggle?: boolean } = {}): void {\n if (!this.#mode.columnEnabled) return;\n const fields = selectableColumnFields(this.visibleColumns);\n if (!fields.includes(field)) return;\n\n this.#enforceMutualExclusion('column');\n\n const multiSelect = this.config.multiSelect !== false;\n if (!multiSelect) {\n this.selectedColumns.clear();\n this.selectedColumns.add(field);\n this.columnAnchor = field;\n this.columnHead = field;\n } else if (options.range && this.columnAnchor) {\n const range = fieldsBetween(this.columnAnchor, field, fields);\n this.selectedColumns.clear();\n for (const f of range) this.selectedColumns.add(f);\n this.columnHead = field;\n } else if (options.toggle) {\n if (this.selectedColumns.has(field)) {\n this.selectedColumns.delete(field);\n } else {\n this.selectedColumns.add(field);\n }\n this.columnAnchor = field;\n this.columnHead = field;\n } else {\n this.selectedColumns.clear();\n this.selectedColumns.add(field);\n this.columnAnchor = field;\n this.columnHead = field;\n }\n\n this.activeAxis = this.selectedColumns.size > 0 ? 'column' : 'none';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Remove a column from the column-axis selection. No-op if `field` isn't selected.\n * @since 2.8.0\n */\n deselectColumn(field: string): void {\n if (!this.#mode.columnEnabled) return;\n if (!this.selectedColumns.delete(field)) return;\n if (this.selectedColumns.size === 0) {\n this.columnAnchor = null;\n this.columnHead = null;\n if (this.activeAxis === 'column') this.activeAxis = 'none';\n }\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Select every selectable column. No-op when `multiSelect: false` or column\n * mode isn't enabled.\n * @since 2.8.0\n */\n selectAllColumns(): void {\n if (!this.#mode.columnEnabled) return;\n if (this.config.multiSelect === false) return;\n const fields = selectableColumnFields(this.visibleColumns);\n if (fields.length === 0) return;\n this.#enforceMutualExclusion('column');\n this.selectedColumns.clear();\n for (const f of fields) this.selectedColumns.add(f);\n this.columnAnchor = fields[0];\n this.columnHead = fields[fields.length - 1];\n this.activeAxis = 'column';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Clear column-axis selection only. Leaves any row/cell/range selection intact.\n * @since 2.8.0\n */\n clearColumnSelection(): void {\n if (this.selectedColumns.size === 0) return;\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n if (this.activeAxis === 'column') this.activeAxis = 'none';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Get the field names of all currently selected columns, in visible-column\n * order. Returns an empty array when the column axis is inactive or empty.\n * @since 2.8.0\n */\n getSelectedColumns(): readonly string[] {\n if (this.selectedColumns.size === 0) return [];\n const fields = selectableColumnFields(this.visibleColumns);\n return fields.filter((f) => this.selectedColumns.has(f));\n }\n\n /**\n * Clear all selection (every axis).\n */\n clearSelection(): void {\n this.selectedCell = null;\n this.selected.clear();\n this.anchor = null;\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n this.activeAxis = 'none';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Set selected ranges programmatically.\n */\n setRanges(ranges: CellRange[]): void {\n this.ranges = ranges.map((r) => ({\n startRow: r.from.row,\n startCol: r.from.col,\n endRow: r.to.row,\n endCol: r.to.col,\n }));\n this.activeRange = this.ranges.length > 0 ? this.ranges[this.ranges.length - 1] : null;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Private Helpers\n\n #buildEvent(): SelectionChangeDetail {\n // Derive the axis from current state so all existing in-row code paths\n // (which mutate state then emit) report the correct axis without needing\n // to pre-set `this.activeAxis` themselves. Mutual exclusion is enforced\n // separately in {@link #enforceMutualExclusion}.\n const primary = this.#mode.primary;\n const inRowPopulated =\n (primary === 'row' && this.selected.size > 0) ||\n (primary === 'cell' && this.selectedCell !== null) ||\n (primary === 'range' && this.ranges.length > 0);\n\n // Mutual exclusion: in `bothAxes` mode, the user just mutated the in-row\n // axis (we know because `#buildEvent` is called from the in-row code paths\n // immediately after state changes) — so clear stale column selection. The\n // column-axis code paths call `#enforceMutualExclusion('column')`\n // explicitly BEFORE mutating; they then update `activeAxis` to 'column'\n // so the in-row state seen here is empty and this branch doesn't trigger.\n if (this.#mode.bothAxes && inRowPopulated && this.selectedColumns.size > 0) {\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n if (this.gridElement) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionAxisChanged', 'row'));\n }\n }\n\n let axis: SelectionAxis;\n if (inRowPopulated) {\n // In-row state always wins when populated — column-axis paths clear\n // in-row state via #enforceMutualExclusion before mutating columns.\n if (primary === 'row') axis = 'row';\n else if (primary === 'cell') axis = 'cell';\n else axis = 'range';\n } else if (this.selectedColumns.size > 0) {\n axis = 'column';\n } else {\n axis = 'none';\n }\n this.activeAxis = axis;\n\n const event = buildSelectionEvent(\n this.config.mode,\n axis,\n primary,\n {\n selectedCell: this.selectedCell,\n selected: this.selected,\n ranges: this.ranges,\n selectedColumns: this.selectedColumns,\n },\n this.columns.length,\n selectableColumnFields(this.visibleColumns),\n );\n // Debounced screen reader announcement for selection changes\n const span = this.#pendingSpanAnnouncement;\n this.#pendingSpanAnnouncement = null;\n if (this.announceTimer) clearTimeout(this.announceTimer);\n this.announceTimer = setTimeout(() => {\n if (span) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'rangeSpanSelected', span.axis, span.count));\n } else if (event.activeAxis === 'column') {\n const cols = event.selectedColumns;\n if (cols.length === 1) {\n const field = cols[0];\n const col = this.columns.find((c) => c.field === field);\n const label = (typeof col?.header === 'string' && col.header) || field;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'columnSelected', label));\n } else if (cols.length > 1) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'columnSelectionChanged', cols.length));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'columnSelectionCleared'));\n }\n } else {\n const count = event.activeAxis === 'row' ? this.selected.size : event.ranges.length;\n if (count > 0) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionChanged', count));\n }\n }\n }, 150);\n return event;\n }\n\n /**\n * Enforce row↔column mutual exclusion when both axes are configured\n * (`mode: ['row', 'column']` etc.). Call BEFORE mutating the destination\n * axis so its data won't be wiped along with the inactive axis.\n *\n * Single-string-mode configs are no-ops (the inactive axis can't have data\n * because there's no UI path to populate it). The in-row → column path is\n * called explicitly by the column-axis code; the column → in-row path is\n * handled automatically inside {@link #buildEvent} (which runs on every\n * in-row state change immediately before emitting).\n */\n #enforceMutualExclusion(toAxis: 'row' | 'cell' | 'range' | 'column'): void {\n if (!this.#mode.bothAxes) return;\n if (toAxis === 'column') {\n const hadInRow = this.selected.size > 0 || this.selectedCell !== null || this.ranges.length > 0;\n if (!hadInRow) return;\n this.selected.clear();\n this.lastSelected = null;\n this.anchor = null;\n this.selectedCell = null;\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n if (this.gridElement) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionAxisChanged', 'column'));\n }\n }\n // Column → in-row flip is handled inside #buildEvent (auto-detected by\n // observing populated in-row state with stale columns).\n }\n\n // #endregion\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/defaults.ts","../../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../../libs/grid/src/lib/core/internal/pointer-modality.ts","../../../../../../libs/grid/src/lib/core/internal/utils.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../../libs/grid/src/lib/plugins/shared/drag-alternative-menu.ts","../../../../../../libs/grid/src/lib/plugins/selection/column-selection.ts","../../../../../../libs/grid/src/lib/plugins/selection/range-selection.ts","../../../../../../libs/grid/src/lib/plugins/selection/selection.css?inline","../../../../../../libs/grid/src/lib/core/internal/pointer-drag.ts","../../../../../../libs/grid/src/lib/plugins/selection/touch-selection.ts","../../../../../../libs/grid/src/lib/plugins/selection/selection-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * Runtime default values for grid configuration.\n *\n * Kept out of `core/types.ts` so that file stays type-only apart from the two\n * enum-like consts whose `type` aliases are derived from them via `typeof`.\n * Import types from `./types`; import the values that back them from here.\n */\n\nimport type { A11yMessages, AnimationConfig, GridIcons } from './types';\n\n/**\n * Default English announcement messages.\n * Used when no custom messages are provided via {@link A11yConfig.messages}.\n * @since 2.0.0\n */\nexport const DEFAULT_A11Y_MESSAGES: Required<A11yMessages> = {\n sortApplied: (column, direction) => `Sorted by ${column}, ${direction}`,\n sortCleared: () => 'Sort cleared',\n filterApplied: (column) => `Filter applied on ${column}`,\n filterCleared: (column) => `Filter cleared from ${column}`,\n allFiltersCleared: () => 'All filters cleared',\n groupExpanded: (name, count) => `Group ${name} expanded, ${count} rows`,\n groupCollapsed: (name) => `Group ${name} collapsed`,\n selectionChanged: (count) => `${count} rows selected`,\n selectAllRows: () => 'Select all rows',\n selectRow: (rowIndex) => `Select row ${rowIndex + 1}`,\n columnSelected: (label) => `Column ${label} selected`,\n columnSelectionChanged: (count) => `${count} columns selected`,\n columnSelectionCleared: () => 'Column selection cleared',\n selectionAxisChanged: (toAxis) =>\n toAxis === 'column'\n ? 'Row selection cleared, column selection active'\n : 'Column selection cleared, row selection active',\n rangeSpanSelected: (axis, count) =>\n count === 1 ? `${axis === 'row' ? 'Row' : 'Column'} selected` : `${count} ${axis}s selected`,\n editingStarted: (rowIndex) => `Editing row ${rowIndex + 1}`,\n editingCommitted: (rowIndex) => `Row ${rowIndex + 1} saved`,\n dataLoaded: (count) => `${count} rows loaded`,\n};\n\n/**\n * Default animation configuration\n * @since 0.2.7\n */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n/**\n * Default icons used when not overridden. Most entries are short text/emoji;\n * `filter` and `filterActive` are empty strings because the actual rendering\n * is driven by the `--tbw-icon-filter[-active]-mask` CSS custom properties\n * (see `core/styles/variables.css`). Userland that wants an HTML/SVG fallback\n * via `gridConfig.icons.filter = '<svg…>'` is unaffected — that path overrides this default.\n * @since 0.1.1\n */\nexport const DEFAULT_GRID_ICONS: Required<GridIcons> = {\n expand: '▶',\n collapse: '▼',\n sortAsc: '▲',\n sortDesc: '▼',\n sortNone: '⇅',\n submenuArrow: '▶',\n dragHandle: '⋮⋮',\n toolPanel: '☰',\n filter: '',\n filterActive: '',\n print: '🖨️',\n};\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","/**\n * Pointer modality detection — distinguishes coarse (touch/stylus) from fine\n * (mouse/precision trackpad) input devices via the `pointer: coarse` media query.\n *\n * This module is **internal only** and is NOT exported from `src/public.ts`.\n * Plugins and features use it to adapt hit-target sizing, long-press thresholds,\n * and gesture disambiguation at runtime without querying the media each call.\n *\n * Design decisions:\n * - A single `MediaQueryList` instance is shared across all subscribers to avoid\n * N OS-level listeners. Subscribers share one `change` handler.\n * - Guards for missing `globalThis.matchMedia` keep the module SSR/happy-dom safe.\n * - Legacy `addListener`/`removeListener` fallback for older Safari and happy-dom\n * environments that predate `MediaQueryList.addEventListener`.\n *\n * @since 3.5.0\n * @internal\n */\n\n// #region Types\n\n/**\n * The resolved pointer category for the primary input device.\n *\n * - `'fine'` — mouse or precision trackpad (pointer: fine).\n * - `'coarse'` — touch screen or low-precision stylus (pointer: coarse).\n *\n * @since 3.5.0\n */\nexport type PointerModality = 'fine' | 'coarse';\n\n// #endregion\n\n// #region Singleton MQL + subscriber registry\n\n/** Lazily-created shared `MediaQueryList` for `(pointer: coarse)`. */\nlet _mql: MediaQueryList | null = null;\n\n/** Active change subscribers. */\nconst _subscribers = new Set<(m: PointerModality) => void>();\n\n/**\n * Resolve the current modality from a `MediaQueryList` state.\n * Returns `'coarse'` when the query matches, `'fine'` otherwise.\n */\nfunction _resolve(mql: MediaQueryList): PointerModality {\n return mql.matches ? 'coarse' : 'fine';\n}\n\n/**\n * Shared MQL change handler — dispatches to all active subscribers.\n */\nfunction _onMqlChange(e: MediaQueryListEvent): void {\n const modality: PointerModality = e.matches ? 'coarse' : 'fine';\n _subscribers.forEach((cb) => cb(modality));\n}\n\n/**\n * Return (and lazily create) the shared MQL.\n * Returns `null` in environments without `matchMedia` (SSR / happy-dom).\n */\nfunction _getMql(): MediaQueryList | null {\n if (_mql) return _mql;\n if (typeof globalThis.matchMedia !== 'function') return null;\n _mql = globalThis.matchMedia('(pointer: coarse)');\n // Attach the single shared change handler via the modern or legacy API.\n if (typeof _mql.addEventListener === 'function') {\n _mql.addEventListener('change', _onMqlChange);\n } else if (typeof (_mql as { addListener?: unknown }).addListener === 'function') {\n // Legacy Safari / older environments\n (_mql as { addListener: (cb: (e: MediaQueryListEvent) => void) => void }).addListener(_onMqlChange);\n }\n return _mql;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Return the current pointer modality for the primary input device.\n *\n * Falls back to `'fine'` in SSR environments or browsers that do not\n * support `matchMedia`.\n *\n * @since 3.5.0\n */\nexport function getPrimaryPointer(): PointerModality {\n const mql = _getMql();\n if (!mql) return 'fine';\n return _resolve(mql);\n}\n\n/**\n * Subscribe to pointer-modality changes.\n *\n * The callback fires whenever the browser detects a switch between fine and\n * coarse primary input (e.g. user connects a mouse to a touch-screen device).\n *\n * Returns an **idempotent** unsubscribe function — calling it multiple times\n * is safe.\n *\n * In SSR / environments without `matchMedia` the callback is never invoked\n * and the returned unsubscribe is a no-op.\n *\n * @since 3.5.0\n */\nexport function onPointerModalityChange(cb: (m: PointerModality) => void): () => void {\n _getMql(); // ensure MQL is created and the shared handler is attached\n _subscribers.add(cb);\n let removed = false;\n return (): void => {\n if (removed) return;\n removed = true;\n _subscribers.delete(cb);\n };\n}\n\n// #endregion\n","// #region Environment Helpers\n\n/**\n * Check if we're running in a development environment.\n * Returns true for localhost or when NODE_ENV !== 'production'.\n * Used to show warnings only in development.\n */\nexport function isDevelopment(): boolean {\n // Check for localhost (browser environment)\n if (typeof window !== 'undefined' && window.location) {\n const hostname = window.location.hostname;\n if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {\n return true;\n }\n }\n // Check for NODE_ENV (build-time or SSR)\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Cell Rendering Helpers\n\n/**\n * Generate accessible HTML for a boolean cell.\n * Uses role=\"checkbox\" with proper aria attributes.\n */\nexport function booleanCellHTML(value: boolean): string {\n return `<span role=\"checkbox\" aria-checked=\"${value}\" aria-label=\"${value}\">${value ? '🗹' : '☐'}</span>`;\n}\n\n/**\n * Format a date value for display.\n * Handles Date objects, timestamps, and date strings.\n * Returns empty string for invalid dates.\n */\nexport function formatDateValue(value: unknown): string {\n if (value == null || value === '') return '';\n if (value instanceof Date) {\n return isNaN(value.getTime()) ? '' : value.toLocaleDateString();\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return isNaN(d.getTime()) ? '' : d.toLocaleDateString();\n }\n return '';\n}\n\n/**\n * Get the row index from a cell element's data-row attribute.\n * Falls back to calculating from parent row's DOM position if data-row is missing.\n * Returns -1 if no valid row index is found.\n */\nexport function getRowIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-row');\n if (attr) return parseInt(attr, 10);\n\n // Fallback: find the parent .data-grid-row and calculate index from siblings\n const rowEl = cell.closest('.data-grid-row');\n if (!rowEl) return -1;\n\n const parent = rowEl.parentElement;\n if (!parent) return -1;\n\n // Get all data-grid-row siblings and find this row's index\n const rows = parent.querySelectorAll(':scope > .data-grid-row');\n for (let i = 0; i < rows.length; i++) {\n if (rows[i] === rowEl) return i;\n }\n return -1;\n}\n\n/**\n * Get the column index from a cell element's data-col attribute.\n * Returns -1 if no valid column index is found.\n */\nexport function getColIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-col');\n return attr ? parseInt(attr, 10) : -1;\n}\n\n/**\n * Clear all cell-focus styling from a root element.\n * Used when changing focus or when selection plugin takes over focus management.\n */\nexport function clearCellFocus(root: Element | null): void {\n if (!root) return;\n root.querySelectorAll('.cell-focus').forEach((el) => el.classList.remove('cell-focus'));\n}\n// #endregion\n\n// #region RTL Helpers\n\n/** Text direction */\nexport type TextDirection = 'ltr' | 'rtl';\n\n/**\n * Get the text direction for an element.\n * Reads from the computed style, which respects the `dir` attribute on the element\n * or any ancestor, as well as CSS `direction` property.\n *\n * @param element - The element to check direction for\n * @returns 'ltr' or 'rtl'\n *\n * @example\n * ```typescript\n * // Detect grid's direction\n * const dir = getDirection(gridElement);\n * if (dir === 'rtl') {\n * // Handle RTL layout\n * }\n * ```\n */\nexport function getDirection(element: Element): TextDirection {\n // Try computed style first (works in real browsers)\n try {\n const computedDir = getComputedStyle(element).direction;\n if (computedDir === 'rtl') return 'rtl';\n } catch {\n // getComputedStyle may fail in some test environments\n }\n\n // Fallback: check dir attribute on element or ancestors\n // This handles test environments where getComputedStyle may not reflect dir attribute\n try {\n const dirAttr = element.closest?.('[dir]')?.getAttribute('dir');\n if (dirAttr === 'rtl') return 'rtl';\n } catch {\n // closest may not be available on mock elements\n }\n\n return 'ltr';\n}\n\n/**\n * Check if an element is in RTL mode.\n *\n * @param element - The element to check\n * @returns true if the element's text direction is right-to-left\n */\nexport function isRTL(element: Element): boolean {\n return getDirection(element) === 'rtl';\n}\n\n/**\n * Resolve a logical inline position to a physical position based on text direction.\n *\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n * - `'left'` / `'right'` → unchanged (physical values)\n *\n * @param position - Logical or physical position\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical position ('left' or 'right')\n *\n * @example\n * ```typescript\n * resolveInlinePosition('start', 'ltr'); // 'left'\n * resolveInlinePosition('start', 'rtl'); // 'right'\n * resolveInlinePosition('left', 'rtl'); // 'left' (unchanged)\n * ```\n */\nexport function resolveInlinePosition(\n position: 'left' | 'right' | 'start' | 'end',\n direction: TextDirection,\n): 'left' | 'right' {\n if (position === 'left' || position === 'right') {\n return position;\n }\n if (direction === 'rtl') {\n return position === 'start' ? 'right' : 'left';\n }\n return position === 'start' ? 'left' : 'right';\n}\n// #endregion\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__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'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n '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(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Base Grid Plugin Class\n *\n * All plugins extend this abstract class.\n * Plugins are instantiated per-grid and manage their own state.\n */\n\n// Injected by Vite at build time from package.json (same as grid.ts)\ndeclare const __GRID_VERSION__: string;\n\nimport type { HeaderContentDefinition, ToolPanelDefinition } from '../../plugins/shell/types';\nimport { DEFAULT_GRID_ICONS } from '../defaults';\nimport {\n type DiagnosticCode,\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n formatDiagnostic,\n gridPrefix,\n} from '../internal/diagnostics';\nimport { setSanitizedHTML } from '../internal/sanitize';\nimport type {\n ColumnConfig,\n ColumnState,\n GridConfig,\n GridIcons,\n GridPlugin,\n IconValue,\n PluginNameMap,\n Translate,\n} from '../types';\n\n// Re-export shared plugin types for convenience\nexport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellCoords,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n ContextMenuItem,\n ContextMenuParams,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n KeyboardModifiers,\n PluginCellRenderContext,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\n/**\n * Grid element interface for plugins.\n * Extends GridElementRef with plugin-specific methods.\n * Note: effectiveConfig is already available from GridElementRef.\n */\nexport interface GridElement extends GridElementRef {\n getPlugin<T extends GridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : BaseGridPlugin) | undefined;\n /**\n * Get a plugin's state by plugin name.\n * This is a loose-coupling method for plugins to access other plugins' state\n * without importing the plugin class directly.\n * @internal Plugin API\n */\n getPluginState?(name: string): unknown;\n\n // Focus Management\n /** Register an external DOM element as a logical focus container. @internal Plugin API */\n registerExternalFocusContainer?(el: Element): void;\n /** Unregister a previously registered external focus container. @internal Plugin API */\n unregisterExternalFocusContainer?(el: Element): void;\n /** Check whether focus is logically inside this grid (own DOM + external containers). @internal Plugin API */\n containsFocus?(node?: Node | null): boolean;\n}\n\n/**\n * Header render context for plugin header renderers.\n */\nexport interface PluginHeaderRenderContext {\n /** Column configuration */\n column: ColumnConfig;\n /** Column index */\n colIndex: number;\n}\n\n// ============================================================================\n// Plugin Dependency Types\n// ============================================================================\n\n/**\n * Declares a dependency on another plugin.\n *\n * @category Plugin Development\n * @example\n * ```typescript\n * export class UndoRedoPlugin extends BaseGridPlugin {\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true },\n * ];\n * }\n * ```\n * @since 0.4.1\n */\nexport interface PluginDependency {\n /**\n * The name of the required plugin (matches the plugin's `name` property).\n * Use string names for loose coupling - avoids static imports.\n */\n name: string;\n\n /**\n * Whether this dependency is required (hard) or optional (soft).\n * - `true`: Plugin cannot function without it. Throws error if missing.\n * - `false`: Plugin works with reduced functionality. Silent if missing\n * (set {@link severity} to `'warn'` or `'info'` to opt into a message).\n * @default true\n */\n required?: boolean;\n\n /**\n * Explicit severity when this dependency is missing. Overrides the default\n * derived from {@link required}.\n * - `'error'`: throw (halts grid setup). Default when `required` is not `false`.\n * - `'warn'`: log a console warning and continue (development only).\n * - `'info'`: log a verbose console.debug message and continue (development only).\n *\n * When omitted, a missing hard dependency throws and a missing soft\n * dependency is silent — preserving backward-compatible behavior.\n * @since 2.16.0\n */\n severity?: 'error' | 'warn' | 'info';\n\n /**\n * Optional predicate that gates whether this dependency applies, evaluated\n * against the depending plugin's own resolved configuration (defaults merged\n * with user config). Return `false` to skip the dependency entirely.\n *\n * Lets a plugin require/recommend another plugin only under certain config\n * — e.g. a plugin that needs a tool-panel host only when its tool panel is\n * enabled. When omitted, the dependency always applies.\n * @example when: (cfg: PivotConfig) => cfg.showToolPanel === true\n * @since 2.16.0\n */\n when?: (pluginConfig: unknown) => boolean;\n\n /**\n * Factory for a default instance of the dependency. When the dependency is\n * missing, the grid attaches the instance this returns (once per grid,\n * before the depending plugin) instead of reporting an error, so consumers\n * never have to add it themselves. An explicitly configured instance always\n * wins.\n *\n * Import the dependency's plugin module statically so it ships with the\n * depending plugin's module graph — never with `import()`.\n *\n * @example provide: () => new HierarchyPlugin()\n * @since 3.9.0\n */\n provide?: () => BaseGridPlugin;\n\n /**\n * Human-readable reason for this dependency.\n * Shown in error/info messages to help users understand why.\n * @example \"UndoRedoPlugin needs EditingPlugin to track cell edits\"\n */\n reason?: string;\n}\n\n/**\n * Declares an incompatibility between plugins.\n * When both plugins are loaded, a warning is logged to help users understand the conflict.\n *\n * @category Plugin Development\n */\nexport interface PluginIncompatibility {\n /**\n * The name of the incompatible plugin (matches the plugin's `name` property).\n */\n name: string;\n\n /**\n * Human-readable reason for the incompatibility.\n * Should explain why the plugins conflict and any workarounds.\n * @example \"Both transform the entire row model in different ways\"\n */\n reason: string;\n}\n\n// ============================================================================\n// Plugin Query & Event Definitions\n// ============================================================================\n\n/**\n * Defines a query that a plugin can handle.\n * Other plugins or the grid can send this query type to retrieve data.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * queries: [\n * {\n * type: 'canMoveColumn',\n * description: 'Check if a column can be moved/reordered',\n * },\n * ]\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * return this.canMoveColumn(query.context);\n * }\n * }\n * ```\n * @since 1.8.0\n */\nexport interface QueryDefinition {\n /**\n * The query type identifier (e.g., 'canMoveColumn', 'getContextMenuItems').\n * Should be unique across all plugins.\n */\n type: string;\n\n /**\n * Human-readable description of what the query does.\n */\n description?: string;\n}\n\n/**\n * Defines an event that a plugin can emit.\n * Other plugins can subscribe to these events via the Event Bus.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * events: [\n * {\n * type: 'filter-change',\n * description: 'Emitted when filter criteria change',\n * },\n * ]\n *\n * // In plugin class - emit\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // In another plugin - subscribe\n * this.on('filter-change', (detail) => console.log('Filter changed:', detail));\n * ```\n * @since 1.8.0\n */\nexport interface EventDefinition {\n /**\n * The event type identifier (e.g., 'filter-change', 'selection-change').\n * Used when emitting and subscribing to events.\n */\n type: string;\n\n /**\n * Human-readable description of what the event represents.\n */\n description?: string;\n\n /**\n * Whether this event is cancelable via `event.preventDefault()`.\n * @default false\n */\n cancelable?: boolean;\n}\n\n// ============================================================================\n// Plugin Manifest Types\n// ============================================================================\n\n/**\n * Defines a property that a plugin \"owns\" - used for configuration validation.\n * When this property is used without the owning plugin loaded, an error is thrown.\n *\n * @category Plugin Development\n */\nexport interface PluginPropertyDefinition {\n /** The property name on column or grid config */\n property: string;\n /** Whether this is a column-level or config-level property */\n level: 'column' | 'config';\n /** Human-readable description for error messages (e.g., 'the \"editable\" column property') */\n description: string;\n /** Import path hint for error messages (e.g., \"import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\") */\n importHint?: string;\n /** Custom check for whether property is considered \"used\" (default: truthy value check) */\n isUsed?: (value: unknown) => boolean;\n}\n\n/**\n * A configuration validation rule for detecting invalid/conflicting settings.\n * Plugins declare rules in their manifest; the validator executes them during grid initialization.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n */\nexport interface PluginConfigRule<TConfig = unknown> {\n /** Rule identifier for debugging (e.g., 'selection/range-dblclick') */\n id: string;\n /** Severity: 'error' throws, 'warn' logs warning */\n severity: 'error' | 'warn';\n /** Human-readable message shown when rule is violated */\n message: string;\n /** Predicate returning true if the rule is VIOLATED (i.e., config is invalid) */\n check: (pluginConfig: TConfig) => boolean;\n}\n\n/**\n * Hook names that can have execution priority configured.\n *\n * @category Plugin Development\n */\nexport type HookName =\n | 'processColumns'\n | 'processConfig'\n | 'processRows'\n | 'afterStructuralRender'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onHeaderClick'\n | 'onKeyDown'\n | 'onRowClick'\n | 'onScroll'\n | 'onScrollRender';\n\n/**\n * Static metadata about a plugin's capabilities and requirements.\n * Declared as a static property on plugin classes.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n *\n * @example\n * ```typescript\n * export class MyPlugin extends BaseGridPlugin<MyConfig> {\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'my-plugin/invalid-combo', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * readonly name = 'myPlugin';\n * }\n * ```\n * @since 1.1.0\n */\nexport interface PluginManifest<TConfig = unknown> {\n /**\n * Properties this plugin owns - validated by validate-config.ts.\n * If a user uses one of these properties without loading the plugin, an error is thrown.\n */\n ownedProperties?: PluginPropertyDefinition[];\n\n /**\n * Hook execution priority (higher = later, default 0).\n * Use negative values to run earlier, positive to run later.\n */\n hookPriority?: Partial<Record<HookName, number>>;\n\n /**\n * Configuration validation rules - checked during grid initialization.\n * Rules with severity 'error' throw, 'warn' logs to console.\n */\n configRules?: PluginConfigRule<TConfig>[];\n\n /**\n * Plugins that are incompatible with this plugin.\n * When both plugins are loaded together, a warning is shown.\n *\n * @example\n * ```typescript\n * incompatibleWith: [\n * { name: 'tree', reason: 'Both transform the entire row model in different ways' },\n * ],\n * ```\n */\n incompatibleWith?: PluginIncompatibility[];\n\n /**\n * Queries this plugin can handle.\n * Declares what query types this plugin responds to via `handleQuery()`.\n * This replaces the centralized PLUGIN_QUERIES approach with manifest-declared queries.\n *\n * @example\n * ```typescript\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * ```\n */\n queries?: QueryDefinition[];\n\n /**\n * Events this plugin can emit.\n * Declares what event types other plugins can subscribe to via `on()`.\n *\n * @example\n * ```typescript\n * events: [\n * { type: 'filter-change', description: 'Emitted when filter criteria change' },\n * ],\n * ```\n */\n events?: EventDefinition[];\n\n /**\n * Whether this plugin's `processRows` hook injects or removes rows\n * (group headers, tree nodes, pivot aggregates, placeholders, etc.).\n *\n * When true, the core sorting module delegates to the render scheduler\n * instead of sorting `_rows` in-place, because `_rows` may contain\n * plugin-generated marker objects that would be corrupted by a direct sort.\n *\n * Plugins that only **filter** or **reorder** existing rows (e.g. Filtering,\n * MultiSort) should leave this unset or set it to `false`.\n */\n modifiesRowStructure?: boolean;\n}\n\n/** Convert camelCase icon key to kebab-case data-icon attribute value. */\nexport function toIconAttr(key: string): string {\n return key.replace(/([A-Z])/g, '-$1').toLowerCase();\n}\n\n/**\n * Abstract base class for all grid plugins.\n *\n * @category Plugin Development\n * @template TConfig - Configuration type for the plugin\n * @since 0.1.1\n */\nexport abstract class BaseGridPlugin<TConfig = unknown> implements GridPlugin {\n /**\n * Plugin dependencies - declare other plugins this one requires.\n *\n * Dependencies are validated when the plugin is attached.\n * Required dependencies throw an error if missing. Optional dependencies are\n * silent by default; set a {@link PluginDependency.severity} of `'warn'` or\n * `'info'` to opt into a development-only log.\n *\n * @example\n * ```typescript\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },\n * { name: 'selection', required: false, reason: 'Enables selection-based undo' },\n * ];\n * ```\n */\n static readonly dependencies?: PluginDependency[];\n\n /**\n * Plugin manifest - declares owned properties, config rules, and hook priorities.\n *\n * This is read by the configuration validator to:\n * - Validate that required plugins are loaded when their properties are used\n * - Execute configRules to detect invalid/conflicting settings\n * - Order hook execution based on priority\n *\n * @example\n * ```typescript\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static readonly manifest?: PluginManifest<any>;\n\n /** Unique plugin identifier (derived from class name by default) */\n abstract readonly name: string;\n\n /**\n * Alternative names for backward compatibility.\n * `getPluginByName()` matches against both `name` and `aliases`.\n * @internal\n */\n readonly aliases?: readonly string[];\n\n /**\n * Plugin version - defaults to grid version for built-in plugins.\n * Third-party plugins can override with their own semver.\n */\n readonly version: string = typeof __GRID_VERSION__ !== 'undefined' ? __GRID_VERSION__ : 'dev';\n\n /** CSS styles to inject via `document.adoptedStyleSheets` */\n readonly styles?: string;\n\n /** Custom cell renderers keyed by type name */\n readonly cellRenderers?: Record<string, CellRenderer>;\n\n /** Custom header renderers keyed by type name */\n readonly headerRenderers?: Record<string, HeaderRenderer>;\n\n /** Custom cell editors keyed by type name */\n readonly cellEditors?: Record<string, CellEditor>;\n\n /** The grid instance this plugin is attached to */\n protected grid!: GridElement;\n\n /** Plugin configuration - merged with defaults in attach() */\n protected config!: TConfig;\n\n /** User-provided configuration from constructor */\n protected readonly userConfig: Partial<TConfig>;\n\n /**\n * Plugin-level AbortController for event listener cleanup.\n * Created fresh in attach(), aborted in detach().\n * This ensures event listeners are properly cleaned up when plugins are re-attached.\n */\n #abortController?: AbortController;\n\n /**\n * Default configuration - subclasses should override this getter.\n * Note: This must be a getter (not property initializer) for proper inheritance\n * since property initializers run after parent constructor.\n */\n protected get defaultConfig(): Partial<TConfig> {\n return {};\n }\n\n constructor(config: Partial<TConfig> = {}) {\n this.userConfig = config;\n }\n\n /**\n * Effective configuration (defaults merged with user config), resolved\n * eagerly so it is available **before** {@link attach} runs. Dependency\n * validation happens before `attach()` merges {@link config}, so config\n * -conditional dependency `when` predicates evaluate against this getter.\n *\n * While attached, returns the already-merged {@link config}; otherwise\n * recomputes `defaultConfig` + `userConfig` fresh. `config` is intentionally\n * **not** trusted when detached because {@link detach} does not clear it — a\n * re-validated plugin would otherwise see stale config from a prior\n * attachment. The {@link attach}-managed abort controller is the\n * attached/detached signal.\n * @internal Plugin infrastructure (used by `validatePluginDependencies`).\n */\n get resolvedConfig(): TConfig {\n return (this.#abortController ? this.config : { ...this.defaultConfig, ...this.userConfig }) as TConfig;\n }\n\n /**\n * Merge user-supplied configuration from other plugin instances into this\n * plugin's `userConfig`. Used by `PluginManager.attachAll()`'s alias-collapse\n * pre-pass: when a consumer instantiates the same plugin more than once (or\n * under an alias name), only one instance is attached, and the other\n * instances' configs are folded in via this method.\n *\n * Merge rules (shallow):\n *\n * - Same key, **same value** → silent.\n * - Same key, **different values** (incl. function vs function) → throws\n * a diagnostic naming the conflicting key. Conflicting callbacks\n * (`canDrag`, `canDrop`, etc.) cannot be reconciled automatically.\n * - **Disjoint keys** → merged cleanly.\n *\n * Subclasses may override to implement custom deep-merge logic, but should\n * call `super.mergeConfigsFrom(others)` to inherit the conflict-detection.\n *\n * @internal Plugin infrastructure (used by `PluginManager.attachAll`).\n * @throws Error with diagnostic code `TBW025` when configs conflict.\n */\n mergeConfigsFrom(others: readonly BaseGridPlugin<TConfig>[]): void {\n if (others.length === 0) return;\n const merged = { ...this.userConfig } as Record<string, unknown>;\n const sources: Record<string, BaseGridPlugin<TConfig>> = {};\n for (const key of Object.keys(merged)) sources[key] = this;\n for (const other of others) {\n const otherConfig = other.userConfig as Record<string, unknown>;\n for (const [key, value] of Object.entries(otherConfig)) {\n if (value === undefined) continue;\n if (!(key in merged)) {\n merged[key] = value;\n sources[key] = other;\n continue;\n }\n if (merged[key] === value) continue;\n // Conflict\n const ownerA = sources[key]?.constructor.name ?? this.constructor.name;\n const ownerB = other.constructor.name;\n const code = formatDiagnostic(\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n `Cannot merge plugin configs for \"${this.name}\": conflicting value for \"${key}\" supplied by both ${ownerA} and ${ownerB}. Pass the option on a single instance, or remove the duplicate.`,\n undefined,\n this.name,\n );\n throw new Error(code);\n }\n }\n // Mutate userConfig in place so re-attach picks it up via attach().\n // `userConfig` is declared `readonly` to prevent reassigning the binding,\n // but the object's own properties are intentionally mutable for this\n // alias-collapse pre-pass. A single cast to a record is enough — no\n // `as unknown as` double-cast needed.\n Object.assign(this.userConfig as Record<string, unknown>, merged);\n }\n\n /**\n * Replace this plugin's `userConfig` with a freshly-resolved sibling\n * instance's config. Used by the grid's per-grid feature-instance gate\n * (`#initializePlugins`): when a feature is re-resolved across a config\n * change, the **existing** instance is reused — so its accumulated state\n * survives — but it must adopt the latest `gridConfig.features.<name>`\n * value. Unlike {@link mergeConfigsFrom}, this is last-write-wins with no\n * conflict detection: the re-resolved instance is authoritative.\n *\n * `attach()` recomputes {@link config} from `userConfig`, so updating\n * `userConfig` here is enough for the reused instance to pick up the new\n * config on its next attach during the same `#initializePlugins` pass.\n *\n * @internal Plugin infrastructure (used by the grid feature gate).\n */\n refreshUserConfigFrom(other: BaseGridPlugin<TConfig>): void {\n const target = this.userConfig as Record<string, unknown>;\n // Snapshot the source BEFORE clearing `target`. A feature factory may\n // store the consumer's config object by reference (e.g. `new\n // ContextMenuPlugin(config)`), and a consumer that passes the *same* config\n // object across re-resolutions (common when `gridConfig` is a recomputed\n // Angular `computed()`) makes `this.userConfig === other.userConfig`.\n // Without this snapshot, the `delete` loop below would empty that shared\n // object first, then `Object.assign` would copy back nothing — silently\n // wiping the config and falling back to plugin defaults. (#contextMenu)\n const source = { ...(other.userConfig as Record<string, unknown>) };\n for (const key of Object.keys(target)) delete target[key];\n Object.assign(target, source);\n }\n\n /**\n * Called when the plugin is attached to a grid.\n * Override to set up event listeners, initialize state, etc.\n *\n * @example\n * ```ts\n * attach(grid: GridElement): void {\n * super.attach(grid);\n * // Set up document-level listeners with auto-cleanup\n * document.addEventListener('keydown', this.handleEscape, {\n * signal: this.disconnectSignal\n * });\n * }\n * ```\n */\n attach(grid: GridElement): void {\n // Abort any previous abort controller (in case of re-attach without detach)\n this.#abortController?.abort();\n // Create fresh abort controller for this attachment\n this.#abortController = new AbortController();\n\n this.grid = grid;\n // Merge config here (after subclass construction is complete)\n this.config = { ...this.defaultConfig, ...this.userConfig } as TConfig;\n }\n\n /**\n * Called when the plugin is detached from a grid.\n * Override to clean up event listeners, timers, etc.\n *\n * @example\n * ```ts\n * detach(): void {\n * // Clean up any state not handled by disconnectSignal\n * this.selectedRows.clear();\n * this.cache = null;\n * }\n * ```\n */\n detach(): void {\n // Abort the plugin's abort controller to clean up all event listeners\n // registered with { signal: this.disconnectSignal }\n this.#abortController?.abort();\n this.#abortController = undefined;\n // Override in subclass for additional cleanup\n }\n\n /**\n * Called when another plugin is attached to the same grid.\n * Use for inter-plugin coordination, e.g., to integrate with new plugins.\n *\n * @param name - The name of the plugin that was attached\n * @param plugin - The plugin instance (for direct access if needed)\n *\n * @example\n * ```ts\n * onPluginAttached(name: string, plugin: BaseGridPlugin): void {\n * if (name === 'selection') {\n * // Integrate with selection plugin\n * this.selectionPlugin = plugin as SelectionPlugin;\n * }\n * }\n * ```\n */\n onPluginAttached?(name: string, plugin: BaseGridPlugin): void;\n\n /**\n * Called when another plugin is detached from the same grid.\n * Use to clean up inter-plugin references.\n *\n * @param name - The name of the plugin that was detached\n *\n * @example\n * ```ts\n * onPluginDetached(name: string): void {\n * if (name === 'selection') {\n * this.selectionPlugin = undefined;\n * }\n * }\n * ```\n */\n onPluginDetached?(name: string): void;\n\n /**\n * Get another plugin instance from the same grid.\n * Use for inter-plugin communication.\n *\n * **Prefer {@link BaseGridPlugin.grid grid.getPluginByName()}** when you don't need the class import.\n *\n * @example\n * ```ts\n * // Preferred: by name\n * const selection = this.grid?.getPluginByName('selection');\n *\n * // Alternative: by class\n * const selection = this.getPlugin(SelectionPlugin);\n * if (selection) {\n * const selectedRows = selection.getSelectedRows();\n * }\n * ```\n */\n protected getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined {\n return this.grid?.getPlugin(PluginClass);\n }\n\n /**\n * Emit a custom event from the grid.\n */\n protected emit<T>(eventName: string, detail: T): void {\n this.grid?.dispatchEvent?.(new CustomEvent(eventName, { detail, bubbles: true }));\n }\n\n /**\n * Emit a cancelable custom event from the grid.\n * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise\n */\n protected emitCancelable<T>(eventName: string, detail: T): boolean {\n const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true });\n this.grid?.dispatchEvent?.(event);\n return event.defaultPrevented;\n }\n\n // =========================================================================\n // Event Bus - Plugin-to-Plugin Communication\n // =========================================================================\n\n /**\n * Subscribe to an event from another plugin.\n * The subscription is automatically cleaned up when this plugin is detached.\n *\n * @category Plugin Development\n * @param eventType - The event type to listen for (e.g., 'filter-change')\n * @param callback - The callback to invoke when the event is emitted\n *\n * @example\n * ```typescript\n * // In attach() or other initialization\n * this.on('filter-change', (detail) => {\n * console.log('Filter changed:', detail);\n * });\n * ```\n */\n protected on<T = unknown>(eventType: string, callback: (detail: T) => void): void {\n this.grid?._pluginManager?.subscribe(this, eventType, callback as (detail: unknown) => void);\n }\n\n /**\n * Unsubscribe from a plugin event.\n *\n * @category Plugin Development\n * @param eventType - The event type to stop listening for\n *\n * @example\n * ```typescript\n * this.off('filter-change');\n * ```\n */\n protected off(eventType: string): void {\n this.grid?._pluginManager?.unsubscribe(this, eventType);\n }\n\n /**\n * Emit an event to other plugins via the Event Bus.\n * This is for inter-plugin communication only; it does NOT dispatch DOM events.\n * Use `emit()` to dispatch DOM events that external consumers can listen to.\n *\n * @category Plugin Development\n * @param eventType - The event type to emit (should be declared in manifest.events)\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Emit to other plugins (not DOM)\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // For DOM events that consumers can addEventListener to:\n * this.emit('filter-change', { field: 'name', value: 'Alice' });\n * ```\n */\n protected emitPluginEvent<T>(eventType: string, detail: T): void {\n this.grid?._pluginManager?.emitPluginEvent(eventType, detail);\n }\n\n /**\n * Emit an event to **both** the plugin Event Bus (for inter-plugin communication)\n * **and** the DOM (for external consumers via `addEventListener`).\n *\n * Use this when a state change is relevant to both other plugins and external\n * consumers. For example, `sort-change` needs to invalidate Selection (plugin bus)\n * and notify the host application (DOM event).\n *\n * @category Plugin Development\n * @param eventType - The event type to broadcast\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Notify both plugins and consumers of a sort change\n * this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n * ```\n */\n protected broadcast<T>(eventType: string, detail: T): void {\n this.emitPluginEvent(eventType, detail);\n this.emit(eventType, detail);\n }\n\n /**\n * Run a yes/no plugin query and reduce the responses to a single boolean.\n *\n * `grid.query()` always returns an **array** of responses (empty when no\n * plugin handles the query), and `[]` is truthy in JavaScript — so testing\n * the raw result directly (`if (grid.query(...))`) is always `true` and\n * silently takes the \"some plugin said yes\" branch even when nothing\n * answered. Always route boolean queries through this helper.\n *\n * @category Plugin Development\n * @param type - The query type to dispatch\n * @param context - Optional query context passed to the handlers\n * @returns `true` only when at least one plugin answered truthily\n *\n * @example\n * ```typescript\n * if (this.queryBoolean('datasource:is-active')) {\n * // A ServerSide data source really is installed\n * }\n * ```\n * @since 3.4.0\n */\n protected queryBoolean(type: string, context: unknown = null): boolean {\n const responses = this.grid?.query?.(type, context);\n // Tolerate hosts/tests that hand back a bare value instead of the array.\n return Array.isArray(responses) ? responses.some(Boolean) : !!responses;\n }\n\n /**\n * Request a re-render of the grid.\n * Uses ROWS phase - does NOT trigger processColumns hooks.\n */\n protected requestRender(): void {\n this.grid?.requestRender?.();\n }\n\n /**\n * Request a columns re-render of the grid.\n * Uses COLUMNS phase - triggers processColumns hooks.\n * Use this when your plugin needs to reprocess column configuration.\n */\n protected requestColumnsRender(): void {\n (this.grid as { requestColumnsRender?: () => void })?.requestColumnsRender?.();\n }\n\n /**\n * Request a re-render and restore focus styling afterward.\n * Use this when a plugin action (like expand/collapse) triggers a render\n * but needs to maintain keyboard navigation focus.\n */\n protected requestRenderWithFocus(): void {\n this.grid?.requestRenderWithFocus?.();\n }\n\n /**\n * Request a lightweight style update without rebuilding DOM.\n * Use this instead of requestRender() when only CSS classes need updating.\n */\n protected requestAfterRender(): void {\n this.grid?.requestAfterRender?.();\n }\n\n /**\n * Re-render visible rows without rebuilding the row model or recalculating geometry.\n * Use this when row data has been updated in-place (e.g., server-side block loads)\n * and only the visible viewport needs to re-render.\n */\n protected requestVirtualRefresh(): void {\n this.grid?.requestVirtualRefresh?.();\n }\n\n /**\n * Get the current rows from the grid.\n */\n protected get rows(): any[] {\n return this.grid?.rows ?? [];\n }\n\n /**\n * Get the original unfiltered/unprocessed rows from the grid.\n * Use this when you need all source data regardless of active filters.\n */\n protected get sourceRows(): any[] {\n return this.grid?.sourceRows ?? [];\n }\n\n /**\n * Get the current columns from the grid.\n */\n protected get columns(): ColumnConfig[] {\n return this.grid?.columns ?? [];\n }\n\n /**\n * Get only visible columns from the grid (excludes hidden).\n * Use this for rendering that needs to match the grid template.\n */\n protected get visibleColumns(): ColumnConfig[] {\n return this.grid?._visibleColumns ?? [];\n }\n\n /**\n * Get the grid as an HTMLElement for direct DOM operations.\n * Use sparingly - prefer the typed GridElementRef API when possible.\n *\n * @example\n * ```ts\n * const width = this.gridElement.clientWidth;\n * this.gridElement.classList.add('my-plugin-active');\n * ```\n */\n protected get gridElement(): HTMLElement {\n return this.grid?._hostElement;\n }\n\n /**\n * Get the disconnect signal for event listener cleanup.\n * This signal is aborted when the grid disconnects from the DOM.\n * Use this when adding event listeners that should be cleaned up automatically.\n *\n * Best for:\n * - Document/window-level listeners added in attach()\n * - Listeners on the grid element itself\n * - Any listener that should persist across renders\n *\n * Not needed for:\n * - Listeners on elements created in afterRender() (removed with element)\n *\n * @example\n * element.addEventListener('click', handler, { signal: this.disconnectSignal });\n * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });\n */\n protected get disconnectSignal(): AbortSignal {\n // Return the plugin's own abort signal for proper cleanup on detach/re-attach\n // Falls back to grid's signal if plugin's controller not yet created\n return this.#abortController?.signal ?? this.grid?.disconnectSignal;\n }\n\n /**\n * Get the grid-level icons configuration.\n * Returns merged icons (user config + defaults).\n */\n protected get gridIcons(): Required<GridIcons> {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n /**\n * Resolve a user-visible string from {@link GridConfig.locale}, falling back to\n * the English default supplied at the call site.\n *\n * The fallback is passed inline rather than read from a shipped default map so\n * that a plugin's strings live in the plugin's own chunk.\n *\n * @param key - Namespaced message id, e.g. `'filter.apply'`.\n * @param fallback - English text used when the key is not overridden.\n * @since 3.5.0\n *\n * @example\n * ```ts\n * applyBtn.textContent = this.t('filter.apply', 'Apply');\n * ```\n */\n protected t(key: string, fallback: string): string {\n return this.grid?.effectiveConfig?.locale?.[key] ?? fallback;\n }\n\n /**\n * A bound `t()` suitable for handing to standalone renderer\n * helpers and public renderer callbacks.\n * @since 3.5.0\n */\n protected get translate(): Translate {\n return (key, fallback) => this.t(key, fallback);\n }\n\n // #region Animation Helpers\n\n /**\n * Check if animations are enabled at the grid level.\n * Respects gridConfig.animation.mode and the CSS variable set by the grid.\n *\n * Plugins should use this to skip animations when:\n * - Animation mode is 'off' or `false`\n * - User prefers reduced motion and mode is 'reduced-motion' (default)\n *\n * @example\n * ```ts\n * private get animationStyle(): 'slide' | 'fade' | false {\n * if (!this.isAnimationEnabled) return false;\n * return this.config.animation ?? 'slide';\n * }\n * ```\n */\n protected get isAnimationEnabled(): boolean {\n const mode = this.grid?.effectiveConfig?.animation?.mode ?? 'reduced-motion';\n\n // Explicit off = disabled\n if (mode === false || mode === 'off') return false;\n\n // Explicit on = always enabled\n if (mode === true || mode === 'on') return true;\n\n // reduced-motion: check CSS variable (set by grid based on media query)\n const host = this.gridElement;\n if (host) {\n const enabled = getComputedStyle(host).getPropertyValue('--tbw-animation-enabled').trim();\n return enabled !== '0';\n }\n\n return true; // Default to enabled\n }\n\n /**\n * Get the animation duration in milliseconds from CSS variable.\n * Falls back to 200ms if not set.\n *\n * Plugins can use this for their animation timing to stay consistent\n * with the grid-level animation.duration setting.\n *\n * @example\n * ```ts\n * element.animate(keyframes, { duration: this.animationDuration });\n * ```\n */\n protected get animationDuration(): number {\n const host = this.gridElement;\n if (host) {\n const durationStr = getComputedStyle(host).getPropertyValue('--tbw-animation-duration').trim();\n const parsed = parseInt(durationStr, 10);\n if (!isNaN(parsed)) return parsed;\n }\n return 200; // Default\n }\n\n // #endregion\n\n /**\n * Set an icon on an element using the CSS-first hybrid approach.\n *\n * Sets `data-icon` for CSS pseudo-element rendering and `data-expanded` for\n * expand/collapse toggle elements. Only injects DOM content when a JS override\n * is present (via `gridConfig.icons` or `pluginOverride`), which naturally\n * suppresses the CSS `::before` via the `:empty` selector.\n *\n * @param element - The element to set the icon on\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, iconKey: keyof GridIcons, pluginOverride?: IconValue): void {\n // Set data-icon attribute for CSS targeting (always)\n element.dataset.icon = toIconAttr(iconKey);\n\n // Set data-expanded for toggle elements (tree, group, master-detail)\n if (iconKey === 'collapse') {\n element.dataset.expanded = '';\n } else if (iconKey === 'expand') {\n delete element.dataset.expanded;\n }\n\n // Resolve JS-only icon (returns undefined when CSS default should be used)\n const jsIcon = this.#resolveJsIcon(iconKey, pluginOverride);\n\n if (jsIcon !== undefined) {\n // JS override: inject content (suppresses CSS ::before via :empty)\n if (typeof jsIcon === 'string') {\n setSanitizedHTML(element, jsIcon);\n } else if (jsIcon instanceof HTMLElement) {\n element.replaceChildren(jsIcon.cloneNode(true));\n }\n } else {\n // CSS path: ensure element is empty so ::before renders\n element.innerHTML = '';\n }\n }\n\n /**\n * Resolve JS-only icon override. Returns undefined if CSS default should render.\n * Only returns a value when the user explicitly set gridConfig.icons[key] or\n * the plugin passed an override.\n */\n #resolveJsIcon(iconKey: keyof GridIcons, pluginOverride?: IconValue): IconValue | undefined {\n if (pluginOverride !== undefined) return pluginOverride;\n return this.grid?.gridConfig?.icons?.[iconKey];\n }\n\n /**\n * Create or replace a sort indicator on a header cell.\n *\n * Handles the full lifecycle: removes any existing indicator, creates a new\n * `<span part=\"sort-indicator\" class=\"sort-indicator\">` with the appropriate\n * icon, sets `aria-sort` / `data-sort` attributes, and inserts it before the\n * filter button or resize handle.\n *\n * Plugins decide **which** cells and **when** — this method handles **how**.\n *\n * @param cell - The header cell element\n * @param direction - `'asc'` | `'desc'` for sorted, `null` for unsorted idle state\n * @returns The created indicator element (useful for appending badges, etc.)\n */\n protected updateSortIndicator(cell: Element, direction: 'asc' | 'desc' | null): HTMLElement {\n // Remove any existing indicator (core uses part, plugins use class)\n cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator')?.remove();\n\n const indicator = document.createElement('span');\n indicator.setAttribute('part', 'sort-indicator');\n indicator.className = 'sort-indicator';\n\n if (direction) {\n cell.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');\n cell.setAttribute('data-sort', direction);\n this.setIcon(indicator, direction === 'asc' ? 'sortAsc' : 'sortDesc');\n } else {\n cell.setAttribute('aria-sort', 'none');\n cell.removeAttribute('data-sort');\n this.setIcon(indicator, 'sortNone');\n }\n\n // Insert before filter button or resize handle to maintain DOM order\n const insertBefore = cell.querySelector('.tbw-filter-btn') ?? cell.querySelector('.resize-handle');\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n return indicator;\n }\n\n /**\n * Log a warning with an optional diagnostic code.\n *\n * When a diagnostic code is provided, the message is formatted with the code\n * and a link to the troubleshooting docs.\n *\n * @example\n * ```ts\n * this.warn('Something went wrong'); // plain\n * this.warn(MISSING_BREAKPOINT, 'Set a breakpoint'); // with code + docs link\n * ```\n */\n protected warn(message: string): void;\n protected warn(code: DiagnosticCode, message: string): void;\n protected warn(codeOrMessage: DiagnosticCode | string, message?: string): void {\n if (message !== undefined) {\n // Called with (code, message)\n console.warn(formatDiagnostic(codeOrMessage as DiagnosticCode, message, this.gridElement.id, this.name));\n } else {\n // Called with (message) — plain warning, no diagnostic code\n console.warn(`${gridPrefix(this.gridElement.id, this.name)} ${codeOrMessage}`);\n }\n }\n\n /**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\n protected throwDiagnostic(code: DiagnosticCode, message: string): never {\n throw new Error(formatDiagnostic(code, message, this.gridElement.id, this.name));\n }\n\n // #region Lifecycle Hooks\n\n /**\n * Transform rows before rendering.\n * Called during each render cycle before rows are rendered to the DOM.\n * Use this to filter, sort, or add computed properties to rows.\n *\n * @param rows - The current rows array (readonly to encourage returning a new array)\n * @returns The modified rows array to render\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Filter out hidden rows\n * return rows.filter(row => !row._hidden);\n * }\n * ```\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Add computed properties\n * return rows.map(row => ({\n * ...row,\n * _fullName: `${row.firstName} ${row.lastName}`\n * }));\n * }\n * ```\n */\n processRows?(rows: readonly any[]): any[];\n\n /**\n * Contribute to the grid's effective configuration during each config merge.\n * Called by {@link ConfigManager.merge} after the base config is assembled,\n * before it becomes `effectiveConfig`. Mutate `config` in place to inject\n * plugin-derived settings (e.g. the shell plugin folds its light-DOM /\n * API-registered state into `config.shell`).\n *\n * This is the config-pipeline counterpart to {@link processColumns} /\n * {@link processRows}: it keeps the core config merge free of any plugin\n * knowledge — each plugin owns the shape it contributes.\n *\n * @param config - The mutable base config being assembled into effectiveConfig.\n *\n * @example\n * ```ts\n * processConfig(config: GridConfig): void {\n * config.shell = { ...config.shell, header: { title: this.#title } };\n * }\n * ```\n */\n processConfig?(config: GridConfig): void;\n\n /**\n * Transform columns before rendering.\n * Called during each render cycle before column headers and cells are rendered.\n * Use this to add, remove, or modify column definitions.\n *\n * @param columns - The current columns array (readonly to encourage returning a new array)\n * @returns The modified columns array to render\n *\n * @example\n * ```ts\n * processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n * // Add a selection checkbox column\n * return [\n * { field: '_select', header: '', width: 40 },\n * ...columns\n * ];\n * }\n * ```\n */\n processColumns?(columns: readonly ColumnConfig[]): ColumnConfig[];\n\n /**\n * Called before each render cycle begins.\n * Use this to prepare state or cache values needed during rendering.\n *\n * **Note:** This hook is currently a placeholder for future implementation.\n * It is defined in the interface but not yet invoked by the grid's render pipeline.\n * If you need this functionality, please open an issue or contribute an implementation.\n *\n * @example\n * ```ts\n * beforeRender(): void {\n * this.visibleRowCount = this.calculateVisibleRows();\n * }\n * ```\n */\n beforeRender?(): void;\n\n /**\n * Called after each render cycle completes.\n * Use this for DOM manipulation, adding event listeners to rendered elements,\n * or applying visual effects like selection highlights.\n *\n * @example\n * ```ts\n * afterRender(): void {\n * // Apply selection styling to rendered rows\n * const rows = this.gridElement?.querySelectorAll('.data-grid-row');\n * rows?.forEach((row, i) => {\n * row.classList.toggle('selected', this.selectedRows.has(i));\n * });\n * }\n * ```\n */\n afterRender?(): void;\n\n /**\n * Called synchronously immediately after the grid (re)builds its root DOM\n * structure, before the browser has a chance to paint.\n *\n * Unlike {@link afterRender} (which runs in the scheduler's STYLE phase and\n * may be deferred), this hook fires inside the same task as the structural\n * DOM build. It is the correct place for plugins that need to wrap, relocate,\n * or augment the freshly built grid DOM without a flash of unstyled/unwrapped\n * content — e.g. a shell that moves the grid content into a chrome wrapper.\n *\n * The grid emits this hook every time it performs a full structural rebuild\n * (initial connect and structural changes), but NOT on the scroll/data hot\n * path. Implementations MUST be idempotent: a full rebuild discards any DOM a\n * previous invocation produced, so re-create it, and no-op when the desired\n * structure is already present.\n *\n * @example\n * ```ts\n * afterStructuralRender(): void {\n * const root = this.grid?._renderRoot;\n * if (!root || root.querySelector('.my-wrapper > .tbw-grid-content')) return;\n * // build wrapper, move .tbw-grid-content inside it\n * }\n * ```\n */\n afterStructuralRender?(): void;\n\n /**\n * Called synchronously by the grid immediately before it tears down and\n * rebuilds its root DOM structure (a structural re-render). Plugins that\n * relocated existing light-DOM nodes into their own chrome (e.g. a shell that\n * moves toolbar buttons into the header bar) use this to move those nodes back\n * to their original container so they survive the rebuild. Generic\n * counterpart to {@link afterStructuralRender}. MUST be idempotent.\n */\n beforeStructuralRender?(): void;\n\n /**\n * Called by the grid after a config merge to ask whether the change requires a\n * full structural re-render (rather than a cheaper in-place update). A plugin\n * returns `true` when its rendered DOM signature no longer matches the new\n * effective config — e.g. a shell header appearing/disappearing or a tool\n * panel's count/position/mode changing. The grid performs a structural\n * re-render if ANY plugin returns `true`. MUST be side-effect free.\n */\n needsStructuralRerender?(): boolean;\n\n /**\n * Called by the grid to let a plugin (re-)parse its own light-DOM\n * contributions into plugin state ahead of a config merge. Generic seam that\n * keeps core free of plugin-specific light-DOM tag knowledge (the plugin reads\n * `this.grid._hostElement`). MUST be idempotent.\n */\n parseLightDom?(): void;\n\n /**\n * Called by the grid (from `refreshColumns` and the light-DOM observer) to\n * let a plugin re-parse its light-DOM contributions AND refresh any\n * already-rendered chrome in place when something changed — e.g. a shell\n * title or tool buttons that a framework projected asynchronously after the\n * initial render. Distinct from {@link parseLightDom}, which only updates\n * plugin state. Generic seam — core holds no plugin-specific knowledge. MUST\n * be idempotent and a no-op when the plugin's chrome is not yet rendered.\n */\n syncLightDom?(): void;\n\n /**\n * Called after each cell is rendered.\n * This hook is more efficient than `afterRender()` for cell-level modifications\n * because you receive the cell context directly - no DOM queries needed.\n *\n * Use cases:\n * - Adding selection/highlight classes to specific cells\n * - Injecting badges or decorations\n * - Applying conditional styling based on cell value\n *\n * Performance note: Called for every visible cell during render. Keep implementation fast.\n * This hook is also called during scroll when cells are recycled.\n *\n * @param context - The cell render context with row, column, value, and elements\n *\n * @example\n * ```ts\n * afterCellRender(context: AfterCellRenderContext): void {\n * // Add selection class without DOM queries\n * if (this.isSelected(context.rowIndex, context.colIndex)) {\n * context.cellElement.classList.add('selected');\n * }\n *\n * // Add validation error styling\n * if (this.hasError(context.row, context.column.field)) {\n * context.cellElement.classList.add('has-error');\n * }\n * }\n * ```\n */\n afterCellRender?(context: AfterCellRenderContext): void;\n\n /**\n * Called after a row is fully rendered (all cells complete).\n * Use this for row-level decorations, styling, or ARIA attributes.\n *\n * Common use cases:\n * - Adding selection classes to entire rows (row-focus, selected)\n * - Setting row-level ARIA attributes\n * - Applying row validation highlighting\n * - Tree indentation styling\n *\n * Performance note: Called for every visible row during render. Keep implementation fast.\n * This hook is also called during scroll when rows are recycled.\n *\n * @param context - The row render context with row data and element\n *\n * @example\n * ```ts\n * afterRowRender(context: AfterRowRenderContext): void {\n * // Add row selection class without DOM queries\n * if (this.isRowSelected(context.rowIndex)) {\n * context.rowElement.classList.add('selected', 'row-focus');\n * }\n *\n * // Add validation error styling\n * if (this.rowHasErrors(context.row)) {\n * context.rowElement.classList.add('has-errors');\n * }\n * }\n * ```\n */\n afterRowRender?(context: AfterRowRenderContext): void;\n\n /**\n * Called after scroll-triggered row rendering completes.\n * This is a lightweight hook for applying visual state to recycled DOM elements.\n * Use this instead of afterRender when you need to reapply styling during scroll.\n *\n * Performance note: This is called frequently during scroll. Keep implementation fast.\n *\n * @example\n * ```ts\n * onScrollRender(): void {\n * // Reapply selection state to visible cells\n * this.applySelectionToVisibleCells();\n * }\n * ```\n */\n onScrollRender?(): void;\n\n /**\n * Get the height of a specific row.\n * Used for synthetic rows (group headers, detail panels, etc.) that have fixed heights.\n * Return undefined if this plugin does not manage the height for this row.\n *\n * This hook is called during position cache rebuilds for variable row height virtualization.\n * Plugins that create synthetic rows should implement this to provide accurate heights.\n *\n * @param row - The row data\n * @param index - The row index in the processed rows array\n * @returns The row height in pixels, or undefined if not managed by this plugin\n */\n getRowHeight?(row: unknown, index: number): number | undefined;\n\n /**\n * Adjust the virtualization start index to render additional rows before the visible range.\n * Use this when expanded content (like detail rows) needs its parent row to remain rendered\n * even when the parent row itself has scrolled above the viewport.\n *\n * @param start - The calculated start row index\n * @param scrollTop - The current scroll position\n * @param rowHeight - The height of a single row\n * @returns The adjusted start index (lower than or equal to original start)\n *\n * @example\n * ```ts\n * adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n * // If row 5 is expanded and scrolled partially, keep it rendered\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * const expandedRowTop = expandedRowIndex * rowHeight;\n * const expandedRowBottom = expandedRowTop + rowHeight + this.detailHeight;\n * if (expandedRowBottom > scrollTop && expandedRowIndex < start) {\n * return expandedRowIndex;\n * }\n * }\n * return start;\n * }\n * ```\n */\n adjustVirtualStart?(start: number, scrollTop: number, rowHeight: number): number;\n\n /**\n * Render a custom row, bypassing the default row rendering.\n * Use this for special row types like group headers, detail rows, or footers.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if the plugin handled rendering (prevents default), `false`/`void` for default rendering\n *\n * @example\n * ```ts\n * renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void {\n * if (row._isGroupHeader) {\n * rowEl.innerHTML = `<div class=\"group-header\">${row._groupLabel}</div>`;\n * return true; // Handled - skip default rendering\n * }\n * // Return void to let default rendering proceed\n * }\n * ```\n */\n renderRow?(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void;\n\n // #endregion\n\n // #region Inter-Plugin Communication\n\n /**\n * Handle queries from other plugins or the grid.\n *\n * Queries are declared in `manifest.queries` and dispatched via `grid.query()`.\n * This enables type-safe, discoverable inter-plugin communication.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * // In manifest\n * static override readonly manifest: PluginManifest = {\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * };\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * const column = query.context as ColumnConfig;\n * return !column.sticky; // Can't move sticky columns\n * }\n * }\n * ```\n */\n handleQuery?(query: PluginQuery): unknown;\n\n // #endregion\n\n // #region Interaction Hooks\n\n /**\n * Handle keyboard events on the grid.\n * Called when a key is pressed while the grid or a cell has focus.\n *\n * @param event - The native KeyboardEvent\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onKeyDown(event: KeyboardEvent): boolean | void {\n * // Handle Ctrl+A for select all\n * if (event.ctrlKey && event.key === 'a') {\n * this.selectAllRows();\n * return true; // Prevent default browser select-all\n * }\n * }\n * ```\n */\n onKeyDown?(event: KeyboardEvent): boolean | void;\n\n /**\n * Handle cell click events.\n * Called when a data cell is clicked (not headers).\n *\n * @param event - Cell click event with row/column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onCellClick(event: CellClickEvent): boolean | void {\n * if (event.field === '_select') {\n * this.toggleRowSelection(event.rowIndex);\n * return true; // Handled\n * }\n * }\n * ```\n */\n onCellClick?(event: CellClickEvent): boolean | void;\n\n /**\n * Handle row click events.\n * Called when any part of a data row is clicked.\n * Note: This is called in addition to onCellClick, not instead of.\n *\n * @param event - Row click event with row context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onRowClick(event: RowClickEvent): boolean | void {\n * if (this.config.mode === 'row') {\n * this.selectRow(event.rowIndex, event.originalEvent);\n * return true;\n * }\n * }\n * ```\n */\n onRowClick?(event: RowClickEvent): boolean | void;\n\n /**\n * Handle header click events.\n * Called when a column header is clicked. Commonly used for sorting.\n *\n * @param event - Header click event with column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onHeaderClick(event: HeaderClickEvent): boolean | void {\n * if (event.column.sortable !== false) {\n * this.toggleSort(event.field);\n * return true;\n * }\n * }\n * ```\n */\n onHeaderClick?(event: HeaderClickEvent): boolean | void;\n\n /**\n * Handle scroll events on the grid viewport.\n * Called during scrolling. Note: This may be called frequently; debounce if needed.\n *\n * @param event - Scroll event with scroll position and viewport dimensions\n *\n * @example\n * ```ts\n * onScroll(event: ScrollEvent): void {\n * // Update sticky column positions\n * this.updateStickyPositions(event.scrollLeft);\n * }\n * ```\n */\n onScroll?(event: ScrollEvent): void;\n\n /**\n * Handle cell mousedown events.\n * Used for initiating drag operations like range selection or column resize.\n *\n * @param event - Mouse event with cell context\n * @returns `true` to indicate drag started (prevents text selection), `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseDown(event: CellMouseEvent): boolean | void {\n * if (event.rowIndex !== undefined && this.config.mode === 'range') {\n * this.startDragSelection(event.rowIndex, event.colIndex);\n * return true; // Prevent text selection\n * }\n * }\n * ```\n */\n onCellMouseDown?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mousemove events during drag operations.\n * Only called when a drag is in progress (after mousedown returned true).\n *\n * @param event - Mouse event with current cell context\n * @returns `true` to continue handling the drag, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseMove(event: CellMouseEvent): boolean | void {\n * if (this.isDragging && event.rowIndex !== undefined) {\n * this.extendSelection(event.rowIndex, event.colIndex);\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseMove?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mouseup events to end drag operations.\n *\n * @param event - Mouse event with final cell context\n * @returns `true` if drag was finalized, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseUp(event: CellMouseEvent): boolean | void {\n * if (this.isDragging) {\n * this.finalizeDragSelection();\n * this.isDragging = false;\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseUp?(event: CellMouseEvent): boolean | void;\n\n // Note: Context menu items are provided via handleQuery('getContextMenuItems').\n // This keeps the core decoupled from the context-menu plugin specifics.\n\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Contribute plugin-specific state for a column.\n * Called by the grid when collecting column state for serialization.\n * Plugins can add their own properties to the column state.\n *\n * @param field - The field name of the column\n * @returns Partial column state with plugin-specific properties, or undefined if no state to contribute\n *\n * @example\n * ```ts\n * getColumnState(field: string): Partial<ColumnState> | undefined {\n * const filterModel = this.filterModels.get(field);\n * if (filterModel) {\n * // Uses module augmentation to add filter property to ColumnState\n * return { filter: filterModel } as Partial<ColumnState>;\n * }\n * return undefined;\n * }\n * ```\n */\n getColumnState?(field: string): Partial<ColumnState> | undefined;\n\n /**\n * Apply plugin-specific state to a column.\n * Called by the grid when restoring column state from serialized data.\n * Plugins should restore their internal state based on the provided state.\n *\n * @param field - The field name of the column\n * @param state - The column state to apply (may contain plugin-specific properties)\n *\n * @example\n * ```ts\n * applyColumnState(field: string, state: ColumnState): void {\n * // Check for filter property added via module augmentation\n * const filter = (state as any).filter;\n * if (filter) {\n * this.filterModels.set(field, filter);\n * this.applyFilter();\n * }\n * }\n * ```\n */\n applyColumnState?(field: string, state: ColumnState): void;\n\n // #endregion\n\n // #region Scroll Boundary Hooks\n\n /**\n * Report horizontal scroll boundary offsets for this plugin.\n * Plugins that obscure part of the scroll area (e.g., pinned/sticky columns)\n * should return how much space they occupy on each side.\n * The keyboard navigation uses this to ensure focused cells are fully visible.\n *\n * @param rowEl - The row element (optional, for calculating widths from rendered cells)\n * @param focusedCell - The currently focused cell element (optional, to determine if scrolling should be skipped)\n * @returns Object with left/right pixel offsets and optional skipScroll flag, or undefined if plugin has no offsets\n *\n * @example\n * ```ts\n * getHorizontalScrollOffsets(rowEl?: HTMLElement, focusedCell?: HTMLElement): { left: number; right: number; skipScroll?: boolean } | undefined {\n * // Calculate total width of left-pinned columns\n * const leftCells = rowEl?.querySelectorAll('.sticky-left') ?? [];\n * let left = 0;\n * leftCells.forEach(el => { left += (el as HTMLElement).offsetWidth; });\n * // Skip scroll if focused cell is pinned (always visible)\n * const skipScroll = focusedCell?.classList.contains('sticky-left');\n * return { left, right: 0, skipScroll };\n * }\n * ```\n */\n getHorizontalScrollOffsets?(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined;\n\n /**\n * Report vertical scroll boundary offsets for this plugin.\n * Plugins that overlay the top or bottom of the rows viewport (e.g. sticky\n * row clones) should return how much space they obscure on each edge, so\n * keyboard navigation can scroll a focused row clear of the overlay rather\n * than leaving it hidden underneath — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n *\n * Only report space that is painted *over* the scrollable rows. Content that\n * takes part in normal flow (pinned-row bars, the column header) already\n * shrinks the viewport and must NOT be reported here, or rows would scroll\n * twice as far as needed.\n *\n * @param focusedRowIndex - Index of the row being scrolled into view, so a plugin can report `skipScroll` when it already renders that row in a permanently visible overlay\n * @returns Object with top/bottom pixel offsets and optional skipScroll flag, or undefined if plugin obscures nothing\n *\n * @example\n * ```ts\n * getVerticalScrollOffsets(focusedRowIndex?: number): { top: number; bottom: number; skipScroll?: boolean } | undefined {\n * if (!this.overlayEl) return undefined;\n * // The row is already pinned in the overlay — it is visible where it is.\n * if (focusedRowIndex != null && this.stuckIndices.includes(focusedRowIndex)) {\n * return { top: 0, bottom: 0, skipScroll: true };\n * }\n * return { top: this.overlayEl.offsetHeight, bottom: 0 };\n * }\n * ```\n *\n * @since 3.6.0\n */\n getVerticalScrollOffsets?(\n focusedRowIndex?: number,\n ): { top: number; bottom: number; skipScroll?: boolean } | undefined;\n\n // #endregion\n\n // #region Shell Integration Hooks\n\n /**\n * Register a tool panel for this plugin.\n * Return undefined if plugin has no tool panel.\n * The shell will create a toolbar toggle button and render the panel content\n * when the user opens the panel.\n *\n * @returns Tool panel definition, or undefined if plugin has no panel\n *\n * @example\n * ```ts\n * getToolPanel(): ToolPanelDefinition | undefined {\n * return {\n * id: 'columns',\n * title: 'Columns',\n * icon: '☰',\n * tooltip: 'Show/hide columns',\n * order: 10,\n * render: (container) => {\n * this.renderColumnList(container);\n * return () => this.cleanup();\n * },\n * };\n * }\n * ```\n */\n getToolPanel?(): ToolPanelDefinition | undefined;\n\n /**\n * Register content for the shell header center section.\n * Return undefined if plugin has no header content.\n * Examples: search input, selection summary, status indicators.\n *\n * @returns Header content definition, or undefined if plugin has no header content\n *\n * @example\n * ```ts\n * getHeaderContent(): HeaderContentDefinition | undefined {\n * return {\n * id: 'quick-filter',\n * order: 10,\n * render: (container) => {\n * const input = document.createElement('input');\n * input.type = 'text';\n * input.placeholder = 'Search...';\n * input.addEventListener('input', this.handleInput);\n * container.appendChild(input);\n * return () => input.removeEventListener('input', this.handleInput);\n * },\n * };\n * }\n * ```\n */\n getHeaderContent?(): HeaderContentDefinition | undefined;\n\n // #endregion\n}\n","/**\n * Shared Expander Column Utilities\n *\n * Provides a fixed expander column for plugins that need expand/collapse icons\n * (MasterDetail, Tree, RowGrouping). The column is:\n * - Always first in the grid\n * - Cannot be reordered (lockPosition: true)\n * - Has no header (empty string)\n * - Has no right border (borderless styling)\n * - Narrow width (just fits the icon)\n */\n\nimport type { ColumnConfig } from '../types';\n\n/** Special field name for the expander column */\nexport const EXPANDER_COLUMN_FIELD = '__tbw_expander';\n\n/** Default width for the expander column (pixels) */\nexport const EXPANDER_COLUMN_WIDTH = 32;\n\n/**\n * Marker interface for expander column renderers.\n * Used to detect if expander column is already present.\n */\nexport interface ExpanderColumnRenderer {\n (ctx: any): HTMLElement;\n __expanderColumn?: true;\n /** Plugin name that created this expander */\n __expanderPlugin?: string;\n}\n\n/**\n * Check if a column is an expander column.\n */\nexport function isExpanderColumn(column: ColumnConfig): boolean {\n return column.field === EXPANDER_COLUMN_FIELD;\n}\n\n/**\n * Check if a column is a utility column (excluded from selection, clipboard, etc.).\n * Utility columns are non-data columns like expander columns.\n */\nexport function isUtilityColumn(column: ColumnConfig): boolean {\n return column.utility === true;\n}\n\n/**\n * Find an existing expander column in the column array.\n */\nexport function findExpanderColumn(columns: readonly ColumnConfig[]): ColumnConfig | undefined {\n return columns.find(isExpanderColumn);\n}\n\n/**\n * Create the base expander column config.\n * Plugins should add their own renderer to customize the expand icon behavior.\n *\n * @param pluginName - Name of the plugin creating the expander (for debugging)\n * @returns Base column config for the expander column\n */\nexport function createExpanderColumnConfig(pluginName: string): ColumnConfig {\n return {\n field: EXPANDER_COLUMN_FIELD as any,\n header: '', // No header text - visually merges with next column\n width: EXPANDER_COLUMN_WIDTH,\n resizable: false,\n sortable: false,\n filterable: false, // No filter button for expander column\n lockPosition: true,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n meta: {\n expanderColumn: true,\n expanderPlugin: pluginName,\n },\n };\n}\n","/**\n * Generic click-only action menu — the shared building block for WCAG 2.2\n * SC 2.5.7 \"Dragging Movements\" alternatives in plugins.\n *\n * Every drag affordance in the grid needs a path that works with plain clicks\n * or taps. A keyboard equivalent is **not** enough: SC 2.5.7 is specifically\n * about pointer input, and the W3C Understanding text calls out \"a pop-up menu\n * after tap\" as a conforming alternative. This module is that pop-up.\n *\n * Lives under `plugins/shared/` on purpose: each plugin bundle is built\n * self-contained, so this code is duplicated into the plugins that use it and\n * never lands in the core `index.js` budget.\n *\n * @since 3.6.0\n * @internal\n */\n\n/** Gap between the anchor element and the menu, in px. */\nconst ANCHOR_GAP_PX = 6;\n\n/** Runtime check — happy-dom and older browsers may lack the Popover API. */\nfunction supportsPopover(): boolean {\n return typeof HTMLElement.prototype?.showPopover === 'function';\n}\n\nlet hoverlessQuery: MediaQueryList | null | undefined;\n\n/** Whether the primary pointer cannot hover (touch, most switch devices). */\nfunction isHoverless(): boolean {\n if (hoverlessQuery === undefined) hoverlessQuery = globalThis.matchMedia?.('(hover: none)') ?? null;\n return hoverlessQuery?.matches ?? false;\n}\n\n/**\n * Whether a plugin should also expose its drag alternative as a dedicated\n * inline control, on top of the menu that is always available.\n *\n * Hover-less pointers get one unconditionally — a control revealed on hover is\n * unreachable when nothing can hover, and long-press is the wrong fallback for\n * the tremor and low-dexterity users SC 2.5.7 exists for.\n *\n * Driven by `a11y.dragAlternatives` on the grid config.\n */\nexport function prefersInlineDragAlternative(grid: {\n effectiveConfig?: { a11y?: { dragAlternatives?: 'menu' | 'inline' } };\n}): boolean {\n return grid.effectiveConfig?.a11y?.dragAlternatives === 'inline' || isHoverless();\n}\n\n/** One clickable entry in the menu. */\nexport interface DragAlternativeAction {\n /** Visible text, also the accessible name. */\n label: string;\n /** Render the entry but refuse the click (e.g. \"Move up\" on the first row). */\n disabled?: boolean;\n /** Invoked on click; the menu closes first so focus handling is predictable. */\n run(): void;\n}\n\nexport interface DragAlternativeMenu {\n /**\n * Show `actions` anchored to `anchor`, with `label` as the group's accessible\n * name. Mounted inside the anchor's grid host so it inherits theme tokens.\n */\n open(anchor: HTMLElement, label: string, actions: readonly DragAlternativeAction[]): void;\n /** Hide the menu. Safe to call when already closed. */\n close(): void;\n /** Remove the element and every document-level listener. */\n dispose(): void;\n /** The menu element, or `null` before the first {@link DragAlternativeMenu.open}. */\n readonly element: HTMLElement | null;\n}\n\n/**\n * Create a menu instance.\n *\n * @param id - DOM id for the menu element, unique per owning plugin.\n * @param className - Root class name, so each plugin can style its own menu.\n */\nexport function createDragAlternativeMenu(id: string, className: string): DragAlternativeMenu {\n let root: HTMLDivElement | null = null;\n let opened = false;\n let disposed = false;\n const abort = new AbortController();\n\n function close(): void {\n if (!opened || !root) return;\n opened = false;\n if (supportsPopover()) root.hidePopover();\n root.hidden = true;\n }\n\n function build(): HTMLDivElement {\n const el = document.createElement('div');\n el.id = id;\n el.className = className;\n el.setAttribute('popover', 'manual');\n el.setAttribute('role', 'group');\n // Without the Popover API there is no UA `display: none`, so hide it here.\n el.hidden = true;\n // Tabbing past either end dismisses instead of stranding the user — the\n // menu has no visible trigger to tab back to. A null `relatedTarget` (click\n // focus in Firefox/Safari, focus leaving the document) is ignored.\n el.addEventListener(\n 'focusout',\n (e) => {\n const next = (e as FocusEvent).relatedTarget;\n if (next instanceof Node && !el.contains(next)) close();\n },\n { signal: abort.signal },\n );\n return el;\n }\n\n /**\n * Position against the anchor in viewport coordinates. The menu lives in the\n * top layer, so its containing block is the viewport regardless of where the\n * anchor sits in the DOM.\n */\n function place(anchor: HTMLElement): void {\n if (!root) return;\n const a = anchor.getBoundingClientRect();\n const box = root.getBoundingClientRect();\n const left = Math.max(4, Math.min(a.left, window.innerWidth - box.width - 4));\n const below = a.bottom + ANCHOR_GAP_PX;\n const top = below + box.height <= window.innerHeight ? below : Math.max(4, a.top - ANCHOR_GAP_PX - box.height);\n root.style.left = `${Math.round(left)}px`;\n root.style.top = `${Math.round(top)}px`;\n }\n\n // Any pointer press outside the menu dismisses it. Capture phase so the press\n // that lands on a grid cell still reaches its own handler afterwards.\n document.addEventListener(\n 'pointerdown',\n (e) => {\n if (opened && root && !e.composedPath().includes(root)) close();\n },\n { capture: true, signal: abort.signal },\n );\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e as KeyboardEvent).key === 'Escape') close();\n },\n { capture: true, signal: abort.signal },\n );\n\n return {\n get element() {\n return root;\n },\n open(anchor, label, actions) {\n if (disposed || actions.length === 0) return;\n if (!root) root = build();\n // Mount inside the grid host (light DOM) so theme custom properties and\n // `color-scheme` cascade in; the top layer keeps it out of overflow.\n const host = anchor.closest<HTMLElement>('tbw-grid, [data-tbw-grid]') ?? document.body;\n if (root.parentNode !== host) host.appendChild(root);\n root.setAttribute('aria-label', label);\n root.replaceChildren();\n for (const action of actions) {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${className}-item`;\n btn.textContent = action.label;\n btn.disabled = action.disabled === true;\n btn.addEventListener('click', () => {\n close();\n action.run();\n });\n root.appendChild(btn);\n }\n root.hidden = false;\n if (!opened && supportsPopover()) root.showPopover();\n opened = true;\n place(anchor);\n root.querySelector<HTMLButtonElement>('button:not([disabled])')?.focus();\n },\n close,\n dispose() {\n disposed = true;\n close();\n abort.abort();\n root?.remove();\n root = null;\n },\n };\n}\n","/**\n * Column Selection Core Logic\n *\n * Pure functions for column selection: mode normalization, range expansion\n * across visible columns, and Ctrl+Shift+Arrow extension math. All grid-DOM\n * interaction lives in `SelectionPlugin.ts` — this module is dependency-free\n * and trivially unit-testable.\n *\n * @since 2.8.0\n */\n\nimport type { ColumnConfig } from '../../core/types';\nimport type { SelectionMode } from './types';\n\n// #region Mode normalization\n\n/**\n * Normalized representation of `SelectionConfig.mode`. Splits the user-supplied\n * mode (single string OR array) into:\n *\n * - `primary`: the in-row axis (`'cell' | 'row' | 'range'`) — drives existing\n * click/drag/keyboard behavior. When the user configured column-only, this is\n * `'column'` and the in-row hooks treat it as inert.\n * - `columnEnabled`: whether the column axis is also active. Drives Ctrl+Click\n * on header, Ctrl+Space on focused cell, and the column-selection render pass.\n * - `bothAxes`: shorthand for \"column AND a non-column axis are configured\" —\n * this is the only state where the row↔column mutual-exclusion logic kicks in.\n */\nexport interface NormalizedModeConfig {\n primary: SelectionMode;\n columnEnabled: boolean;\n bothAxes: boolean;\n}\n\n/**\n * Normalize the user-supplied `mode` and validate it.\n *\n * Allowed shapes:\n * - Single: `'cell' | 'row' | 'column' | 'range'`\n * - Array: `['column', X]` or `[X, 'column']` where X is `'cell' | 'row' | 'range'`\n * - Single-element arrays are treated as the contained string for ergonomics.\n *\n * Throws on:\n * - Empty array\n * - Arrays of 3+ items\n * - Arrays without `'column'` (the only purpose of an array is to enable the\n * column axis alongside an in-row axis — `['row', 'cell']` etc. don't compose)\n * - Duplicate entries\n * - Unknown mode strings\n */\nexport function normalizeMode(mode: SelectionMode | SelectionMode[]): NormalizedModeConfig {\n const validModes: ReadonlySet<string> = new Set(['cell', 'row', 'column', 'range']);\n\n if (typeof mode === 'string') {\n if (!validModes.has(mode)) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode: \"${mode}\". Expected one of: 'cell' | 'row' | 'column' | 'range'.`,\n );\n }\n return {\n primary: mode,\n columnEnabled: mode === 'column',\n bothAxes: false,\n };\n }\n\n if (!Array.isArray(mode)) {\n throw new Error(`[SelectionPlugin] Invalid selection mode: expected string or array, got ${typeof mode}.`);\n }\n\n if (mode.length === 0) {\n throw new Error(`[SelectionPlugin] Invalid selection mode: array must contain at least one mode.`);\n }\n\n for (const m of mode) {\n if (!validModes.has(m)) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode in array: \"${m}\". Expected one of: 'cell' | 'row' | 'column' | 'range'.`,\n );\n }\n }\n\n // Reject duplicates\n if (new Set(mode).size !== mode.length) {\n throw new Error(`[SelectionPlugin] Invalid selection mode: array contains duplicate entries (${mode.join(', ')}).`);\n }\n\n // Single-element array → treat as plain string\n if (mode.length === 1) {\n return normalizeMode(mode[0]);\n }\n\n if (mode.length > 2) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode: arrays of more than 2 modes are not supported. Got [${mode.join(', ')}].`,\n );\n }\n\n // 2-element array: must contain 'column' + one of 'cell' | 'row' | 'range'\n const hasColumn = mode.includes('column');\n if (!hasColumn) {\n throw new Error(\n `[SelectionPlugin] Invalid selection mode: [${mode.join(', ')}]. ` +\n `Two-mode arrays must include 'column' (the only valid combinations are ['row','column'], ['cell','column'], ['range','column']). ` +\n `Other in-row modes don't compose meaningfully.`,\n );\n }\n\n const other = mode.find((m) => m !== 'column') as SelectionMode;\n if (other === 'column') {\n // Both entries are 'column' — already caught by duplicate check, but defend\n throw new Error(`[SelectionPlugin] Invalid selection mode: [${mode.join(', ')}].`);\n }\n\n return {\n primary: other,\n columnEnabled: true,\n bothAxes: true,\n };\n}\n\n// #endregion\n\n// #region Field/index helpers\n\n/**\n * Build the ordered list of selectable column fields from the grid's visible\n * columns. Utility columns (checkbox, expander, drag handle) are excluded —\n * they exist to support grid behavior, not to be selected.\n */\nexport function selectableColumnFields(visibleColumns: readonly ColumnConfig[]): string[] {\n const fields: string[] = [];\n for (const col of visibleColumns) {\n if ((col as { utility?: boolean }).utility === true) continue;\n if (typeof col.field === 'string' && col.field.length > 0) {\n fields.push(col.field);\n }\n }\n return fields;\n}\n\n/** Find the index of `field` within the selectable-fields list. -1 if not present. */\nexport function indexOfField(field: string, fields: readonly string[]): number {\n return fields.indexOf(field);\n}\n\n/**\n * Compute the inclusive field range from `anchor` to `target` along the\n * visible-column order. Returns the fields between the two (inclusive),\n * regardless of which one is to the left. Empty if either is missing.\n */\nexport function fieldsBetween(anchor: string, target: string, fields: readonly string[]): string[] {\n const a = indexOfField(anchor, fields);\n const b = indexOfField(target, fields);\n if (a === -1 || b === -1) return [];\n const start = Math.min(a, b);\n const end = Math.max(a, b);\n return fields.slice(start, end + 1);\n}\n\n// #endregion\n\n// #region Keyboard extension (Ctrl+Shift+Arrow)\n\n/**\n * Compute the new \"head\" field after pressing `ArrowLeft` or `ArrowRight`\n * during column-axis keyboard extension. `head` is the most recently focused\n * column edge (the one that moves); `anchor` stays put.\n *\n * Returns the new head field, or `null` if the move would go out of bounds\n * (caller should leave selection unchanged).\n */\nexport function computeKeyboardExtension(\n head: string | null,\n fields: readonly string[],\n direction: 'left' | 'right',\n): string | null {\n if (fields.length === 0) return null;\n if (head === null) return null;\n const headIdx = indexOfField(head, fields);\n if (headIdx < 0) return null;\n const next = direction === 'left' ? Math.max(0, headIdx - 1) : Math.min(fields.length - 1, headIdx + 1);\n if (next === headIdx) return null;\n return fields[next];\n}\n\n// #endregion\n","/**\n * Cell Range Selection Core Logic\n *\n * Pure functions for cell range selection operations.\n */\n\nimport type { InternalCellRange, CellRange } from './types';\n\n/**\n * Normalize a range so startRow/startCol are always <= endRow/endCol.\n * This handles cases where user drags from bottom-right to top-left.\n *\n * @param range - The range to normalize\n * @returns Normalized range with start <= end for both dimensions\n */\nexport function normalizeRange(range: InternalCellRange): InternalCellRange {\n return {\n startRow: Math.min(range.startRow, range.endRow),\n startCol: Math.min(range.startCol, range.endCol),\n endRow: Math.max(range.startRow, range.endRow),\n endCol: Math.max(range.startCol, range.endCol),\n };\n}\n\n/**\n * Convert an internal range to the public event format.\n *\n * @param range - The internal range to convert\n * @returns Public CellRange format with from/to coordinates\n */\nexport function toPublicRange(range: InternalCellRange): CellRange {\n const normalized = normalizeRange(range);\n return {\n from: { row: normalized.startRow, col: normalized.startCol },\n to: { row: normalized.endRow, col: normalized.endCol },\n };\n}\n\n/**\n * Convert multiple internal ranges to public format.\n *\n * @param ranges - Array of internal ranges\n * @returns Array of public CellRange format\n */\nexport function toPublicRanges(ranges: InternalCellRange[]): CellRange[] {\n return ranges.map(toPublicRange);\n}\n\n/**\n * Check if a cell is within a specific range.\n *\n * @param row - The row index to check\n * @param col - The column index to check\n * @param range - The range to check against\n * @returns True if the cell is within the range\n */\nexport function isCellInRange(row: number, col: number, range: InternalCellRange): boolean {\n const normalized = normalizeRange(range);\n return (\n row >= normalized.startRow && row <= normalized.endRow && col >= normalized.startCol && col <= normalized.endCol\n );\n}\n\n/**\n * Check if a cell is within any of the provided ranges.\n *\n * @param row - The row index to check\n * @param col - The column index to check\n * @param ranges - Array of ranges to check against\n * @returns True if the cell is within any range\n */\nexport function isCellInAnyRange(row: number, col: number, ranges: InternalCellRange[]): boolean {\n return ranges.some((range) => isCellInRange(row, col, range));\n}\n\n/**\n * Get all cells within a range as an array of {row, col} objects.\n *\n * @param range - The range to enumerate\n * @returns Array of all cell coordinates in the range\n */\nexport function getCellsInRange(range: InternalCellRange): Array<{ row: number; col: number }> {\n const cells: Array<{ row: number; col: number }> = [];\n const normalized = normalizeRange(range);\n\n for (let row = normalized.startRow; row <= normalized.endRow; row++) {\n for (let col = normalized.startCol; col <= normalized.endCol; col++) {\n cells.push({ row, col });\n }\n }\n\n return cells;\n}\n\n/**\n * Get all unique cells across multiple ranges.\n * Deduplicates cells that appear in overlapping ranges.\n *\n * @param ranges - Array of ranges to enumerate\n * @returns Array of unique cell coordinates\n */\nexport function getAllCellsInRanges(ranges: InternalCellRange[]): Array<{ row: number; col: number }> {\n const cellMap = new Map<string, { row: number; col: number }>();\n\n for (const range of ranges) {\n for (const cell of getCellsInRange(range)) {\n cellMap.set(`${cell.row},${cell.col}`, cell);\n }\n }\n\n return [...cellMap.values()];\n}\n\n/**\n * Merge overlapping or adjacent ranges into fewer ranges.\n * Simple implementation - returns ranges as-is for now.\n * More complex merging logic can be added later for optimization.\n *\n * @param ranges - Array of ranges to merge\n * @returns Merged array of ranges\n */\nexport function mergeRanges(ranges: InternalCellRange[]): InternalCellRange[] {\n // Simple implementation - more complex merging can be added later\n return ranges;\n}\n\n/**\n * Create a range from an anchor cell to a current cell position.\n * The range is not normalized - it preserves the direction of selection.\n *\n * @param anchor - The anchor cell (where selection started)\n * @param current - The current cell (where selection ends)\n * @returns An InternalCellRange from anchor to current\n */\nexport function createRangeFromAnchor(\n anchor: { row: number; col: number },\n current: { row: number; col: number }\n): InternalCellRange {\n return {\n startRow: anchor.row,\n startCol: anchor.col,\n endRow: current.row,\n endCol: current.col,\n };\n}\n\n/**\n * Calculate the number of cells in a range.\n *\n * @param range - The range to measure\n * @returns Total number of cells in the range\n */\nexport function getRangeCellCount(range: InternalCellRange): number {\n const normalized = normalizeRange(range);\n const rowCount = normalized.endRow - normalized.startRow + 1;\n const colCount = normalized.endCol - normalized.startCol + 1;\n return rowCount * colCount;\n}\n\n/**\n * Check if two ranges are equal (same boundaries).\n *\n * @param a - First range\n * @param b - Second range\n * @returns True if ranges have same boundaries after normalization\n */\nexport function rangesEqual(a: InternalCellRange, b: InternalCellRange): boolean {\n const normA = normalizeRange(a);\n const normB = normalizeRange(b);\n return (\n normA.startRow === normB.startRow &&\n normA.startCol === normB.startCol &&\n normA.endRow === normB.endRow &&\n normA.endCol === normB.endCol\n );\n}\n\n/**\n * Check if a range is a single cell (1x1).\n *\n * @param range - The range to check\n * @returns True if the range is exactly one cell\n */\nexport function isSingleCell(range: InternalCellRange): boolean {\n const normalized = normalizeRange(range);\n return normalized.startRow === normalized.endRow && normalized.startCol === normalized.endCol;\n}\n","/**\n * Selection Plugin Styles\n *\n * Scoped to tbw-grid using CSS nesting for light DOM compatibility.\n */\n@layer tbw-plugins {\n tbw-grid {\n /* Prevent text selection during range drag */\n &.selecting .data-grid-row > .cell {\n user-select: none;\n }\n\n /* Prevent text selection when any selection exists */\n &:has(.selection) {\n user-select: none;\n /* Disable cell-focus outline in row mode - row is the focus unit.\n Also disable row-focus outline - selection plugin uses ::after borders instead. */\n &[data-selection-mode='row'] .cell-focus,\n &[data-selection-mode='row'] .row-focus,\n &[data-selection-mode='range'] .cell-focus {\n outline: none;\n }\n }\n\n /* Row selection - use accent color for row focus (only when grid is focused) */\n .data-grid-row.row-focus {\n background-color: var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%));\n /* Replace outline with per-side pseudo-element borders that can collapse */\n outline: none;\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n border-width: 0;\n border-style: var(--tbw-selection-border-style, var(--tbw-border-style));\n border-color: var(--tbw-range-border-color, var(--tbw-color-accent));\n border-top-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n border-bottom-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n /* Must paint above sticky cells (.sticky-left/.sticky-right have\n z-index: 25 in core/styles/base.css) so the selection border is\n continuous across pinned columns. The border ::after has\n pointer-events: none, so it doesn't interfere with cell content. */\n z-index: 26;\n }\n\n /* Collapse borders between adjacent selected rows */\n + .data-grid-row.row-focus::after {\n border-top-width: 0;\n }\n &:has(+ .data-grid-row.row-focus)::after {\n border-bottom-width: 0;\n }\n\n /* Sticky cells (.sticky-left/.sticky-right) have an opaque\n background-color to hide unpinned content scrolling beneath them\n (see core/styles/rows.css). That opaque layer also hides the\n row-focus background-color set above. Re-paint sticky cells with\n the focus tint layered over panel-bg so the selection tint is\n visible across pinned columns. The selection BORDER is handled by\n the row's ::after with z-index above the sticky cells — no need\n to redraw it here. */\n > .cell.sticky-left,\n > .cell.sticky-right {\n background:\n linear-gradient(var(--tbw-focus-background, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),\n var(--tbw-color-panel-bg);\n }\n }\n\n /* Non-selectable selected rows use the row-alt background as the muted\n indicator — keep sticky cells in sync so pinned columns don't flash\n the accent tint on rows that aren't actually selectable. */\n .data-grid-row.row-focus[data-selectable='false'] {\n > .cell.sticky-left,\n > .cell.sticky-right {\n background: var(--tbw-color-row-alt);\n }\n }\n\n /* Selection cell styles - for range mode */\n .data-grid-row > .cell.selected {\n background-color: var(--tbw-range-selection-bg);\n position: relative;\n\n /* Pseudo-element draws the selection range border inside the cell's\n padding box. Uses inset: 0 to stay within overflow: hidden bounds.\n The cell's own borders (row divider, column border) remain visible\n outside the selection border — this is intentional and consistent\n across all themes regardless of border width or visibility. */\n &::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n border: 0 var(--tbw-selection-border-style, var(--tbw-border-style)) var(--tbw-range-border-color);\n z-index: 1;\n }\n\n &.top::after {\n border-top-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n &.bottom::after {\n border-bottom-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n &.first::after {\n border-left-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n &.last::after {\n border-right-width: var(--tbw-selection-border-width, var(--tbw-border-width));\n }\n }\n\n /* Non-selectable row styling (row mode).\n The row is dimmed to signal \"not selectable\", but its *data* is still\n content the user has to read, so the exception WCAG 1.4.3 grants inactive\n UI components does not apply. 0.6 pushed the text to 4.24:1 on white;\n 0.65 is the lowest value that clears AA (5.02:1). */\n .data-grid-row[data-selectable='false'] {\n cursor: not-allowed;\n opacity: var(--tbw-selection-disabled-opacity, 0.65);\n\n /* Muted selection indicator */\n &.row-focus {\n background-color: var(--tbw-color-row-alt);\n }\n }\n\n /* Non-selectable cell styling (cell/range mode) */\n .data-grid-row > .cell[data-selectable='false'] {\n cursor: not-allowed;\n opacity: var(--tbw-selection-disabled-opacity, 0.65);\n\n /* Prevent selection highlight on non-selectable cells */\n &.selected {\n background-color: var(--tbw-selection-warning-bg, rgba(from var(--tbw-color-error) r g b / 50%));\n }\n }\n\n /* Selection summary in header */\n .tbw-selection-summary {\n font-size: var(--tbw-font-size-sm, 0.8125rem);\n color: var(--tbw-color-fg-muted);\n white-space: nowrap;\n }\n\n /* Checkbox column styles */\n .data-grid-row > .cell[data-field='__tbw_checkbox'],\n .header-row > .cell[data-field='__tbw_checkbox'] {\n text-align: center;\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n /* Row checkboxes are visual indicators - clicks pass through to cell */\n .tbw-select-row-checkbox {\n pointer-events: none;\n margin: 0;\n cursor: pointer;\n }\n\n /* Header select-all checkbox container */\n .tbw-checkbox-header {\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100%;\n cursor: pointer;\n }\n\n .tbw-select-all-checkbox {\n margin: 0;\n cursor: pointer;\n }\n\n /* SC 2.5.8: the row checkbox is not itself a target — the row click selects,\n so the whole cell accepts the pointer. Select-all has its own handler, and\n gets a cell-sized target from the `<label>` wrapper instead of a 24px box.\n Coarse pointers still grow the boxes, which is a comfort choice. */\n @media (pointer: coarse) {\n .tbw-select-row-checkbox,\n .tbw-select-all-checkbox {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n\n /* ---- Column-axis selection ----------------------------------------- */\n /* Background tint for selected columns. Applied to the matching header\n cell and every data cell whose data-col resolves to a selected field.\n The tint uses --tbw-column-selection-bg (overridable) defaulting to\n a 12 % accent overlay — visually distinct from row-focus but uses the\n same palette so themes stay coherent. */\n .header-row > .cell.column-selected,\n .data-grid-row > .cell.column-selected {\n background:\n linear-gradient(var(--tbw-column-selection-bg, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),\n var(--tbw-color-panel-bg);\n }\n\n /* Pinned columns repaint with sticky background; layer the column tint\n on top of the panel-bg fill so the indicator survives pinning. */\n .data-grid-row > .cell.column-selected.sticky-left,\n .data-grid-row > .cell.column-selected.sticky-right,\n .header-row > .cell.column-selected.sticky-left,\n .header-row > .cell.column-selected.sticky-right {\n background:\n linear-gradient(var(--tbw-column-selection-bg, rgba(from var(--tbw-color-accent) r g b / 12%)) 0 0),\n var(--tbw-color-panel-bg);\n }\n\n /* ========== Touch selection mode (#304) ========== */\n\n /* Sticky action bar shown while touch selection mode is active. Rendered\n only on coarse-pointer devices, so the sizing targets a finger. */\n .tbw-selection-toolbar {\n position: sticky;\n top: 0;\n z-index: 30;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 4px 8px;\n background: var(--tbw-color-panel-bg);\n border-bottom: 1px solid var(--tbw-color-border);\n font-size: 13px;\n }\n\n .tbw-selection-toolbar-count {\n font-weight: 600;\n white-space: nowrap;\n }\n\n .tbw-selection-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n /* `--tbw-touch-target-min` is owned by the touch-target sub-issue (#305);\n the literal fallback keeps this file readable on its own. */\n .tbw-selection-toolbar-btn {\n min-height: var(--tbw-touch-target-min, 24px);\n padding: 0 10px;\n border: 0;\n /* Themes set radii for much larger surfaces (Material uses 12px), which\n turns a 24px control into a lozenge. */\n border-radius: min(var(--tbw-border-radius, 4px), 6px);\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n\n &:hover,\n &:focus-visible {\n background: var(--tbw-color-row-hover, light-dark(#f0f0f0, #2a2a2a));\n }\n\n &[data-action='done'] {\n font-weight: 600;\n color: var(--tbw-color-accent-text, var(--tbw-color-accent));\n }\n }\n\n /* Draggable corner dots on an active cell range (iOS Numbers idiom).\n Additive — a mouse user gets them too. The visible dot is smaller than\n the hit area, and `touch-action` is off because the drag runs on pointer\n capture. */\n .tbw-range-handle {\n position: absolute;\n z-index: 25;\n width: var(--tbw-touch-target-min, 24px);\n height: var(--tbw-touch-target-min, 24px);\n translate: -50% -50%;\n background: radial-gradient(circle, var(--tbw-color-accent) 0 5px, transparent 5px);\n cursor: crosshair;\n touch-action: none;\n\n &[hidden] {\n display: none;\n }\n\n &.dragging {\n scale: 1.3;\n }\n\n /* Tapped rather than dragged: the handle is waiting for the cell that\n will receive this corner (WCAG 2.2 SC 2.5.7). */\n &.armed {\n scale: 1.3;\n outline: 2px solid var(--tbw-color-accent);\n outline-offset: -4px;\n border-radius: 50%;\n }\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Generic pointer-drag helper — converts a `pointerdown` event into a\n * deterministic drag lifecycle with pointer capture, threshold / long-press\n * promotion, and clean teardown.\n *\n * ## Usage\n * ```ts\n * element.addEventListener('pointerdown', (e) => {\n * startPointerDrag(e, element, {\n * onMove: (pe) => resize(pe.clientX),\n * onEnd: (pe) => commit(),\n * });\n * });\n * ```\n *\n * ## Fine vs coarse pointer promotion\n * For **fine** pointers (mouse / precision trackpad): set `longPressDuration`\n * to `0` (or omit it) so the drag is promoted immediately on first move.\n *\n * For **coarse** pointers (touch / stylus): set `longPressDuration` to e.g.\n * `400` ms so the drag is only promoted after the user holds still long\n * enough. Movement exceeding `longPressSlop` px before the timer fires\n * **cancels** the drag — this lets native scroll proceed uninterrupted.\n *\n * Branch example at call site:\n * ```ts\n * const isCoarse = e.pointerType === 'touch' || e.pointerType === 'pen';\n * startPointerDrag(e, el, handlers, { longPressDuration: isCoarse ? 400 : 0 });\n * ```\n *\n * ## Pointer capture\n * `setPointerCapture` / `releasePointerCapture` are called on `captureTarget`\n * only — **never** `document` / `window` listeners. Capture is claimed at\n * **promotion**, not on `pointerdown`: a captured pointer makes the browser\n * retarget the compatibility mouse events (`mouseup`, `click`, `dblclick`) to\n * the capture element, which would break click-driven grid features.\n *\n * ## Re-entrancy\n * Simultaneous drags on the same element with the same `pointerId` are\n * silently ignored (returns a no-op cancel function).\n *\n * ## DnD plugins\n * HTML5 drag-and-drop plugins (`RowDragDropPlugin`, `ReorderPlugin`, etc.)\n * are expected to consume this module in the future (#228). The API is\n * intentionally general.\n *\n * This module is **internal only** — NOT exported from `src/public.ts`.\n *\n * @since 3.5.0\n * @internal\n */\n\n// #region Types\n\n/**\n * Callbacks supplied to {@link startPointerDrag}.\n *\n * @since 3.5.0\n */\nexport interface PointerDragHandlers {\n /**\n * Called on each `pointermove` after the drag has been promoted.\n * For immediate drags (no threshold / long-press) this fires on the very\n * first move. For long-press promotion it fires only after the delay.\n */\n onMove(e: PointerEvent): void;\n\n /**\n * Called when the pointer is released after promotion (`pointerup`).\n * Not called if the drag was cancelled before promotion.\n */\n onEnd(e: PointerEvent): void;\n\n /**\n * Called once when the drag is promoted — when the movement threshold is\n * crossed or the long-press timer elapses. Fires before the first `onMove`.\n *\n * Long-press call sites use this to enter a mode (e.g. cell range-paint) and\n * give feedback at the moment the press is recognised, even if the pointer\n * never subsequently moves.\n */\n onPromote?(): void;\n\n /**\n * Called when the drag is cancelled — by `pointercancel`, `lostpointercapture`,\n * an `Escape` keydown, or the returned `cancel()` function.\n * Also called when the pointer is released *before* promotion (tap / short press).\n */\n onCancel?(): void;\n}\n\n/**\n * Options for {@link startPointerDrag}.\n *\n * @since 3.5.0\n */\nexport interface PointerDragOptions {\n /**\n * Minimum movement in px before the drag is promoted.\n * `0` (default) promotes immediately on the first `pointermove`.\n */\n threshold?: number;\n\n /**\n * Long-press duration in milliseconds.\n *\n * `0` (default) = no long-press requirement; the drag is promoted as soon\n * as the pointer moves (subject to `threshold`).\n *\n * When `> 0` the drag is promoted only after this many ms have elapsed\n * without the pointer exceeding `longPressSlop`. If the slop is exceeded\n * before the timer fires the entire drag is cancelled so native scroll can\n * proceed.\n *\n * Use this for **coarse** (touch/stylus) pointers to disambiguate a tap\n * or a scroll gesture from an intentional drag-to-paint interaction.\n */\n longPressDuration?: number;\n\n /**\n * Maximum pointer movement (px) permitted during the long-press wait.\n * If this distance is exceeded the drag is cancelled.\n * Default: `8`.\n */\n longPressSlop?: number;\n}\n\n// #endregion\n\n// #region Re-entrancy guard\n\n/**\n * Tracks active pointer IDs per capture target.\n * Prevents two simultaneous drags on the same element with the same pointer.\n */\nconst _activePointers = new WeakMap<Element, Set<number>>();\n\nfunction _claimPointer(el: Element, id: number): boolean {\n let set = _activePointers.get(el);\n if (!set) {\n set = new Set();\n _activePointers.set(el, set);\n }\n if (set.has(id)) return false;\n set.add(id);\n return true;\n}\n\nfunction _releasePointer(el: Element, id: number): void {\n _activePointers.get(el)?.delete(id);\n}\n\n// #endregion\n\n// #region startPointerDrag\n\n/**\n * Begin a pointer drag on `captureTarget`.\n *\n * Attaches `pointermove`, `pointerup`, `pointercancel`, and\n * `lostpointercapture` listeners directly to `captureTarget` (never to\n * `document` / `window`). `setPointerCapture` is called when the drag is\n * promoted so events continue to route here regardless of DOM changes; an\n * unpromoted press never captures and therefore leaves `click` / `dblclick`\n * targeting the element actually under the pointer.\n *\n * Returns a `cancel()` dispose function. Calling it is idempotent.\n *\n * @param startEvent - The originating `pointerdown` event.\n * @param captureTarget - Element on which to set pointer capture and attach\n * pointer listeners. Must be in a connected document.\n * @param handlers - `onMove`, `onEnd`, and optional `onCancel` callbacks.\n * @param options - Threshold, long-press, and slop configuration.\n *\n * @since 3.5.0\n */\nexport function startPointerDrag(\n startEvent: PointerEvent,\n captureTarget: Element,\n handlers: PointerDragHandlers,\n options?: PointerDragOptions,\n): () => void {\n const { onMove, onEnd, onCancel, onPromote } = handlers;\n const threshold = options?.threshold ?? 0;\n const longPressDuration = options?.longPressDuration ?? 0;\n const longPressSlop = options?.longPressSlop ?? 8;\n const pointerId = startEvent.pointerId;\n\n // Re-entrancy guard — silently ignore duplicate drags on same element/pointer\n if (!_claimPointer(captureTarget, pointerId)) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const startX = startEvent.clientX;\n const startY = startEvent.clientY;\n\n // Promotion: true when the drag has passed threshold/long-press requirements\n let promoted = threshold === 0 && longPressDuration === 0;\n let done = false;\n let longPressTimer: ReturnType<typeof setTimeout> | null = null;\n let captureFailed = false;\n\n // #region Internal helpers\n\n /**\n * Claim pointer capture — called at promotion, never at `pointerdown`.\n *\n * While an element holds pointer capture the browser retargets the\n * compatibility mouse events (`mouseup`, `click`, `dblclick`) to that element\n * instead of the element actually under the pointer. Capturing on press would\n * therefore break every click-driven feature in the grid (cell editing,\n * header sort, row click) because those handlers resolve their target with\n * `event.target.closest(...)`. Promotion is the point where the gesture is\n * known to be a drag, so capturing there keeps plain clicks intact.\n */\n function acquireCapture(): boolean {\n try {\n captureTarget.setPointerCapture(pointerId);\n return true;\n } catch {\n // setPointerCapture can fail in unit-test environments without full\n // browser APIs, or if the element was detached mid-gesture. Callers\n // typically flip UI state (cursor, user-select, `isResizing`) *before*\n // calling us, so report the failure as a cancellation — otherwise that\n // state is never rolled back and the UI sticks mid-drag.\n captureFailed = true;\n return false;\n }\n }\n\n /** Promote the gesture to a drag: take capture, then notify the caller. */\n function promote(): boolean {\n promoted = true;\n if (!acquireCapture()) {\n cancel();\n return false;\n }\n onPromote?.();\n return true;\n }\n\n function teardown(): void {\n if (done) return;\n done = true;\n if (longPressTimer !== null) {\n clearTimeout(longPressTimer);\n longPressTimer = null;\n }\n captureTarget.removeEventListener('pointermove', onPointerMove);\n captureTarget.removeEventListener('pointerup', onPointerUp);\n captureTarget.removeEventListener('pointercancel', onPointerCancel);\n captureTarget.removeEventListener('lostpointercapture', onLostCapture);\n document.removeEventListener('keydown', onEscapeKey, true);\n _releasePointer(captureTarget, pointerId);\n try {\n if (captureTarget.hasPointerCapture(pointerId)) {\n captureTarget.releasePointerCapture(pointerId);\n }\n } catch {\n // Ignore — element may have been removed from DOM\n }\n }\n\n function cancel(): void {\n if (done) return;\n teardown();\n onCancel?.();\n }\n\n // #endregion\n\n // #region Event handlers\n\n function onPointerMove(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n\n const dx = e.clientX - startX;\n const dy = e.clientY - startY;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n if (!promoted) {\n if (longPressDuration > 0) {\n // Cancel entire drag if movement exceeds slop while waiting for long-press\n if (dist > longPressSlop) {\n cancel();\n return;\n }\n // Long-press not yet elapsed — swallow event, no onMove\n return;\n }\n // Threshold promotion only (no long-press)\n if (threshold > 0 && dist < threshold) return;\n if (!promote()) return;\n }\n\n onMove(e);\n }\n\n function onPointerUp(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n const wasPromoted = promoted;\n teardown();\n if (wasPromoted) {\n onEnd(e);\n } else {\n onCancel?.();\n }\n }\n\n function onPointerCancel(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n cancel();\n }\n\n function onLostCapture(event: Event): void {\n const e = event as PointerEvent;\n if (e.pointerId !== pointerId || done) return;\n cancel();\n }\n\n function onEscapeKey(event: Event): void {\n const e = event as KeyboardEvent;\n if (e.key === 'Escape' && !done) cancel();\n }\n\n // #endregion\n\n // Attach listeners before starting the long-press timer\n captureTarget.addEventListener('pointermove', onPointerMove);\n captureTarget.addEventListener('pointerup', onPointerUp);\n captureTarget.addEventListener('pointercancel', onPointerCancel);\n captureTarget.addEventListener('lostpointercapture', onLostCapture);\n document.addEventListener('keydown', onEscapeKey, true);\n\n // Drags with neither a threshold nor a long-press are promoted up front.\n if (promoted) {\n promote();\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n if (captureFailed) return () => {};\n }\n\n // Start long-press timer after capture so the countdown is accurate\n if (longPressDuration > 0) {\n longPressTimer = setTimeout(() => {\n longPressTimer = null;\n if (done) return;\n promote();\n }, longPressDuration);\n }\n\n return cancel;\n}\n\n// #endregion\n","/**\n * Touch selection mode — long-press to enter, tap to toggle, tap \"Done\" to exit.\n *\n * This module owns the *presentation* half of the touch selection idiom\n * (#304): the selection-mode toolbar and the cell-range corner handles.\n * The state machine itself lives in `SelectionPlugin`, which drives these two\n * classes from its `afterRender()` hook.\n *\n * @module\n * @since 3.5.0\n */\n\nimport { startPointerDrag } from '../../core/internal/pointer-drag';\n\n// #region Types\n\n/**\n * What happens to the selection when touch selection mode is exited.\n *\n * - `'transient'` (default) — leaving selection mode clears the selection, the\n * Gmail / Google Photos behaviour. The mode *is* the selection.\n * - `'sticky'` — the selection survives, so a subsequent tap-to-toggle round\n * can start from what was already selected.\n *\n * @since 3.5.0\n */\nexport type TouchSelectionMode = 'sticky' | 'transient';\n\n/** Callbacks the toolbar invokes; all are supplied by `SelectionPlugin`. */\nexport interface SelectionToolbarCallbacks {\n /** \"Select all\" pressed. */\n selectAll(): void;\n /** \"Clear\" pressed. */\n clear(): void;\n /** \"Done\" pressed — exit selection mode. */\n done(): void;\n /** \"More…\" pressed. Receives the button so a menu can be anchored to it. */\n more(anchor: HTMLElement): void;\n}\n\n/** A normalized rectangle in (row, visible-column) space. */\nexport interface RangeRect {\n startRow: number;\n startCol: number;\n endRow: number;\n endCol: number;\n}\n\n/** Which corner of the range rectangle a handle controls. */\nexport type RangeCorner = 'start' | 'end';\n\n/** Callbacks the corner handles invoke while being dragged. */\nexport interface RangeHandleCallbacks {\n /** Resolve the cell under a viewport coordinate, or `null` if there is none. */\n cellAt(clientX: number, clientY: number): { row: number; col: number } | null;\n /** The dragged corner moved to a new cell. */\n resize(corner: RangeCorner, row: number, col: number): void;\n /** The drag finished (pointerup) or was aborted (Escape / pointercancel). */\n commit(): void;\n /**\n * The handle was pressed and released without moving — a tap, not a drag.\n * The non-dragging path required by WCAG 2.2 SC 2.5.7: the plugin arms this\n * corner and the next cell tap places it.\n */\n arm(corner: RangeCorner): void;\n}\n\n// #endregion\n\n// #region Selection-mode toolbar\n\n/** CSS class of the toolbar root. Also used by `selection.css`. */\nconst TOOLBAR_CLASS = 'tbw-selection-toolbar';\n\n/**\n * The sticky \"N selected · Select all · Clear · More… · Done\" bar shown while\n * touch selection mode is active.\n *\n * Mounted lazily on first {@link SelectionToolbar.show} and torn down by\n * {@link SelectionToolbar.destroy}. Buttons are `type=\"button\"` so the bar can\n * safely live inside a host form.\n */\nexport class SelectionToolbar {\n #root: HTMLElement | null = null;\n #count: HTMLElement | null = null;\n #more: HTMLButtonElement | null = null;\n #callbacks: SelectionToolbarCallbacks | null = null;\n\n /** True while the toolbar is in the DOM. */\n get mounted(): boolean {\n return this.#root !== null;\n }\n\n /** The toolbar root element, or `null` when not mounted. Test seam. */\n get element(): HTMLElement | null {\n return this.#root;\n }\n\n /**\n * Mount (if needed) and update the toolbar.\n *\n * @param container Element the toolbar is prepended to — normally `.tbw-grid-root`.\n * @param selectedCount Number of selected items, rendered as \"N selected\".\n * @param showMore Whether the \"More…\" overflow button is rendered. Only true\n * when a `ContextMenuPlugin` is registered, so touch users keep parity with\n * the right-click menu.\n */\n show(container: HTMLElement, selectedCount: number, showMore: boolean, callbacks: SelectionToolbarCallbacks): void {\n this.#callbacks = callbacks;\n if (!this.#root) this.#mount(container);\n if (this.#count) {\n this.#count.textContent = `${selectedCount} selected`;\n }\n this.#root?.setAttribute('data-selected-count', String(selectedCount));\n if (this.#more) this.#more.hidden = !showMore;\n }\n\n /** Remove the toolbar from the DOM. Safe to call when not mounted. */\n destroy(): void {\n this.#root?.remove();\n this.#root = null;\n this.#count = null;\n this.#more = null;\n this.#callbacks = null;\n }\n\n #mount(container: HTMLElement): void {\n const root = container.ownerDocument.createElement('div');\n root.className = TOOLBAR_CLASS;\n root.setAttribute('role', 'toolbar');\n root.setAttribute('aria-label', 'Selection');\n\n const count = container.ownerDocument.createElement('span');\n count.className = `${TOOLBAR_CLASS}-count`;\n count.setAttribute('aria-live', 'polite');\n root.appendChild(count);\n\n const actions = container.ownerDocument.createElement('div');\n actions.className = `${TOOLBAR_CLASS}-actions`;\n root.appendChild(actions);\n\n actions.appendChild(this.#button(container, 'select-all', 'Select all', () => this.#callbacks?.selectAll()));\n actions.appendChild(this.#button(container, 'clear', 'Clear', () => this.#callbacks?.clear()));\n const more = this.#button(container, 'more', 'More…', (btn) => this.#callbacks?.more(btn));\n more.setAttribute('aria-haspopup', 'menu');\n actions.appendChild(more);\n actions.appendChild(this.#button(container, 'done', 'Done', () => this.#callbacks?.done()));\n\n container.prepend(root);\n this.#root = root;\n this.#count = count;\n this.#more = more;\n }\n\n #button(\n container: HTMLElement,\n action: string,\n label: string,\n onClick: (btn: HTMLButtonElement) => void,\n ): HTMLButtonElement {\n const btn = container.ownerDocument.createElement('button');\n btn.type = 'button';\n btn.className = `${TOOLBAR_CLASS}-btn`;\n btn.dataset.action = action;\n btn.textContent = label;\n btn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n onClick(btn);\n });\n return btn;\n }\n}\n\n// #endregion\n\n// #region Cell-range corner handles\n\n/** CSS class of a corner handle. Also used by `selection.css`. */\nconst HANDLE_CLASS = 'tbw-range-handle';\n\n/**\n * The two draggable dots at the top-left and bottom-right corners of an active\n * cell range (the iOS Numbers / Google Sheets idiom).\n *\n * They are additive rather than touch-only: a mouse user can drag them too.\n * Whether they are rendered at all is decided by `SelectionPlugin`, which only\n * asks for them when the range was started by a coarse pointer.\n * Positioning is recomputed on every render because the grid recycles row DOM\n * during virtualization, so a handle's anchor cell may not exist at all after\n * a scroll — in that case the handle is simply hidden.\n */\nexport class RangeCornerHandles {\n #start: HTMLElement | null = null;\n #end: HTMLElement | null = null;\n #callbacks: RangeHandleCallbacks | null = null;\n #armed: RangeCorner | null = null;\n\n /**\n * Render (or hide) the handles for `rect`.\n *\n * @param container Positioned ancestor the handles are absolutely placed in.\n * @param rect The normalized range, or `null` to hide the handles.\n * @param cellFor Resolves the DOM cell for a (row, col) pair, or `undefined`\n * when that cell is outside the rendered window.\n */\n render(\n container: HTMLElement,\n rect: RangeRect | null,\n cellFor: (row: number, col: number) => HTMLElement | null,\n callbacks: RangeHandleCallbacks,\n ): void {\n this.#callbacks = callbacks;\n if (!rect) {\n this.#hide();\n return;\n }\n\n this.#start ??= this.#createHandle(container, 'start');\n this.#end ??= this.#createHandle(container, 'end');\n\n this.#place(container, this.#start, cellFor(rect.startRow, rect.startCol), 'start');\n this.#place(container, this.#end, cellFor(rect.endRow, rect.endCol), 'end');\n this.setArmed(this.#armed);\n }\n\n /**\n * Mark one corner as waiting for the tap that will place it, or clear the\n * marking with `null`. Purely presentational — the plugin owns the state.\n */\n setArmed(corner: RangeCorner | null): void {\n this.#armed = corner;\n for (const [handle, name] of [\n [this.#start, 'start'],\n [this.#end, 'end'],\n ] as const) {\n if (!handle) continue;\n const on = corner === name;\n handle.classList.toggle('armed', on);\n handle.setAttribute('aria-pressed', String(on));\n }\n }\n\n /** Remove both handles from the DOM. Safe to call when not rendered. */\n destroy(): void {\n this.#start?.remove();\n this.#end?.remove();\n this.#start = null;\n this.#end = null;\n this.#callbacks = null;\n this.#armed = null;\n }\n\n #hide(): void {\n if (this.#start) this.#start.hidden = true;\n if (this.#end) this.#end.hidden = true;\n }\n\n #place(container: HTMLElement, handle: HTMLElement | null, cell: HTMLElement | null, corner: RangeCorner): void {\n if (!handle) return;\n if (!cell) {\n handle.hidden = true;\n return;\n }\n handle.hidden = false;\n // getBoundingClientRect is unavailable/zeroed in some test environments;\n // the handle still renders (and is assertable) with a zero offset.\n const cellRect = cell.getBoundingClientRect?.();\n const hostRect = container.getBoundingClientRect?.();\n if (!cellRect || !hostRect) return;\n const x = corner === 'start' ? cellRect.left - hostRect.left : cellRect.right - hostRect.left;\n const y = corner === 'start' ? cellRect.top - hostRect.top : cellRect.bottom - hostRect.top;\n handle.style.left = `${x}px`;\n handle.style.top = `${y}px`;\n }\n\n #createHandle(container: HTMLElement, corner: RangeCorner): HTMLElement {\n const el = container.ownerDocument.createElement('div');\n el.className = `${HANDLE_CLASS} ${HANDLE_CLASS}-${corner}`;\n el.dataset.corner = corner;\n // A real button, not decoration: it is tappable as well as draggable, so it\n // needs a name and a pressed state for the armed (tap-to-place) mode.\n el.setAttribute('role', 'button');\n el.setAttribute('tabindex', '-1');\n el.setAttribute('aria-pressed', 'false');\n el.setAttribute(\n 'aria-label',\n corner === 'start'\n ? 'Selection start corner — drag, or activate to place it with a tap'\n : 'Selection end corner — drag, or activate to place it with a tap',\n );\n el.addEventListener('pointerdown', (e) => this.#onPointerDown(e as PointerEvent, el, corner));\n container.appendChild(el);\n return el;\n }\n\n #onPointerDown(e: PointerEvent, handle: HTMLElement, corner: RangeCorner): void {\n if (e.button !== 0 && e.pointerType === 'mouse') return;\n e.preventDefault();\n e.stopPropagation();\n const callbacks = this.#callbacks;\n if (!callbacks) return;\n\n handle.classList.add('dragging');\n // `startPointerDrag` promotes immediately with these options, so `onMove`\n // firing at least once is the only reliable signal that this was a drag.\n let moved = false;\n const finish = (): void => {\n handle.classList.remove('dragging');\n callbacks.commit();\n };\n startPointerDrag(e, handle, {\n onMove: (moveEvent) => {\n moved = true;\n const target = callbacks.cellAt(moveEvent.clientX, moveEvent.clientY);\n if (target) callbacks.resize(corner, target.row, target.col);\n },\n onEnd: () => {\n handle.classList.remove('dragging');\n if (moved) callbacks.commit();\n else callbacks.arm(corner);\n },\n // An aborted drag still leaves the range wherever the last move put it,\n // so the plugin must re-render — but it is never a tap.\n onCancel: finish,\n });\n }\n}\n\n// #endregion\n","/**\n * Selection Plugin (Class-based)\n *\n * Provides selection functionality for tbw-grid.\n * Supports three modes:\n * - 'cell': Single cell selection (default). No border, just focus highlight.\n * - 'row': Row selection. Clicking a cell selects the entire row.\n * - 'range': Range selection. Shift+click or drag to select rectangular cell ranges.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { getPrimaryPointer } from '../../core/internal/pointer-modality';\nimport { clearCellFocus, getRowIndexFromCell } from '../../core/internal/utils';\nimport type { GridElement, HeaderClickEvent, PluginManifest, PluginQuery } from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin, CellClickEvent, CellMouseEvent } from '../../core/plugin/base-plugin';\nimport { isExpanderColumn, isUtilityColumn } from '../../core/plugin/expander-column';\nimport type { ColumnConfig } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport {\n createDragAlternativeMenu,\n type DragAlternativeAction,\n type DragAlternativeMenu,\n} from '../shared/drag-alternative-menu';\nimport {\n computeKeyboardExtension,\n fieldsBetween,\n normalizeMode,\n selectableColumnFields,\n type NormalizedModeConfig,\n} from './column-selection';\nimport {\n createRangeFromAnchor,\n getAllCellsInRanges,\n isCellInAnyRange,\n normalizeRange,\n rangesEqual,\n toPublicRanges,\n} from './range-selection';\nimport styles from './selection.css?inline';\nimport { RangeCornerHandles, SelectionToolbar, type RangeCorner, type RangeRect } from './touch-selection';\nimport type {\n CellRange,\n InternalCellRange,\n SelectionAxis,\n SelectionChangeDetail,\n SelectionConfig,\n SelectionMode,\n SelectionResult,\n} from './types';\n\n/**\n * Resolve the primary in-row mode from a config that may be a single string\n * or an array. Used by `configRules` (which run before `attach()` populates\n * the cached normalized mode). Falls back to `'cell'` on invalid input —\n * `attach()` will throw the proper error message later.\n */\nfunction primaryModeOf(mode: SelectionMode | SelectionMode[] | undefined): SelectionMode {\n if (typeof mode === 'string') return mode;\n if (Array.isArray(mode)) {\n const other = mode.find((m) => m !== 'column');\n if (other) return other;\n if (mode.includes('column')) return 'column';\n }\n return 'cell';\n}\n\n/** Special field name for the selection checkbox column */\nconst CHECKBOX_COLUMN_FIELD = '__tbw_checkbox';\n\n/**\n * Order band for the range-extension item contributed to the context menu.\n * `insertGroupSeparators` groups by the tens digit, so 60 keeps it in its own\n * group below column move (50-53).\n */\nconst EXTEND_ITEM_ORDER = 60;\n\n/** Keys that move grid focus — selection reacts to these in cell/range mode. */\nfunction isFormField(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n return tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA' || target.isContentEditable;\n}\n\nconst NAV_KEYS: readonly string[] = [\n 'ArrowUp',\n 'ArrowDown',\n 'ArrowLeft',\n 'ArrowRight',\n 'Tab',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n];\n\n/**\n * Build the selection change event detail for the current state.\n *\n * `axis` decides which axis \"won\" — when both row and column are populated\n * (which can only happen in transient states inside mutual-exclusion handling),\n * `axis` is the source of truth for which one to report.\n */\nfunction buildSelectionEvent(\n configuredMode: SelectionMode | SelectionMode[],\n axis: SelectionAxis,\n primary: SelectionMode,\n state: {\n selectedCell: { row: number; col: number } | null;\n selected: Set<number>;\n ranges: InternalCellRange[];\n selectedColumns: Set<string>;\n },\n colCount: number,\n visibleColumnFieldsInOrder: readonly string[],\n): SelectionChangeDetail {\n // Column axis active → ignore in-row state, report column field names in\n // visible-column order. WHY: `Set` insertion order reflects toggle history\n // (so the same selection emits different orderings depending on how the\n // user reached it), which makes `selection-change.detail.selectedColumns`\n // non-deterministic for consumers. Filtering the visible field list keeps\n // the event detail stable and consistent with `getSelectedColumns()`.\n if (axis === 'column' && state.selectedColumns.size > 0) {\n return {\n mode: configuredMode,\n activeAxis: 'column',\n ranges: [],\n selectedColumns: visibleColumnFieldsInOrder.filter((f) => state.selectedColumns.has(f)),\n };\n }\n\n if (primary === 'cell' && state.selectedCell) {\n return {\n mode: configuredMode,\n activeAxis: 'cell',\n ranges: [\n {\n from: { row: state.selectedCell.row, col: state.selectedCell.col },\n to: { row: state.selectedCell.row, col: state.selectedCell.col },\n },\n ],\n selectedColumns: [],\n };\n }\n\n if (primary === 'row' && state.selected.size > 0) {\n // Sort rows and merge contiguous indices into minimal ranges\n const sorted = [...state.selected].sort((a, b) => a - b);\n const ranges: CellRange[] = [];\n let start = sorted[0];\n let end = start;\n for (let i = 1; i < sorted.length; i++) {\n if (sorted[i] === end + 1) {\n end = sorted[i];\n } else {\n ranges.push({ from: { row: start, col: 0 }, to: { row: end, col: colCount - 1 } });\n start = sorted[i];\n end = start;\n }\n }\n ranges.push({ from: { row: start, col: 0 }, to: { row: end, col: colCount - 1 } });\n return { mode: configuredMode, activeAxis: 'row', ranges, selectedColumns: [] };\n }\n\n if (primary === 'range' && state.ranges.length > 0) {\n return {\n mode: configuredMode,\n activeAxis: 'range',\n ranges: toPublicRanges(state.ranges),\n selectedColumns: [],\n };\n }\n\n return { mode: configuredMode, activeAxis: 'none', ranges: [], selectedColumns: [] };\n}\n\n/**\n * Selection Plugin for tbw-grid\n *\n * Adds cell, row, and range selection capabilities to the grid with full keyboard support.\n * Whether you need simple cell highlighting or complex multi-range selections, this plugin has you covered.\n *\n * ## Installation\n *\n * ```ts\n * import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';\n * ```\n *\n * ## Selection Modes\n *\n * Configure the plugin with one of three modes via {@link SelectionConfig}:\n *\n * - **`'cell'`** - Single cell selection (default). Click cells to select individually.\n * - **`'row'`** - Full row selection. Click anywhere in a row to select the entire row.\n * - **`'range'`** - Rectangular selection. Click and drag or Shift+Click to select ranges.\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Arrow Keys` | Move selection |\n * | `Shift + Arrow` | Extend selection (range mode) |\n * | `Ctrl/Cmd + Click` | Toggle selection (multi-select) |\n * | `Shift + Click` | Extend to clicked cell/row |\n * | `Ctrl/Cmd + A` | Select all (range mode) |\n * | `Escape` | Clear selection |\n *\n * > **Note:** When `multiSelect: false`, Ctrl/Shift modifiers are ignored —\n * > clicks always select a single item.\n *\n * ## CSS Custom Properties\n *\n * | Property | Description |\n * |----------|-------------|\n * | `--tbw-focus-background` | Focused row background |\n * | `--tbw-range-selection-bg` | Range selection fill |\n * | `--tbw-range-border-color` | Range selection border |\n *\n * @example Basic row selection\n * ```ts\n * grid.gridConfig = {\n * columns: [...],\n * plugins: [new SelectionPlugin({ mode: 'row' })],\n * };\n * ```\n *\n * @example Range selection with event handling\n * ```ts\n * grid.gridConfig = {\n * plugins: [new SelectionPlugin({ mode: 'range' })],\n * };\n *\n * grid.on('selection-change', ({ mode, ranges }) => {\n * console.log(`Selected ${ranges.length} ranges in ${mode} mode`);\n * });\n * ```\n *\n * @example Programmatic selection control\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n *\n * // Get current selection\n * const selection = plugin.getSelection();\n * console.log(selection.ranges);\n *\n * // Set selection programmatically\n * plugin.setRanges([{ from: { row: 0, col: 0 }, to: { row: 5, col: 3 } }]);\n *\n * // Clear all selection\n * plugin.clearSelection();\n * ```\n *\n * @see {@link SelectionMode} for detailed mode descriptions\n * @see {@link SelectionConfig} for configuration options\n * @see {@link SelectionResult} for the selection result structure\n * @see {@link SelectionConfig} for interactive examples in the docs site\n * @since 0.1.1\n */\nexport class SelectionPlugin extends BaseGridPlugin<SelectionConfig> {\n /**\n * Plugin manifest - declares queries and configuration validation rules.\n * @internal\n */\n static override readonly manifest: PluginManifest<SelectionConfig> = {\n hookPriority: {\n // Inject the checkbox before pinned-columns reorders and applies sticky offsets.\n processColumns: -20,\n },\n queries: [\n { type: 'getSelection', description: 'Get the current selection state' },\n { type: 'selectRows', description: 'Select specific rows by index (row mode only)' },\n { type: 'getSelectedRowIndices', description: 'Get sorted array of selected row indices' },\n { type: 'getSelectedRows', description: 'Get actual row objects for the current selection (works in all modes)' },\n { type: 'getSelectedColumns', description: 'Get field names of selected columns (column mode only)' },\n { type: 'selectColumns', description: 'Select specific columns by field name (column mode only)' },\n {\n type: 'getContextMenuItems',\n description: 'Contribute the non-dragging range-extension action (WCAG 2.2 SC 2.5.7)',\n },\n ],\n configRules: [\n {\n id: 'selection/range-dblclick',\n severity: 'warn',\n message:\n `\"triggerOn: 'dblclick'\" has no effect when mode is \"range\".\\n` +\n ` → Range selection uses drag interaction (mousedown → mousemove), not click events.\\n` +\n ` → The \"triggerOn\" option only affects \"cell\" and \"row\" selection modes.`,\n check: (config) => primaryModeOf(config.mode) === 'range' && config.triggerOn === 'dblclick',\n },\n {\n id: 'selection/column-checkbox',\n severity: 'warn',\n message:\n `\"checkbox: true\" only renders in row mode.\\n` +\n ` → Column selection has no checkbox UI; activate columns via Ctrl+Click on a header or Ctrl+Space on a focused cell.`,\n check: (config) => !!config.checkbox && primaryModeOf(config.mode) !== 'row',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'selection';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<SelectionConfig> {\n return {\n mode: 'cell',\n triggerOn: 'click',\n enabled: true,\n multiSelect: true,\n touchMode: 'transient',\n };\n }\n\n // #region Touch Selection State (#304)\n\n /**\n * True while touch selection mode is active — long-press entered it, plain\n * taps now toggle instead of replacing, and the selection toolbar is shown.\n *\n * Read-only for consumers; drive it with {@link exitTouchSelection}.\n *\n * @since 3.5.0\n */\n get touchSelectionActive(): boolean {\n return this.#touchActive;\n }\n\n #touchActive = false;\n /**\n * Whether the *current* range was started by a finger/stylus. Gates the\n * corner handles: on desktop they would sit inside other ranges (multi-range\n * is mouse-only) and get in the way of normal drag-selection.\n */\n #rangeFromCoarsePointer = false;\n readonly #toolbar = new SelectionToolbar();\n readonly #cornerHandles = new RangeCornerHandles();\n\n // #endregion\n\n // #region Non-dragging range alternatives (WCAG 2.2 SC 2.5.7)\n\n /** Corner waiting for the tap that will place it, set by tapping its handle. */\n #armedCorner: RangeCorner | null = null;\n /**\n * Last cell the user deliberately anchored with a *primary* interaction.\n * Kept separate from {@link cellAnchor} because a right-click outside the\n * range re-anchors that one so the menu targets the clicked cell — which\n * would leave \"Extend selection to here\" with nothing to extend from.\n */\n #extendAnchor: { row: number; col: number } | null = null;\n #extendMenu: DragAlternativeMenu | null = null;\n\n // #endregion\n\n // #region Internal State\n /** Row selection state (row mode) */\n private selected = new Set<number>();\n private lastSelected: number | null = null;\n private anchor: number | null = null;\n\n /** Range selection state (range mode) */\n private ranges: InternalCellRange[] = [];\n private activeRange: InternalCellRange | null = null;\n private cellAnchor: { row: number; col: number } | null = null;\n private isDragging = false;\n\n /** Pending keyboard navigation update (processed in afterRender) */\n private pendingKeyboardUpdate: { shiftKey: boolean } | null = null;\n\n /** Pending row-mode keyboard update (processed in afterRender) */\n private pendingRowKeyUpdate: { shiftKey: boolean } | null = null;\n\n /** Cell selection state (cell mode) */\n private selectedCell: { row: number; col: number } | null = null;\n\n /**\n * Column selection state (column mode / `['row','column']` array mode).\n * Stored as field-name strings so selection survives column pinning,\n * reordering, and virtualization recycling.\n */\n private selectedColumns = new Set<string>();\n /** Anchor column for Ctrl+Shift+click range extension. */\n private columnAnchor: string | null = null;\n /** Head column for Ctrl+Shift+Arrow keyboard extension. */\n private columnHead: string | null = null;\n /** Which axis last won. Drives `SelectionChangeDetail.activeAxis` and mutual exclusion. */\n private activeAxis: SelectionAxis = 'none';\n\n /**\n * Normalized mode config, computed once in `attach()`. All mode checks in this\n * plugin go through `this.#mode` rather than `this.config.mode` so the\n * single-string vs. array distinction is resolved in exactly one place.\n */\n #mode: NormalizedModeConfig = { primary: 'cell', columnEnabled: false, bothAxes: false };\n\n /** Last synced focus row — used to detect when grid focus moves so selection follows */\n private lastSyncedFocusRow = -1;\n /** Last synced focus col (cell mode) */\n private lastSyncedFocusCol = -1;\n\n /** Debounce timer for selection announcements */\n private announceTimer: ReturnType<typeof setTimeout> | null = null;\n /** Set by Shift/Ctrl+Space so the next announcement names the rows/columns selected. */\n #pendingSpanAnnouncement: { axis: 'row' | 'column'; count: number } | null = null;\n /** Range built by Shift/Ctrl+Space; Shift+Arrow keeps its spanned axis full while it is still `activeRange`. */\n #spanRange: { axis: 'row' | 'column'; range: InternalCellRange } | null = null;\n\n /** True when selection was explicitly set (click/keyboard) — prevents #syncSelectionToFocus from overwriting */\n private explicitSelection = false;\n\n // #endregion\n\n // #region Private Helpers - Selection Enabled Check\n\n /**\n * Check if selection is enabled at the grid level.\n * Grid-wide `selectable: false` or plugin's `enabled: false` disables all selection.\n */\n private isSelectionEnabled(): boolean {\n // Check plugin config first\n if (this.config.enabled === false) return false;\n // Check grid-level config\n return this.grid.effectiveConfig?.selectable !== false;\n }\n\n // #endregion\n\n // #region Private Helpers - Selectability\n\n /**\n * Check if a row/cell is selectable.\n * Returns true if selectable, false if not.\n */\n private checkSelectable(rowIndex: number, colIndex?: number): boolean {\n const { isSelectable } = this.config;\n if (!isSelectable) return true; // No callback = all selectable\n\n const row = this.rows[rowIndex];\n if (!row) return false;\n\n // colIndex is a visible-column index (from data-col), so use visibleColumns\n const column = colIndex !== undefined ? this.visibleColumns[colIndex] : undefined;\n return isSelectable(row, rowIndex, column, colIndex);\n }\n\n /**\n * Check if an entire row is selectable (for row mode).\n */\n private isRowSelectable(rowIndex: number): boolean {\n return this.checkSelectable(rowIndex);\n }\n\n /**\n * Check if a cell is selectable (for cell/range modes).\n */\n private isCellSelectable(rowIndex: number, colIndex: number): boolean {\n return this.checkSelectable(rowIndex, colIndex);\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Resolve the user-supplied mode (string OR array) into a single normalized\n // shape. Throws on invalid combinations (e.g. ['row', 'cell']).\n this.#mode = normalizeMode(this.config.mode);\n\n // Subscribe to events that invalidate selection\n // When rows change due to filtering/grouping/tree/sort operations, selection indices become invalid\n this.on('filter-change', () => this.clearSelectionSilent());\n this.on('group-toggle', () => this.clearSelectionSilent());\n this.on('tree-expand', () => this.clearSelectionSilent());\n this.on('sort-change', () => this.clearSelectionSilent());\n\n // Auto-select the row currently being edited so consumers of getSelectedRows()\n // / selectedRows() always see the row the user is actually working with.\n // Issue #284: editing and selection were independent, so a row could be in\n // edit mode while selectedRows() returned a stale (or different) row.\n // Only meaningful in row mode — cell mode tracks single-cell focus, range\n // mode is for bulk selection. We listen to `edit-open` (broadcast by\n // EditingPlugin) — `edit-close` is intentionally ignored so existing\n // selections are preserved when the user finishes editing.\n this.on<{ rowIndex: number; row: unknown }>('edit-open', ({ rowIndex, row }) => {\n if (!this.isSelectionEnabled()) return;\n if (row == null || rowIndex < 0) return;\n if (this.#mode.primary !== 'row') return;\n if (!this.isRowSelectable(rowIndex)) return;\n if (this.selected.has(rowIndex)) return;\n // multiSelect: false → replace; otherwise add to existing set so\n // multi-selection is preserved when the user enters edit on one row.\n if (this.config.multiSelect === false) {\n this.selected.clear();\n }\n this.selected.add(rowIndex);\n this.lastSelected = rowIndex;\n this.anchor = rowIndex;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n });\n\n // Source-row collection replaced from outside (host swapped `[rows]`).\n // `data-change` also fires for in-place cell edits, so gate on sourceRowCount\n // changing — the only signal that the source collection actually grew/shrank.\n // Without this, stored row indices resolve against a different array and\n // getSelectedRows() silently returns the wrong rows.\n let lastSourceRowCount = -1;\n grid.addEventListener(\n 'data-change',\n ((event: CustomEvent<{ sourceRowCount: number }>) => {\n const { sourceRowCount } = event.detail;\n const hasSelection = this.selected.size > 0 || this.ranges.length > 0 || this.selectedCell !== null;\n if (lastSourceRowCount !== -1 && sourceRowCount !== lastSourceRowCount && hasSelection) {\n this.clearSelectionSilent();\n }\n lastSourceRowCount = sourceRowCount;\n }) as EventListener,\n { signal: this.disconnectSignal },\n );\n\n // SC 2.5.7 fallback: without a ContextMenuPlugin there is no menu to\n // contribute \"Extend selection to here\" to, so host a bare one. Delegated\n // rather than per-cell — cells are recycled on every render.\n grid.addEventListener(\n 'contextmenu',\n ((event: MouseEvent) => {\n if (this.#contextMenuPlugin()) return; // the real menu owns this gesture\n const cell = (event.target as Element | null)?.closest?.<HTMLElement>('.cell[data-row][data-col]');\n if (!cell) return;\n const row = parseInt(cell.getAttribute('data-row') ?? '-1', 10);\n const col = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n const actions = this.#extendActions(row, col);\n if (!actions) return;\n event.preventDefault();\n this.#openExtendMenu(cell, actions);\n }) as EventListener,\n { signal: this.disconnectSignal },\n );\n }\n\n /**\n * Handle queries from other plugins.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getSelection') {\n return this.getSelection();\n }\n if (query.type === 'getSelectedRowIndices') {\n return this.getSelectedRowIndices();\n }\n if (query.type === 'getSelectedRows') {\n return this.getSelectedRows();\n }\n if (query.type === 'selectRows') {\n this.selectRows(query.context as number[]);\n return true;\n }\n if (query.type === 'getSelectedColumns') {\n return this.getSelectedColumns();\n }\n if (query.type === 'selectColumns') {\n const fields = query.context as string[];\n this.#setColumnSelection(fields);\n return true;\n }\n if (query.type === 'getContextMenuItems') {\n const params = query.context as ContextMenuParams | undefined;\n if (!params || params.isHeader) return undefined;\n const actions = this.#extendActions(params.rowIndex, params.columnIndex);\n if (!actions) return undefined;\n return actions.map((action, i): HeaderContextMenuItem => ({\n id: `selection-extend-${i}`,\n label: action.label,\n disabled: action.disabled,\n action: () => action.run(),\n order: EXTEND_ITEM_ORDER + i,\n }));\n }\n return undefined;\n }\n\n /** @internal */\n override detach(): void {\n // Clear aria-multiselectable that we set on the role=grid element.\n // Other lifecycle teardown happens below.\n const rowsBodyEl = this.gridElement?.querySelector('.rows-body');\n rowsBodyEl?.removeAttribute('aria-multiselectable');\n\n this.#toolbar.destroy();\n this.#cornerHandles.destroy();\n this.#extendMenu?.dispose();\n this.#extendMenu = null;\n this.#armedCorner = null;\n this.#extendAnchor = null;\n this.#touchActive = false;\n\n this.selected.clear();\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.isDragging = false;\n this.selectedCell = null;\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n this.activeAxis = 'none';\n this.pendingKeyboardUpdate = null;\n this.pendingRowKeyUpdate = null;\n this.lastSyncedFocusRow = -1;\n this.lastSyncedFocusCol = -1;\n // Cancel the debounced screen-reader announcement so it can't fire after\n // teardown (the callback reaches `announce()` → `requestAnimationFrame`,\n // which is undefined once the host/DOM env is gone — surfaces as an\n // \"Unhandled Error: requestAnimationFrame is not defined\" in tests).\n if (this.announceTimer) {\n clearTimeout(this.announceTimer);\n this.announceTimer = null;\n }\n }\n\n /**\n * Clear selection without emitting an event.\n * Used when selection is invalidated by external changes (filtering, grouping, etc.)\n *\n * Column selection is intentionally PRESERVED here — it tracks field names,\n * not row indices, so it stays valid across filter/sort/group changes. Use\n * {@link clearSelection} (public) or {@link #clearAllAxesSilent} for a full wipe.\n */\n private clearSelectionSilent(): void {\n this.selected.clear();\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.selectedCell = null;\n this.lastSelected = null;\n this.anchor = null;\n this.lastSyncedFocusRow = -1;\n this.lastSyncedFocusCol = -1;\n if (this.activeAxis !== 'column') {\n this.activeAxis = 'none';\n }\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Touch Selection Mode (#304)\n\n /**\n * True when a press originated from a finger or stylus.\n *\n * Prefers the per-event `pointerType` over the `(pointer: coarse)` media\n * query so that a hybrid device (e.g. a Surface with both a touchscreen and\n * a mouse) keeps mouse chords working while touch gets the long-press idiom.\n */\n #isCoarseEvent(e: MouseEvent): boolean {\n const pointerType = (e as PointerEvent).pointerType;\n if (pointerType === 'touch' || pointerType === 'pen') return true;\n if (pointerType === 'mouse') return false;\n return getPrimaryPointer() === 'coarse';\n }\n\n /**\n * Enter touch selection mode, seeding it with `rowIndex`.\n *\n * Long-pressing a row while already in the mode range-extends from the\n * anchor instead — that branch is handled by the caller.\n */\n #enterTouchSelection(rowIndex: number): boolean {\n if (!this.isRowSelectable(rowIndex)) return false;\n this.#touchActive = true;\n this.selected.clear();\n this.selected.add(rowIndex);\n this.anchor = rowIndex;\n this.lastSelected = rowIndex;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /**\n * Leave touch selection mode.\n *\n * With the default `touchMode: 'transient'` this also clears the selection —\n * the mode *is* the selection, as in Gmail. With `touchMode: 'sticky'` the\n * selection survives so a later round can build on it.\n *\n * No-op when the mode is not active.\n *\n * @since 3.5.0\n */\n exitTouchSelection(): void {\n if (!this.#touchActive) return;\n this.#touchActive = false;\n if ((this.config.touchMode ?? 'transient') === 'transient') {\n this.selected.clear();\n this.anchor = null;\n this.lastSelected = null;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n this.requestAfterRender();\n }\n\n /** Range-extend from the touch anchor to `rowIndex` (second long-press). */\n #extendTouchSelection(rowIndex: number): void {\n const anchor = this.anchor ?? rowIndex;\n const start = Math.min(anchor, rowIndex);\n const end = Math.max(anchor, rowIndex);\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) this.selected.add(i);\n }\n this.lastSelected = rowIndex;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /** The `ContextMenuPlugin` instance, when one is registered. */\n #contextMenuPlugin(): { showMenu(x: number, y: number, params: Record<string, unknown>): void } | undefined {\n const plugin = this.grid?.getPluginByName?.('contextMenu') ?? this.grid?.getPluginByName?.('context-menu');\n const candidate = plugin as { showMenu?: unknown } | undefined;\n return typeof candidate?.showMenu === 'function'\n ? (candidate as { showMenu(x: number, y: number, params: Record<string, unknown>): void })\n : undefined;\n }\n\n /** Resolve the rendered cell element for a (row, visible-col) pair. */\n #cellFor(row: number, col: number): HTMLElement | null {\n return this.gridElement?.querySelector<HTMLElement>(`.cell[data-row=\"${row}\"][data-col=\"${col}\"]`) ?? null;\n }\n\n /** Resolve the (row, col) of the cell under a viewport coordinate. */\n #cellAtPoint(clientX: number, clientY: number): { row: number; col: number } | null {\n const host = this.gridElement;\n const doc = host?.ownerDocument;\n const el = doc?.elementFromPoint?.(clientX, clientY);\n const cell = (el as Element | null)?.closest?.('.cell[data-row][data-col]');\n // `elementFromPoint` is document-wide: when several grids share a page the\n // drag can wander over a *different* grid's cells. Only accept cells that\n // belong to this grid instance.\n if (!cell || !host?.contains?.(cell)) return null;\n const row = parseInt(cell.getAttribute('data-row') ?? '-1', 10);\n const col = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n return row >= 0 && col >= 0 ? { row, col } : null;\n }\n\n /** Move one corner of the active range while its handle is dragged. */\n #resizeRangeCorner(corner: RangeCorner, row: number, col: number): void {\n const current = this.ranges[this.ranges.length - 1];\n if (!current) return;\n const normalized = normalizeRange(current);\n const anchor =\n corner === 'start'\n ? { row: normalized.endRow, col: normalized.endCol }\n : { row: normalized.startRow, col: normalized.startCol };\n const next = createRangeFromAnchor(anchor, { row, col });\n if (rangesEqual(current, next)) return;\n this.ranges[this.ranges.length - 1] = next;\n this.activeRange = next;\n this.cellAnchor = anchor;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n // #endregion\n // #region Non-dragging range alternatives (WCAG 2.2 SC 2.5.7)\n\n /**\n * The single-pointer alternative to paint-dragging a range: click the first\n * cell as usual, then open the context menu on the opposite corner and pick\n * \"Extend selection to here\".\n *\n * Returns `null` — not an empty array — when the action does not apply, so\n * the plugin contributes nothing rather than a dead entry.\n */\n #extendActions(rowIndex: number, colIndex: number): DragAlternativeAction[] | null {\n if (this.#mode.primary !== 'range') return null;\n if (!this.isSelectionEnabled()) return null;\n if (rowIndex < 0 || colIndex < 0) return null;\n if (!this.isCellSelectable(rowIndex, colIndex)) return null;\n const anchor = this.#extendAnchor;\n if (!anchor) return null;\n if (anchor.row === rowIndex && anchor.col === colIndex) return null;\n return [\n {\n label: 'Extend selection to here',\n run: () => this.#extendSelectionTo(rowIndex, colIndex),\n },\n ];\n }\n\n /** Replace the active range with the rectangle from {@link #extendAnchor} to (row, col). */\n #extendSelectionTo(row: number, col: number): void {\n const anchor = this.#extendAnchor;\n if (!anchor) return;\n const next = createRangeFromAnchor(anchor, { row, col });\n this.ranges = [next];\n this.activeRange = next;\n this.cellAnchor = anchor;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Fallback menu for when no `ContextMenuPlugin` is registered. The alternative\n * to a drag must exist whether or not an optional plugin is installed, so the\n * shared bare menu stands in — but it must never stack on top of the real one.\n */\n #openExtendMenu(anchor: HTMLElement, actions: DragAlternativeAction[]): void {\n this.#extendMenu ??= createDragAlternativeMenu('tbw-range-extend-menu', 'tbw-range-extend-menu');\n this.#extendMenu.open(anchor, 'Selection', actions);\n }\n\n /**\n * Arm (or disarm) a range corner after its handle was tapped rather than\n * dragged. The next cell click places the corner.\n */\n #armRangeCorner(corner: RangeCorner): void {\n this.#armedCorner = this.#armedCorner === corner ? null : corner;\n this.#cornerHandles.setArmed(this.#armedCorner);\n if (this.gridElement) {\n announce(\n this.gridElement,\n this.#armedCorner ? 'Select a cell to place the selection corner' : 'Selection corner placement cancelled',\n );\n }\n }\n\n /** Clear any armed corner. Returns `true` when something was actually cleared. */\n #disarmRangeCorner(): boolean {\n if (!this.#armedCorner) return false;\n this.#armedCorner = null;\n this.#cornerHandles.setArmed(null);\n return true;\n }\n\n // #endregion\n // #region Touch chrome\n\n /**\n * Render (or tear down) the touch chrome: the selection-mode toolbar and the\n * range corner handles. Called from `afterRender()`.\n *\n * The toolbar follows `#touchActive`, which can only have been set by a\n * coarse-pointer long-press — the modality check happens once, per event, at\n * entry. Re-testing the `(pointer: coarse)` media query here would break\n * hybrid devices (a Surface reports a fine primary pointer even while the\n * user is touching the screen). The corner handles follow the same rule via\n * `#rangeFromCoarsePointer`: they exist because touch has no Shift/Ctrl, so a\n * mouse-started range never gets them.\n */\n #renderTouchChrome(container: HTMLElement | null): void {\n if (!container) return;\n\n if (this.#touchActive) {\n this.#toolbar.show(container, this.selected.size, this.#contextMenuPlugin() !== undefined, {\n selectAll: () => this.selectAll(),\n clear: () => this.clearSelection(),\n done: () => this.exitTouchSelection(),\n more: (btn) => this.#openOverflowMenu(btn),\n });\n } else {\n this.#toolbar.destroy();\n }\n\n const active =\n this.#rangeFromCoarsePointer && this.#mode.primary === 'range' && this.ranges.length > 0\n ? this.ranges[this.ranges.length - 1]\n : null;\n const rect: RangeRect | null = active ? normalizeRange(active) : null;\n if (!rect) this.#disarmRangeCorner();\n this.#cornerHandles.render(container, rect, (row, col) => this.#cellFor(row, col), {\n cellAt: (x, y) => this.#cellAtPoint(x, y),\n resize: (corner, row, col) => this.#resizeRangeCorner(corner, row, col),\n commit: () => this.requestAfterRender(),\n arm: (corner) => this.#armRangeCorner(corner),\n });\n }\n\n /**\n * Surface the `ContextMenuPlugin` items from the toolbar's \"More…\" button, so\n * touch users don't lose the actions a mouse user reaches by right-clicking.\n */\n #openOverflowMenu(anchorEl: HTMLElement): void {\n const menu = this.#contextMenuPlugin();\n if (!menu) return;\n const rect = anchorEl.getBoundingClientRect?.();\n const rowIndex = this.lastSelected ?? -1;\n menu.showMenu(rect?.left ?? 0, rect?.bottom ?? 0, {\n row: rowIndex >= 0 ? this.rows[rowIndex] : null,\n rowIndex,\n // ContextMenuParams.selectedRows is a list of row *indices*, not row objects.\n selectedRows: this.getSelectedRowIndices(),\n });\n }\n\n // #endregion\n // #region Event Handlers\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return false;\n\n // Skip if event type doesn't match configured trigger.\n // This allows dblclick mode to only select on double-click.\n const { triggerOn = 'click' } = this.config;\n if (event.originalEvent.type !== triggerOn) return false;\n\n switch (this.#mode.primary) {\n case 'cell':\n return this.#clickSelectCell(event);\n case 'row':\n return this.#clickSelectRow(event);\n case 'range':\n return this.#clickSelectRange(event);\n default:\n return false;\n }\n }\n\n /**\n * CELL MODE: single-cell selection. Skips utility columns and non-selectable\n * cells, and only emits when the selection actually changed.\n */\n #clickSelectCell(event: CellClickEvent): boolean {\n const { rowIndex, colIndex } = event;\n\n // Check if this is a utility column (expander columns, etc.)\n // event.column is already resolved from _visibleColumns in the event builder.\n // Allow the event to propagate, but don't select utility cells.\n if (event.column && isUtilityColumn(event.column)) return false;\n if (!this.isCellSelectable(rowIndex, colIndex)) return false;\n\n const currentCell = this.selectedCell;\n // Same cell already selected\n if (currentCell && currentCell.row === rowIndex && currentCell.col === colIndex) return false;\n\n this.selectedCell = { row: rowIndex, col: colIndex };\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return false;\n }\n\n /** ROW MODE: multi-select with Shift/Ctrl, checkbox toggle, or single select. */\n #clickSelectRow(event: CellClickEvent): boolean {\n const { rowIndex, originalEvent, column } = event;\n if (!this.isRowSelectable(rowIndex)) return false;\n\n const multiSelect = this.config.multiSelect !== false;\n const shiftKey = originalEvent.shiftKey && multiSelect;\n // While touch selection mode is active a plain tap toggles, exactly as if\n // Ctrl were held — that is the whole point of the mode. Mouse chords are\n // untouched, so a hybrid device supports both at once.\n const ctrlKey = (originalEvent.ctrlKey || originalEvent.metaKey || this.#touchActive) && multiSelect;\n const isCheckbox = column?.checkboxColumn === true;\n\n if (shiftKey && this.anchor !== null) {\n // Shift+Click: Range select from anchor to clicked row\n const start = Math.min(this.anchor, rowIndex);\n const end = Math.max(this.anchor, rowIndex);\n if (!ctrlKey) this.selected.clear();\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) this.selected.add(i);\n }\n } else if (ctrlKey || (isCheckbox && multiSelect)) {\n // Ctrl+Click or checkbox click: Toggle individual row\n if (this.selected.has(rowIndex)) this.selected.delete(rowIndex);\n else this.selected.add(rowIndex);\n this.anchor = rowIndex;\n } else {\n // Plain click (or any click when multiSelect is false): select only clicked row.\n // Same row already selected → nothing to do.\n if (this.selected.size === 1 && this.selected.has(rowIndex)) return false;\n this.selected.clear();\n this.selected.add(rowIndex);\n this.anchor = rowIndex;\n }\n\n this.lastSelected = rowIndex;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return false;\n }\n\n /** RANGE MODE: Shift+click extends the selection, plain click starts a new one. */\n #clickSelectRange(event: CellClickEvent): boolean {\n const { rowIndex, colIndex, originalEvent } = event;\n\n // Skip utility columns and non-selectable cells - don't start selection from them\n if (event.column && isUtilityColumn(event.column)) return false;\n if (!this.isCellSelectable(rowIndex, colIndex)) return false;\n\n // A tapped (not dragged) corner handle is waiting for a destination — this\n // click places it instead of starting a new selection (SC 2.5.7).\n if (this.#armedCorner) {\n const corner = this.#armedCorner;\n this.#disarmRangeCorner();\n this.#resizeRangeCorner(corner, rowIndex, colIndex);\n return true;\n }\n\n // A tap never reaches `onCellMouseDown` (coarse presses only dispatch it\n // after a long-press), so the handle gate has to be updated here too.\n this.#rangeFromCoarsePointer = this.#isCoarseEvent(originalEvent);\n\n const shiftKey = originalEvent.shiftKey;\n const ctrlKey = (originalEvent.ctrlKey || originalEvent.metaKey) && this.config.multiSelect !== false;\n\n if (shiftKey && this.cellAnchor) {\n // Extend selection from anchor\n const newRange = createRangeFromAnchor(this.cellAnchor, { row: rowIndex, col: colIndex });\n\n // Check if range actually changed\n const currentRange = this.ranges.length > 0 ? this.ranges[this.ranges.length - 1] : null;\n if (currentRange && rangesEqual(currentRange, newRange)) return false;\n\n if (!ctrlKey) this.ranges = [newRange];\n else if (this.ranges.length > 0) this.ranges[this.ranges.length - 1] = newRange;\n else this.ranges.push(newRange);\n this.activeRange = newRange;\n } else {\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n if (ctrlKey) {\n this.ranges.push(newRange);\n } else {\n // Plain click - only emit if the same single-cell range isn't already selected\n if (this.ranges.length === 1 && rangesEqual(this.ranges[0], newRange)) return false;\n this.ranges = [newRange];\n }\n this.activeRange = newRange;\n this.cellAnchor = { row: rowIndex, col: colIndex };\n this.#extendAnchor = { row: rowIndex, col: colIndex };\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return false;\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return false;\n\n if (this.#mode.columnEnabled && this.#keyColumnAxis(event)) return true;\n\n const mode = this.#mode.primary;\n\n // Escape unwinds one step at a time: an armed range corner first, then\n // touch selection mode, then the selection itself.\n if (event.key === 'Escape' && this.#disarmRangeCorner()) return true;\n\n // Escape leaves touch selection mode first (an external keyboard is the\n // documented escape hatch), and only clears selection on a second press.\n if (event.key === 'Escape' && this.#touchActive) {\n this.exitTouchSelection();\n return true;\n }\n\n // Escape clears selection in all modes\n if (event.key === 'Escape') return this.#keyClearSelection(mode);\n\n const isNavKey = NAV_KEYS.includes(event.key);\n switch (mode) {\n case 'cell':\n return isNavKey ? this.#keyNavCellMode() : false;\n case 'row':\n return this.#keyRowMode(event);\n case 'range':\n return this.#keyRangeMode(event, isNavKey);\n default:\n return false;\n }\n }\n\n /**\n * Column-axis keyboard chords (only reachable when `columnEnabled`):\n * - **Ctrl/⌘+Space** — WAI-ARIA Grid: toggle column selection for the focused\n * column. Per spec the key is `' '`; some older hosts report `'Spacebar'`.\n * - **Ctrl/⌘+Shift+ArrowLeft/Right** — extend column selection along the\n * visible columns.\n *\n * @returns `true` when the chord was consumed.\n */\n #keyColumnAxis(event: KeyboardEvent): boolean {\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\n if (!ctrlOrMeta) return false;\n // Editors keep their own Ctrl+Space / Ctrl+Shift+Arrow (word selection) behaviour.\n if (isFormField(event.target) || this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n\n if (event.key === ' ' || event.key === 'Spacebar') {\n const column = this.visibleColumns[this.grid._focusCol];\n if (!column || isUtilityColumn(column) || typeof column.field !== 'string') return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectColumn(column.field, { toggle: true });\n return true;\n }\n\n if (\n event.shiftKey &&\n this.activeAxis === 'column' &&\n this.config.multiSelect !== false &&\n (event.key === 'ArrowLeft' || event.key === 'ArrowRight')\n ) {\n const fields = selectableColumnFields(this.visibleColumns);\n const direction = event.key === 'ArrowLeft' ? 'left' : 'right';\n const newHead = computeKeyboardExtension(this.columnHead, fields, direction);\n if (newHead === null || this.columnAnchor === null) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectColumn(newHead, { range: true });\n return true;\n }\n\n return false;\n }\n\n /** Escape: clear the active axis. Defers to EditingPlugin while an edit is open. */\n #keyClearSelection(mode: SelectionMode): boolean {\n // If editing is active, let the EditingPlugin cancel the active edit first\n if (this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n\n // Column axis — clear it only; a single Escape clears the *active* axis when\n // both axes are configured.\n if (this.activeAxis === 'column') {\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n this.activeAxis = 'none';\n } else if (mode === 'cell') {\n this.selectedCell = null;\n } else if (mode === 'row') {\n this.selected.clear();\n this.anchor = null;\n } else if (mode === 'range') {\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /** CELL MODE: selection follows focus (but respects selectability). */\n #keyNavCellMode(): boolean {\n // Use queueMicrotask so grid's handler runs first and updates focusRow/focusCol\n queueMicrotask(() => {\n const focusRow = this.grid._focusRow;\n const focusCol = this.grid._focusCol;\n // Only select if the cell is selectable; clear when navigating to a non-selectable cell\n this.selectedCell = this.isCellSelectable(focusRow, focusCol) ? { row: focusRow, col: focusCol } : null;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n });\n return false; // Let grid handle navigation\n }\n\n /** ROW MODE: Arrow/Page/Ctrl+Home/End move selection, Shift extends, Ctrl+A selects all. */\n #keyRowMode(event: KeyboardEvent): boolean {\n const multiSelect = this.config.multiSelect !== false;\n const isRowNavKey =\n event.key === 'ArrowUp' ||\n event.key === 'ArrowDown' ||\n event.key === 'PageUp' ||\n event.key === 'PageDown' ||\n ((event.ctrlKey || event.metaKey) && (event.key === 'Home' || event.key === 'End'));\n\n if (isRowNavKey) {\n const shiftKey = event.shiftKey && multiSelect;\n\n // Set anchor SYNCHRONOUSLY before grid moves focus\n if (shiftKey && this.anchor === null) this.anchor = this.grid._focusRow;\n\n // Mark explicit selection SYNCHRONOUSLY so #syncSelectionToFocus\n // won't overwrite the anchor if afterRender fires before our update\n this.explicitSelection = true;\n\n // Store pending update — processed in afterRender when grid has updated focusRow\n this.pendingRowKeyUpdate = { shiftKey };\n\n // Schedule afterRender (grid's refreshVirtualWindow(false) may skip it)\n queueMicrotask(() => this.requestAfterRender());\n return false; // Let grid handle navigation\n }\n\n return this.#keySelectAll(event, multiSelect);\n }\n\n /**\n * RANGE MODE: Shift+Arrow extends, plain Arrow resets, Ctrl+A selects all.\n * Tab always navigates without extending (even with Shift).\n */\n #keyRangeMode(event: KeyboardEvent, isNavKey: boolean): boolean {\n if (!isNavKey) return this.#keyRangeSpan(event) || this.#keySelectAll(event, this.config.multiSelect !== false);\n\n const shouldExtend = event.shiftKey && event.key !== 'Tab';\n\n // Capture anchor BEFORE grid moves focus (synchronous)\n // This ensures the anchor is the starting point, not the destination\n if (shouldExtend && !this.cellAnchor) {\n this.cellAnchor = { row: this.grid._focusRow, col: this.grid._focusCol };\n }\n\n // Mark pending update - will be processed in afterRender when grid updates focus\n this.pendingKeyboardUpdate = { shiftKey: shouldExtend };\n\n // Schedule afterRender to run after grid's keyboard handler completes.\n // Grid's refreshVirtualWindow(false) skips afterRender for performance,\n // so we explicitly request it to process pendingKeyboardUpdate.\n queueMicrotask(() => this.requestAfterRender());\n\n return false; // Let grid handle navigation\n }\n\n /**\n * WAI-ARIA APG grid chords in range mode:\n * - **Shift+Space** — select the row(s) spanned by the active range (or the focused row).\n * - **Ctrl/⌘+Space** — select the spanned column(s); only when the column axis is not\n * configured (otherwise {@link #keyColumnAxis} owns it).\n *\n * Skipped for form-field targets and while an edit is open so editors still type spaces.\n */\n #keyRangeSpan(event: KeyboardEvent): boolean {\n if (event.key !== ' ' && event.key !== 'Spacebar') return false;\n const ctrlOrMeta = event.ctrlKey || event.metaKey;\n let axis: 'row' | 'column';\n if (event.shiftKey && !ctrlOrMeta) axis = 'row';\n else if (ctrlOrMeta && !event.shiftKey && !this.#mode.columnEnabled) axis = 'column';\n else return false;\n if (event.altKey) return false;\n if (isFormField(event.target)) return false;\n if (this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n\n const rowCount = this.rows.length;\n const colCount = this.visibleColumns.length;\n if (rowCount === 0 || colCount === 0) return false;\n\n const focus = { row: this.grid._focusRow, col: this.grid._focusCol };\n const span = this.activeRange\n ? normalizeRange(this.activeRange)\n : { startRow: focus.row, startCol: focus.col, endRow: focus.row, endCol: focus.col };\n const range: InternalCellRange =\n axis === 'row'\n ? { startRow: span.startRow, startCol: 0, endRow: span.endRow, endCol: colCount - 1 }\n : { startRow: 0, startCol: span.startCol, endRow: rowCount - 1, endCol: span.endCol };\n\n event.preventDefault();\n event.stopPropagation();\n\n // Anchor on the corner farthest from focus so a following Shift+Arrow keeps the far edge.\n const anchor = {\n row: Math.abs(focus.row - range.startRow) >= Math.abs(focus.row - range.endRow) ? range.startRow : range.endRow,\n col: Math.abs(focus.col - range.startCol) >= Math.abs(focus.col - range.endCol) ? range.startCol : range.endCol,\n };\n this.ranges = [range];\n this.activeRange = range;\n this.#spanRange = { axis, range };\n this.cellAnchor = anchor;\n this.#extendAnchor = anchor;\n this.#pendingSpanAnnouncement = {\n axis,\n count: axis === 'row' ? range.endRow - range.startRow + 1 : range.endCol - range.startCol + 1,\n };\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /** Ctrl/⌘+A: select all (skipped while editing, and when single-select). */\n #keySelectAll(event: KeyboardEvent, multiSelect: boolean): boolean {\n if (!multiSelect || event.key !== 'a' || !(event.ctrlKey || event.metaKey)) return false;\n if (this.grid.query<boolean>('isEditing').some(Boolean)) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectAll();\n return true;\n }\n\n /** @internal */\n override onCellMouseDown(event: CellMouseEvent): boolean | void {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n // ROW MODE — touch selection entry (#304).\n //\n // `event-delegation.ts` only dispatches `mousedown` for a coarse pointer\n // *after* a 400 ms long-press has elapsed, so reaching here with a coarse\n // event already means \"the user held the row down\". Header presses are\n // excluded — they belong to the column header menu (#270).\n if (this.#mode.primary === 'row') {\n if (event.rowIndex === undefined || event.rowIndex < 0) return;\n if (this.config.multiSelect === false) return;\n if (!this.#isCoarseEvent(event.originalEvent)) return;\n if (event.column && isUtilityColumn(event.column)) return;\n if (this.#touchActive) {\n this.#extendTouchSelection(event.rowIndex);\n return true;\n }\n return this.#enterTouchSelection(event.rowIndex) || undefined;\n }\n\n if (this.#mode.primary !== 'range') return;\n if (event.rowIndex === undefined || event.colIndex === undefined) return;\n if (event.rowIndex < 0) return; // Header\n\n // A tapped corner handle owns the next press: swallow it so no drag starts\n // and the range is not reset before `onCellClick` places the corner.\n if (this.#armedCorner) return true;\n\n this.#rangeFromCoarsePointer = this.#isCoarseEvent(event.originalEvent);\n\n // Right-click (secondary button) inside an existing range must NOT clear the\n // selection: this lets the context menu act on the whole range without the\n // user having to hold Ctrl. Outside any range, fall through to the normal\n // behavior (clear + select the clicked cell so the menu targets it).\n if (event.originalEvent.button === 2 && isCellInAnyRange(event.rowIndex, event.colIndex, this.ranges)) {\n return; // keep selection, don't start a drag\n }\n\n // Skip utility columns (expander columns, etc.)\n // event.column is already resolved from _visibleColumns in the event builder\n if (event.column && isUtilityColumn(event.column)) {\n return; // Don't start selection on utility columns\n }\n\n // Skip non-selectable cells - don't start drag from them\n if (!this.isCellSelectable(event.rowIndex, event.colIndex)) {\n return;\n }\n\n // Let onCellClick handle shift+click for range extension\n if (event.originalEvent.shiftKey && this.cellAnchor) {\n return;\n }\n\n // Start drag selection\n this.isDragging = true;\n const rowIndex = event.rowIndex;\n const colIndex = event.colIndex;\n\n // When multiSelect is false, Ctrl+click starts a new single range instead of adding\n const ctrlKey = (event.originalEvent.ctrlKey || event.originalEvent.metaKey) && this.config.multiSelect !== false;\n\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n\n // Check if selection is actually changing (for non-Ctrl clicks)\n if (!ctrlKey && this.ranges.length === 1 && rangesEqual(this.ranges[0], newRange)) {\n // Same cell already selected, just update anchor for potential drag\n this.cellAnchor = { row: rowIndex, col: colIndex };\n this.#setExtendAnchor(event, rowIndex, colIndex);\n return true;\n }\n\n this.cellAnchor = { row: rowIndex, col: colIndex };\n this.#setExtendAnchor(event, rowIndex, colIndex);\n\n if (!ctrlKey) {\n this.ranges = [];\n }\n\n this.ranges.push(newRange);\n this.activeRange = newRange;\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /**\n * Remember where a *primary* press anchored the range. A secondary press is\n * deliberately ignored: right-clicking outside the range re-anchors\n * {@link cellAnchor} so the menu targets the clicked cell, and reusing that\n * would make \"Extend selection to here\" extend from itself.\n */\n #setExtendAnchor(event: CellMouseEvent, row: number, col: number): void {\n if (event.originalEvent.button === 2) return;\n this.#extendAnchor = { row, col };\n }\n\n /** @internal */\n override onCellMouseMove(event: CellMouseEvent): boolean | void {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n if (this.#mode.primary !== 'range') return;\n if (!this.isDragging || !this.cellAnchor) return;\n if (event.rowIndex === undefined || event.colIndex === undefined) return;\n if (event.rowIndex < 0) return;\n\n // When dragging, clamp to first data column (skip utility columns)\n // colIndex from events is a visible-column index (from data-col)\n let targetCol = event.colIndex;\n const column = this.visibleColumns[targetCol];\n if (column && isUtilityColumn(column)) {\n // Find the first non-utility visible column\n const firstDataCol = this.visibleColumns.findIndex((col) => !isUtilityColumn(col));\n if (firstDataCol >= 0) {\n targetCol = firstDataCol;\n }\n }\n\n const newRange = createRangeFromAnchor(this.cellAnchor, { row: event.rowIndex, col: targetCol });\n\n // Only update and emit if the range actually changed\n const currentRange = this.ranges.length > 0 ? this.ranges[this.ranges.length - 1] : null;\n if (currentRange && rangesEqual(currentRange, newRange)) {\n return true; // Range unchanged, no need to update\n }\n\n if (this.ranges.length > 0) {\n this.ranges[this.ranges.length - 1] = newRange;\n } else {\n this.ranges.push(newRange);\n }\n this.activeRange = newRange;\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n return true;\n }\n\n /** @internal */\n override onCellMouseUp(_event: CellMouseEvent): boolean | void {\n // Skip all selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n if (this.#mode.primary !== 'range') return;\n if (this.isDragging) {\n this.isDragging = false;\n return true;\n }\n }\n\n /**\n * Header click handler — drives column-axis selection.\n *\n * - Plain click (and plain Shift+click): defer to MultiSort / core sort by\n * returning `false`. Header-click sort behavior is unchanged.\n * - **Ctrl/Cmd+click**: toggle selection of the clicked column (or replace,\n * when `multiSelect: false`).\n * - **Ctrl/Cmd+Shift+click**: extend selection from the column anchor to the\n * clicked column. Avoids the plain `Shift+click` chord owned by MultiSort.\n *\n * No-ops when column selection isn't enabled or when the clicked column is\n * a utility column (`utility: true`).\n * @internal\n */\n override onHeaderClick(event: HeaderClickEvent): boolean | void {\n if (!this.isSelectionEnabled()) return false;\n if (!this.#mode.columnEnabled) return false;\n if (event.column && isUtilityColumn(event.column)) return false;\n\n const e = event.originalEvent;\n const ctrlKey = e.ctrlKey || e.metaKey;\n if (!ctrlKey) return false; // Plain / Shift click → sort path\n\n const field = event.field;\n if (!field) return false;\n\n e.preventDefault();\n if (typeof e.stopPropagation === 'function') e.stopPropagation();\n\n const range = e.shiftKey === true && this.config.multiSelect !== false && this.columnAnchor !== null;\n this.selectColumn(field, range ? { range: true } : { toggle: true });\n return true; // Handled — suppress sort\n }\n\n // #region Checkbox Column\n\n /**\n * Inject checkbox column when `checkbox: true` and mode is `'row'`.\n * @internal\n */\n override processColumns(columns: ColumnConfig[]): ColumnConfig[] {\n if (this.config.checkbox && this.#mode.primary === 'row') {\n // Check if checkbox column already exists\n if (columns.some((col) => col.field === CHECKBOX_COLUMN_FIELD)) {\n return columns;\n }\n const checkboxCol = this.#createCheckboxColumn();\n // Insert after the expander or any leading left-pinned columns.\n const expanderIdx = columns.findIndex(isExpanderColumn);\n const firstUnpinnedIdx = columns.findIndex(\n (column) => (column as ColumnConfig & { pinned?: string }).pinned !== 'left',\n );\n const leftPinnedEnd = firstUnpinnedIdx < 0 ? columns.length : firstUnpinnedIdx;\n const insertAt = expanderIdx >= 0 ? Math.max(expanderIdx + 1, leftPinnedEnd) : leftPinnedEnd;\n return [...columns.slice(0, insertAt), checkboxCol, ...columns.slice(insertAt)];\n }\n return columns;\n }\n\n /**\n * Create the checkbox utility column configuration.\n */\n #createCheckboxColumn(): ColumnConfig & { pinned: 'left' } {\n return {\n field: CHECKBOX_COLUMN_FIELD,\n header: '',\n width: 32,\n pinned: 'left',\n resizable: false,\n sortable: false,\n lockPosition: true,\n utility: true,\n checkboxColumn: true,\n headerRenderer: () => {\n // A label, not a div: it forwards the pointer to the checkbox natively,\n // so the target is the whole 32px cell without inflating the box (SC 2.5.8).\n const container = document.createElement('label');\n container.className = 'tbw-checkbox-header';\n // Hide \"select all\" checkbox in single-select mode\n if (this.config.multiSelect === false) return container;\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-select-all-checkbox';\n // The wrapping label names the checkbox implicitly AND gives the\n // otherwise-empty `columnheader` cell screen-reader-visible text.\n const labelText = document.createElement('span');\n labelText.className = 'tbw-sr-only';\n labelText.textContent = getA11yMessage(this.gridElement, 'selectAllRows');\n checkbox.addEventListener('click', (e) => {\n e.stopPropagation(); // Prevent header sort\n if ((e.target as HTMLInputElement).checked) {\n this.selectAll();\n } else {\n this.clearSelection();\n }\n });\n // The label's own click keeps bubbling after it forwards to the\n // checkbox; stop it so the header cell sees nothing.\n container.addEventListener('click', (e) => e.stopPropagation());\n container.append(checkbox, labelText);\n return container;\n },\n renderer: (ctx) => {\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-select-row-checkbox';\n // Set initial checked state from current selection\n const cellEl = ctx.cellEl;\n const rowIndex = cellEl ? parseInt(cellEl.getAttribute('data-row') ?? '-1', 10) : -1;\n if (rowIndex >= 0) {\n checkbox.checked = this.selected.has(rowIndex);\n }\n checkbox.setAttribute('aria-label', getA11yMessage(this.gridElement, 'selectRow', Math.max(rowIndex, 0)));\n return checkbox;\n },\n };\n }\n\n /**\n * Update checkbox checked states to reflect current selection.\n * Called from #applySelectionClasses.\n */\n #updateCheckboxStates(gridEl: HTMLElement): void {\n // Update row checkboxes\n const rowCheckboxes = gridEl.querySelectorAll('.tbw-select-row-checkbox') as NodeListOf<HTMLInputElement>;\n rowCheckboxes.forEach((checkbox) => {\n const cell = checkbox.closest('.cell');\n const rowIndex = cell ? getRowIndexFromCell(cell) : -1;\n if (rowIndex >= 0) {\n checkbox.checked = this.selected.has(rowIndex);\n }\n });\n\n // Update header select-all checkbox\n const headerCheckbox = gridEl.querySelector('.tbw-select-all-checkbox') as HTMLInputElement | null;\n if (headerCheckbox) {\n const rowCount = this.rows.length;\n let selectableCount = 0;\n if (this.config.isSelectable) {\n for (let i = 0; i < rowCount; i++) {\n if (this.isRowSelectable(i)) selectableCount++;\n }\n } else {\n selectableCount = rowCount;\n }\n const allSelected = selectableCount > 0 && this.selected.size >= selectableCount;\n const someSelected = this.selected.size > 0;\n headerCheckbox.checked = allSelected;\n headerCheckbox.indeterminate = someSelected && !allSelected;\n }\n }\n\n // #endregion\n\n /**\n * True when focus is currently inside the grid host (data-has-focus is set on focusin).\n * Used to determine whether selection should follow the focus cursor.\n */\n #gridHasFocus(): boolean {\n return this.gridElement?.hasAttribute('data-has-focus') ?? false;\n }\n\n /**\n * Sync selection state to the grid's current focus position.\n * In row mode, keeps `selected` in sync with `_focusRow`.\n * In cell mode, keeps `selectedCell` in sync with `_focusRow`/`_focusCol`.\n * Only updates when the focus has changed since the last sync.\n * Skips when `explicitSelection` is set (click/keyboard set selection directly).\n *\n * Issue #392: selection must be a deliberate user action. The grid's focus\n * cursor defaults to (0,0) even before focus enters the grid, so following it\n * unconditionally auto-selects row 0 / cell 0,0 on first render. Only follow\n * focus when focus is genuinely inside the grid. Always advance the synced\n * markers so the resting position is treated as already-synced (tabbing in and\n * landing on the default cell must not select it).\n */\n #syncSelectionToFocus(mode: string): void {\n const focusRow = this.grid._focusRow;\n const focusCol = this.grid._focusCol;\n\n // Only follow focus when the grid actually has focus. Otherwise, keep the\n // synced markers in step so the resting position is not retroactively selected\n // when focus enters the grid later.\n if (!this.#gridHasFocus()) {\n this.lastSyncedFocusRow = focusRow;\n this.lastSyncedFocusCol = focusCol;\n return;\n }\n\n if (mode === 'row') {\n // Skip auto-sync when selection was explicitly set (Shift/Ctrl click, keyboard)\n if (this.explicitSelection) {\n this.explicitSelection = false;\n this.lastSyncedFocusRow = focusRow;\n return;\n }\n\n if (focusRow !== this.lastSyncedFocusRow) {\n this.lastSyncedFocusRow = focusRow;\n if (this.isRowSelectable(focusRow)) {\n if (!this.selected.has(focusRow) || this.selected.size !== 1) {\n this.selected.clear();\n this.selected.add(focusRow);\n this.lastSelected = focusRow;\n this.anchor = focusRow;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n }\n }\n }\n\n if (mode === 'cell') {\n if (this.explicitSelection) {\n this.explicitSelection = false;\n this.lastSyncedFocusRow = focusRow;\n this.lastSyncedFocusCol = focusCol;\n return;\n }\n\n if (focusRow !== this.lastSyncedFocusRow || focusCol !== this.lastSyncedFocusCol) {\n this.lastSyncedFocusRow = focusRow;\n this.lastSyncedFocusCol = focusCol;\n if (this.isCellSelectable(focusRow, focusCol)) {\n const cur = this.selectedCell;\n if (!cur || cur.row !== focusRow || cur.col !== focusCol) {\n this.selectedCell = { row: focusRow, col: focusCol };\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n }\n }\n }\n }\n\n /**\n * Apply CSS selection classes to row/cell elements.\n * Shared by afterRender and onScrollRender.\n */\n #applySelectionClasses(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const mode = this.#mode.primary;\n const columnEnabled = this.#mode.columnEnabled;\n const hasSelectableCallback = !!this.config.isSelectable;\n\n // Reflect multi-select capability on the role=grid element so screen readers\n // announce the grid as multi-selectable. WAI-ARIA requires aria-multiselectable\n // on the element carrying role=\"grid\" — that's `.rows-body` in our render tree,\n // not the host. `multiSelect` defaults to true; only `false` opts out.\n const rowsBodyEl = gridEl.querySelector('.rows-body');\n if (rowsBodyEl) {\n const multi = this.config.multiSelect !== false;\n rowsBodyEl.setAttribute('aria-multiselectable', multi ? 'true' : 'false');\n }\n\n // Clear all selection classes first (including column-selected).\n // Also reset stale aria-selected on cells that previously carried a\n // selection marker — range/column passes set aria-selected=\"true\" on\n // selected cells, and core only updates the attribute when focus\n // changes — so without an explicit reset a cell that lost selection\n // (axis flipped, range shrank, virtualization recycled the node into a\n // non-selected position) would keep the stale value. We MUST scope the\n // reset to cells that had `.selected` / `.column-selected` BEFORE the\n // class wipe — clearing aria-selected unconditionally would also blow\n // away the focused-cell marker that core's keyboard handler / row\n // renderer set for the active cell, regressing keyboard a11y.\n // Header aria-selected is handled in the columnEnabled block below.\n const allCells = gridEl.querySelectorAll('.cell');\n allCells.forEach((cell) => {\n const hadSelectionMarker =\n cell.classList.contains(GridClasses.SELECTED) || cell.classList.contains('column-selected');\n cell.classList.remove(GridClasses.SELECTED, 'top', 'bottom', 'first', 'last', 'column-selected');\n if (hadSelectionMarker) {\n cell.removeAttribute('aria-selected');\n }\n // Clear selectable attribute - will be re-applied below\n if (hasSelectableCallback) {\n cell.removeAttribute('data-selectable');\n }\n });\n\n const allRows = gridEl.querySelectorAll('.data-grid-row');\n allRows.forEach((row) => {\n row.classList.remove(GridClasses.SELECTED, 'row-focus');\n row.setAttribute('aria-selected', 'false');\n // Clear selectable attribute - will be re-applied below\n if (hasSelectableCallback) {\n row.removeAttribute('data-selectable');\n }\n });\n\n // Clear column-selected from header cells too\n if (columnEnabled) {\n const headerCells = gridEl.querySelectorAll('.header-row > .cell');\n headerCells.forEach((cell) => {\n cell.classList.remove('column-selected');\n cell.removeAttribute('aria-selected');\n });\n }\n\n // ROW MODE: Add row-focus class to selected rows, disable cell-focus, update checkboxes\n if (mode === 'row') {\n // In row mode, disable ALL cell-focus styling - row selection takes precedence\n clearCellFocus(gridEl);\n\n allRows.forEach((row) => {\n const firstCell = row.querySelector('.cell[data-row]');\n const rowIndex = getRowIndexFromCell(firstCell);\n if (rowIndex >= 0) {\n // Mark non-selectable rows\n if (hasSelectableCallback && !this.isRowSelectable(rowIndex)) {\n row.setAttribute('data-selectable', 'false');\n }\n if (this.selected.has(rowIndex)) {\n row.classList.add(GridClasses.SELECTED, 'row-focus');\n row.setAttribute('aria-selected', 'true');\n }\n }\n });\n\n // Update checkbox states if checkbox column is enabled\n if (this.config.checkbox) {\n this.#updateCheckboxStates(gridEl);\n }\n }\n\n // CELL/RANGE MODE: Mark non-selectable cells\n if ((mode === 'cell' || mode === 'range') && hasSelectableCallback) {\n const cells = gridEl.querySelectorAll('.cell[data-row][data-col]');\n cells.forEach((cell) => {\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) {\n if (!this.isCellSelectable(rowIndex, colIndex)) {\n cell.setAttribute('data-selectable', 'false');\n }\n }\n });\n }\n\n // RANGE MODE: Add selected and edge classes to cells\n // Uses neighbor-based edge detection for correct multi-range borders\n if (mode === 'range' && this.ranges.length > 0) {\n // Clear all cell-focus first - selection plugin manages focus styling in range mode\n clearCellFocus(gridEl);\n\n // Pre-normalize ranges for efficient neighbor checks\n const normalizedRanges = this.ranges.map(normalizeRange);\n\n // Fast selection check against pre-normalized ranges\n const isInSelection = (r: number, c: number): boolean => {\n for (const range of normalizedRanges) {\n if (r >= range.startRow && r <= range.endRow && c >= range.startCol && c <= range.endCol) {\n return true;\n }\n }\n return false;\n };\n\n const cells = gridEl.querySelectorAll('.cell[data-row][data-col]');\n cells.forEach((cell) => {\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) {\n // Skip utility columns entirely - don't add any selection classes\n // colIndex from data-col is a visible-column index\n const column = this.visibleColumns[colIndex];\n if (column && isUtilityColumn(column)) {\n return;\n }\n\n if (isInSelection(rowIndex, colIndex)) {\n cell.classList.add(GridClasses.SELECTED);\n cell.setAttribute('aria-selected', 'true');\n\n // Edge detection: add border class where neighbor is not selected\n // This handles single ranges, multi-range, and irregular selections correctly\n if (!isInSelection(rowIndex - 1, colIndex)) cell.classList.add('top');\n if (!isInSelection(rowIndex + 1, colIndex)) cell.classList.add('bottom');\n if (!isInSelection(rowIndex, colIndex - 1)) cell.classList.add('first');\n if (!isInSelection(rowIndex, colIndex + 1)) cell.classList.add('last');\n }\n }\n });\n }\n\n // CELL MODE: Let the grid's native .cell-focus styling handle cell highlighting\n // No additional action needed - the grid already manages focus styling\n\n // COLUMN AXIS: Apply column-selected class + aria-selected to the header cell\n // and every data cell in the matching column. Identifies columns by their\n // visible-index (data-col matches the visibleColumns position) so it works\n // regardless of pinning / reordering — selectedColumns stores fields, not\n // indices, so the rendering layer resolves them per-render.\n if (columnEnabled && this.selectedColumns.size > 0) {\n // Build visible-index → field map once for O(1) lookups.\n const colFieldByIndex: (string | undefined)[] = this.visibleColumns.map((c) =>\n typeof c.field === 'string' ? c.field : undefined,\n );\n\n // Header cells — the grid renders header cells with data-col attributes.\n const headerCells = gridEl.querySelectorAll<HTMLElement>('.header-row > .cell[data-col]');\n headerCells.forEach((cell) => {\n const colIndex = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n const field = colIndex >= 0 ? colFieldByIndex[colIndex] : undefined;\n if (field && this.selectedColumns.has(field)) {\n cell.classList.add('column-selected');\n cell.setAttribute('aria-selected', 'true');\n }\n });\n\n // Data cells.\n const cells = gridEl.querySelectorAll<HTMLElement>('.cell[data-col]:not(.header-row .cell)');\n cells.forEach((cell) => {\n // Skip header cells (filter above isn't reliable for nested .header-row scoping)\n if (cell.closest('.header-row')) return;\n const colIndex = parseInt(cell.getAttribute('data-col') ?? '-1', 10);\n if (colIndex < 0) return;\n const column = this.visibleColumns[colIndex];\n if (!column || isUtilityColumn(column)) return;\n const field = colFieldByIndex[colIndex];\n if (field && this.selectedColumns.has(field)) {\n cell.classList.add('column-selected');\n cell.setAttribute('aria-selected', 'true');\n }\n });\n }\n }\n\n /** @internal */\n override afterRender(): void {\n // Skip rendering selection if disabled at grid level or plugin level\n if (!this.isSelectionEnabled()) return;\n\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const container = gridEl.querySelector('.tbw-grid-root');\n const mode = this.#mode.primary;\n\n // Process pending row keyboard navigation update (row mode)\n // This runs AFTER the grid has updated focusRow\n if (this.pendingRowKeyUpdate && mode === 'row') {\n const { shiftKey } = this.pendingRowKeyUpdate;\n this.pendingRowKeyUpdate = null;\n\n const focusRow = this.grid._focusRow;\n\n if (shiftKey && this.anchor !== null) {\n // Shift+nav: Extend selection from anchor to new focus\n this.selected.clear();\n const start = Math.min(this.anchor, focusRow);\n const end = Math.max(this.anchor, focusRow);\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) {\n this.selected.add(i);\n }\n }\n } else {\n // Plain nav: Single select\n if (this.isRowSelectable(focusRow)) {\n this.selected.clear();\n this.selected.add(focusRow);\n this.anchor = focusRow;\n } else {\n this.selected.clear();\n }\n }\n\n this.lastSelected = focusRow;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n\n // Process pending keyboard navigation update (range mode)\n // This runs AFTER the grid has updated focusRow/focusCol\n if (this.pendingKeyboardUpdate && mode === 'range') {\n const { shiftKey } = this.pendingKeyboardUpdate;\n this.pendingKeyboardUpdate = null;\n\n const currentRow = this.grid._focusRow;\n const currentCol = this.grid._focusCol;\n\n if (shiftKey && this.cellAnchor) {\n // Extend selection from anchor to current focus\n let newRange = createRangeFromAnchor(this.cellAnchor, { row: currentRow, col: currentCol });\n const span = this.#spanRange;\n if (span && span.range === this.activeRange) {\n newRange =\n span.axis === 'row'\n ? { ...newRange, startCol: 0, endCol: this.visibleColumns.length - 1 }\n : { ...newRange, startRow: 0, endRow: this.rows.length - 1 };\n span.range = newRange;\n }\n this.ranges = [newRange];\n this.activeRange = newRange;\n } else if (!shiftKey) {\n // Without shift, clear selection (cell-focus will show instead)\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = { row: currentRow, col: currentCol }; // Reset anchor to current position\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n }\n\n // Sync selection to grid's focus position.\n // This ensures selection follows keyboard navigation (Tab, arrows, etc.)\n // regardless of which plugin moved the focus.\n this.#syncSelectionToFocus(mode);\n\n // Set data attribute on host for CSS variable scoping\n this.gridElement.setAttribute('data-selection-mode', mode);\n\n // Toggle .selecting class during drag to prevent text selection\n if (container) {\n container.classList.toggle('selecting', this.isDragging);\n }\n\n this.#applySelectionClasses();\n this.#renderTouchChrome(container as HTMLElement | null);\n }\n\n /**\n * Called after scroll-triggered row rendering.\n * Reapplies selection classes to recycled DOM elements.\n * @internal\n */\n override onScrollRender(): void {\n // Skip rendering selection classes if disabled\n if (!this.isSelectionEnabled()) return;\n\n this.#applySelectionClasses();\n }\n\n // #endregion\n\n // #region Public API\n\n /**\n * Get the current selection as a unified result.\n * Works for all selection modes and always returns ranges.\n *\n * @example\n * ```ts\n * const selection = plugin.getSelection();\n * if (selection.ranges.length > 0) {\n * const { from, to } = selection.ranges[0];\n * // For cell mode: from === to (single cell)\n * // For row mode: from.col = 0, to.col = lastCol (full row)\n * // For range mode: rectangular selection\n * }\n * ```\n */\n getSelection(): SelectionResult {\n const event = this.#buildEvent();\n return {\n mode: this.config.mode,\n activeAxis: event.activeAxis,\n ranges: event.ranges,\n selectedColumns: event.selectedColumns,\n anchor: this.cellAnchor,\n };\n }\n\n /**\n * Get all selected cells across all ranges.\n */\n getSelectedCells(): Array<{ row: number; col: number }> {\n return getAllCellsInRanges(this.ranges);\n }\n\n /**\n * Check if a specific cell is in range selection.\n */\n isCellSelected(row: number, col: number): boolean {\n return isCellInAnyRange(row, col, this.ranges);\n }\n\n /**\n * Select all selectable rows (row mode) or all cells (range mode).\n *\n * In row mode, selects every row where `isSelectable` returns true (or all rows if no callback).\n * In range mode, creates a single range spanning all rows and visible columns.\n * Array modes resolve to their row/range axis. Has no effect in cell mode.\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * plugin.selectAll(); // Selects everything in current mode\n * ```\n */\n selectAll(): void {\n const mode = this.#mode.primary;\n\n // Single-select mode: selectAll is a no-op\n if (this.config.multiSelect === false) return;\n\n if (mode === 'row') {\n this.selected.clear();\n for (let i = 0; i < this.rows.length; i++) {\n if (this.isRowSelectable(i)) {\n this.selected.add(i);\n }\n }\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n } else if (mode === 'range') {\n const rowCount = this.rows.length;\n const colCount = this.visibleColumns.length;\n if (rowCount > 0 && colCount > 0) {\n const allRange: InternalCellRange = {\n startRow: 0,\n startCol: 0,\n endRow: rowCount - 1,\n endCol: colCount - 1,\n };\n this.ranges = [allRange];\n this.activeRange = allRange;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n }\n }\n\n /**\n * Select specific rows by index (row mode only).\n * Replaces the current selection with the provided row indices.\n * Indices that are out of bounds or fail the `isSelectable` check are ignored.\n *\n * @param indices - Array of row indices to select\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * plugin.selectRows([0, 2, 4]); // Select rows 0, 2, and 4\n * ```\n */\n selectRows(indices: number[]): void {\n if (this.#mode.primary !== 'row') return;\n // In single-select mode, only use the last index\n const effectiveIndices =\n this.config.multiSelect === false && indices.length > 1 ? [indices[indices.length - 1]] : indices;\n this.selected.clear();\n for (const idx of effectiveIndices) {\n if (idx >= 0 && idx < this.rows.length && this.isRowSelectable(idx)) {\n this.selected.add(idx);\n }\n }\n this.anchor = effectiveIndices.length > 0 ? effectiveIndices[effectiveIndices.length - 1] : null;\n this.explicitSelection = true;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Get the indices of all selected rows (convenience for row mode).\n * Returns indices sorted in ascending order.\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * const rows = plugin.getSelectedRowIndices(); // [0, 2, 4]\n * ```\n */\n getSelectedRowIndices(): number[] {\n return [...this.selected].sort((a, b) => a - b);\n }\n\n /**\n * Get the actual row objects for the current selection.\n *\n * Works across all selection modes:\n * - **Row mode**: Returns the row objects for all selected rows.\n * - **Cell mode**: Returns the single row containing the selected cell, or `[]`.\n * - **Range mode**: Returns the unique row objects that intersect any selected range.\n *\n * Row objects are resolved from the grid's processed (sorted/filtered) row array,\n * so they always reflect the current visual order.\n *\n * @example\n * ```ts\n * const plugin = grid.getPluginByName('selection');\n * const selected = plugin.getSelectedRows(); // [{ id: 1, name: 'Alice' }, ...]\n * ```\n */\n getSelectedRows<T = unknown>(): T[] {\n const mode = this.#mode.primary;\n const rows = this.rows;\n\n if (mode === 'row') {\n return this.getSelectedRowIndices()\n .filter((i) => i >= 0 && i < rows.length)\n .map((i) => rows[i]) as T[];\n }\n\n if (mode === 'cell' && this.selectedCell) {\n const { row } = this.selectedCell;\n return row >= 0 && row < rows.length ? [rows[row] as T] : [];\n }\n\n if (mode === 'range' && this.ranges.length > 0) {\n // Collect unique row indices across all ranges\n const rowIndices = new Set<number>();\n for (const range of this.ranges) {\n const minRow = Math.max(0, Math.min(range.startRow, range.endRow));\n const maxRow = Math.min(rows.length - 1, Math.max(range.startRow, range.endRow));\n for (let r = minRow; r <= maxRow; r++) {\n rowIndices.add(r);\n }\n }\n return [...rowIndices].sort((a, b) => a - b).map((i) => rows[i]) as T[];\n }\n\n return [];\n }\n\n /**\n * Replace the entire column-axis selection with `fields` in a single\n * batched operation. Filters out unknown / utility / hidden fields against\n * the current visible columns, updates `selectedColumns` once, sets the\n * anchor/head to the last accepted field (or null when empty), emits\n * `selection-change` exactly once, and schedules a single render.\n *\n * Used by the `selectColumns` plugin query so external callers wiring\n * many fields at once don't pay N×emit + N×requestAfterRender.\n */\n #setColumnSelection(fields: readonly string[]): void {\n if (!this.#mode.columnEnabled) return;\n\n const visible = selectableColumnFields(this.visibleColumns);\n const allowed = new Set(visible);\n // Preserve caller order but de-duplicate and filter unknowns/utility cols.\n const accepted: string[] = [];\n const seen = new Set<string>();\n for (const f of fields) {\n if (!allowed.has(f) || seen.has(f)) continue;\n seen.add(f);\n accepted.push(f);\n }\n\n this.#enforceMutualExclusion('column');\n\n this.selectedColumns.clear();\n for (const f of accepted) this.selectedColumns.add(f);\n const last = accepted.length > 0 ? accepted[accepted.length - 1] : null;\n this.columnAnchor = last;\n this.columnHead = last;\n this.activeAxis = this.selectedColumns.size > 0 ? 'column' : 'none';\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Toggle, add, or replace a column in the column-axis selection.\n *\n * Column selection is identified by **field name**, so it survives column\n * pinning, reordering, and virtualization recycling. The column must be\n * present in the grid's visible columns and must not be a utility column.\n *\n * Only available when `mode` includes `'column'`. With `multiSelect: false`,\n * `range`/`toggle` options are ignored and the call always replaces the\n * current selection with the single column.\n *\n * @param field - The column field name to select.\n * @param options.range - When true and a `columnAnchor` exists, selects\n * every column from anchor to `field` inclusive (Ctrl+Shift+Click semantics).\n * @param options.toggle - When true, removes `field` if already selected;\n * otherwise adds it. Without `toggle`, plain calls replace the selection.\n * @since 2.8.0\n */\n selectColumn(field: string, options: { range?: boolean; toggle?: boolean } = {}): void {\n if (!this.#mode.columnEnabled) return;\n const fields = selectableColumnFields(this.visibleColumns);\n if (!fields.includes(field)) return;\n\n this.#enforceMutualExclusion('column');\n\n const multiSelect = this.config.multiSelect !== false;\n if (!multiSelect) {\n this.selectedColumns.clear();\n this.selectedColumns.add(field);\n this.columnAnchor = field;\n this.columnHead = field;\n } else if (options.range && this.columnAnchor) {\n const range = fieldsBetween(this.columnAnchor, field, fields);\n this.selectedColumns.clear();\n for (const f of range) this.selectedColumns.add(f);\n this.columnHead = field;\n } else if (options.toggle) {\n if (this.selectedColumns.has(field)) {\n this.selectedColumns.delete(field);\n } else {\n this.selectedColumns.add(field);\n }\n this.columnAnchor = field;\n this.columnHead = field;\n } else {\n this.selectedColumns.clear();\n this.selectedColumns.add(field);\n this.columnAnchor = field;\n this.columnHead = field;\n }\n\n this.activeAxis = this.selectedColumns.size > 0 ? 'column' : 'none';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Remove a column from the column-axis selection. No-op if `field` isn't selected.\n * @since 2.8.0\n */\n deselectColumn(field: string): void {\n if (!this.#mode.columnEnabled) return;\n if (!this.selectedColumns.delete(field)) return;\n if (this.selectedColumns.size === 0) {\n this.columnAnchor = null;\n this.columnHead = null;\n if (this.activeAxis === 'column') this.activeAxis = 'none';\n }\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Select every selectable column. No-op when `multiSelect: false` or column\n * mode isn't enabled.\n * @since 2.8.0\n */\n selectAllColumns(): void {\n if (!this.#mode.columnEnabled) return;\n if (this.config.multiSelect === false) return;\n const fields = selectableColumnFields(this.visibleColumns);\n if (fields.length === 0) return;\n this.#enforceMutualExclusion('column');\n this.selectedColumns.clear();\n for (const f of fields) this.selectedColumns.add(f);\n this.columnAnchor = fields[0];\n this.columnHead = fields[fields.length - 1];\n this.activeAxis = 'column';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Clear column-axis selection only. Leaves any row/cell/range selection intact.\n * @since 2.8.0\n */\n clearColumnSelection(): void {\n if (this.selectedColumns.size === 0) return;\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n if (this.activeAxis === 'column') this.activeAxis = 'none';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Get the field names of all currently selected columns, in visible-column\n * order. Returns an empty array when the column axis is inactive or empty.\n * @since 2.8.0\n */\n getSelectedColumns(): readonly string[] {\n if (this.selectedColumns.size === 0) return [];\n const fields = selectableColumnFields(this.visibleColumns);\n return fields.filter((f) => this.selectedColumns.has(f));\n }\n\n /**\n * Clear all selection (every axis).\n */\n clearSelection(): void {\n this.selectedCell = null;\n this.selected.clear();\n this.anchor = null;\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n this.activeAxis = 'none';\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n /**\n * Set selected ranges programmatically.\n */\n setRanges(ranges: CellRange[]): void {\n this.ranges = ranges.map((r) => ({\n startRow: r.from.row,\n startCol: r.from.col,\n endRow: r.to.row,\n endCol: r.to.col,\n }));\n this.activeRange = this.ranges.length > 0 ? this.ranges[this.ranges.length - 1] : null;\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Private Helpers\n\n #buildEvent(): SelectionChangeDetail {\n // Derive the axis from current state so all existing in-row code paths\n // (which mutate state then emit) report the correct axis without needing\n // to pre-set `this.activeAxis` themselves. Mutual exclusion is enforced\n // separately in {@link #enforceMutualExclusion}.\n const primary = this.#mode.primary;\n const inRowPopulated =\n (primary === 'row' && this.selected.size > 0) ||\n (primary === 'cell' && this.selectedCell !== null) ||\n (primary === 'range' && this.ranges.length > 0);\n\n // Mutual exclusion: in `bothAxes` mode, the user just mutated the in-row\n // axis (we know because `#buildEvent` is called from the in-row code paths\n // immediately after state changes) — so clear stale column selection. The\n // column-axis code paths call `#enforceMutualExclusion('column')`\n // explicitly BEFORE mutating; they then update `activeAxis` to 'column'\n // so the in-row state seen here is empty and this branch doesn't trigger.\n if (this.#mode.bothAxes && inRowPopulated && this.selectedColumns.size > 0) {\n this.selectedColumns.clear();\n this.columnAnchor = null;\n this.columnHead = null;\n if (this.gridElement) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionAxisChanged', 'row'));\n }\n }\n\n let axis: SelectionAxis;\n if (inRowPopulated) {\n // In-row state always wins when populated — column-axis paths clear\n // in-row state via #enforceMutualExclusion before mutating columns.\n if (primary === 'row') axis = 'row';\n else if (primary === 'cell') axis = 'cell';\n else axis = 'range';\n } else if (this.selectedColumns.size > 0) {\n axis = 'column';\n } else {\n axis = 'none';\n }\n this.activeAxis = axis;\n\n const event = buildSelectionEvent(\n this.config.mode,\n axis,\n primary,\n {\n selectedCell: this.selectedCell,\n selected: this.selected,\n ranges: this.ranges,\n selectedColumns: this.selectedColumns,\n },\n this.columns.length,\n selectableColumnFields(this.visibleColumns),\n );\n // Debounced screen reader announcement for selection changes\n const span = this.#pendingSpanAnnouncement;\n this.#pendingSpanAnnouncement = null;\n if (this.announceTimer) clearTimeout(this.announceTimer);\n this.announceTimer = setTimeout(() => {\n if (span) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'rangeSpanSelected', span.axis, span.count));\n } else if (event.activeAxis === 'column') {\n const cols = event.selectedColumns;\n if (cols.length === 1) {\n const field = cols[0];\n const col = this.columns.find((c) => c.field === field);\n const label = (typeof col?.header === 'string' && col.header) || field;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'columnSelected', label));\n } else if (cols.length > 1) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'columnSelectionChanged', cols.length));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'columnSelectionCleared'));\n }\n } else {\n const count = event.activeAxis === 'row' ? this.selected.size : event.ranges.length;\n if (count > 0) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionChanged', count));\n }\n }\n }, 150);\n return event;\n }\n\n /**\n * Enforce row↔column mutual exclusion when both axes are configured\n * (`mode: ['row', 'column']` etc.). Call BEFORE mutating the destination\n * axis so its data won't be wiped along with the inactive axis.\n *\n * Single-string-mode configs are no-ops (the inactive axis can't have data\n * because there's no UI path to populate it). The in-row → column path is\n * called explicitly by the column-axis code; the column → in-row path is\n * handled automatically inside {@link #buildEvent} (which runs on every\n * in-row state change immediately before emitting).\n */\n #enforceMutualExclusion(toAxis: 'row' | 'cell' | 'range' | 'column'): void {\n if (!this.#mode.bothAxes) return;\n if (toAxis === 'column') {\n const hadInRow = this.selected.size > 0 || this.selectedCell !== null || this.ranges.length > 0;\n if (!hadInRow) return;\n this.selected.clear();\n this.lastSelected = null;\n this.anchor = null;\n this.selectedCell = null;\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n if (this.gridElement) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionAxisChanged', 'column'));\n }\n }\n // Column → in-row flip is handled inside #buildEvent (auto-detected by\n // observing populated in-row state with stale columns).\n }\n\n // #endregion\n}\n"],"mappings":"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"}
|