@toolbox-web/grid 2.7.1 → 2.7.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"selection.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/selection/range-selection.ts","../../../../../libs/grid/src/lib/plugins/selection/SelectionPlugin.ts"],"sourcesContent":["/**\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 (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 { clearCellFocus, getRowIndexFromCell } from '../../core/internal/utils';\nimport type { GridElement, 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 {\n createRangeFromAnchor,\n getAllCellsInRanges,\n isCellInAnyRange,\n normalizeRange,\n rangesEqual,\n toPublicRanges,\n} from './range-selection';\nimport styles from './selection.css?inline';\nimport type {\n CellRange,\n InternalCellRange,\n SelectionChangeDetail,\n SelectionConfig,\n SelectionMode,\n SelectionResult,\n} from './types';\n\n/** Special field name for the selection checkbox column */\nconst CHECKBOX_COLUMN_FIELD = '__tbw_checkbox';\n\n/**\n * Build the selection change event detail for the current state.\n */\nfunction buildSelectionEvent(\n mode: SelectionMode,\n state: {\n selectedCell: { row: number; col: number } | null;\n selected: Set<number>;\n ranges: InternalCellRange[];\n },\n colCount: number,\n): SelectionChangeDetail {\n if (mode === 'cell' && state.selectedCell) {\n return {\n mode,\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 };\n }\n\n if (mode === '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, ranges };\n }\n\n if (mode === 'range' && state.ranges.length > 0) {\n return { mode, ranges: toPublicRanges(state.ranges) };\n }\n\n return { mode, ranges: [] };\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 */\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 ],\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) => config.mode === 'range' && config.triggerOn === 'dblclick',\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 };\n }\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 /** 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\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 // 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\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 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.selected.clear();\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.isDragging = false;\n this.selectedCell = null;\n this.pendingKeyboardUpdate = null;\n this.pendingRowKeyUpdate = null;\n this.lastSyncedFocusRow = -1;\n this.lastSyncedFocusCol = -1;\n }\n\n /**\n * Clear selection without emitting an event.\n * Used when selection is invalidated by external changes (filtering, grouping, etc.)\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 this.requestAfterRender();\n }\n\n // #endregion\n\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 const { rowIndex, colIndex, originalEvent } = event;\n const { mode, triggerOn = 'click' } = this.config;\n\n // Skip if event type doesn't match configured trigger\n // This allows dblclick mode to only select on double-click\n if (originalEvent.type !== triggerOn) {\n return false;\n }\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 const column = event.column;\n const isUtility = column && isUtilityColumn(column);\n\n // CELL MODE: Single cell selection - skip utility columns and non-selectable cells\n if (mode === 'cell') {\n if (isUtility) {\n return false; // Allow event to propagate, but don't select utility cells\n }\n if (!this.isCellSelectable(rowIndex, colIndex)) {\n return false; // Cell is not selectable\n }\n // Only emit if selection actually changed\n const currentCell = this.selectedCell;\n if (currentCell && currentCell.row === rowIndex && currentCell.col === colIndex) {\n return false; // Same cell already selected\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 if (mode === 'row') {\n if (!this.isRowSelectable(rowIndex)) {\n return false; // Row is not selectable\n }\n\n const multiSelect = this.config.multiSelect !== false;\n const shiftKey = originalEvent.shiftKey && multiSelect;\n const ctrlKey = (originalEvent.ctrlKey || originalEvent.metaKey) && 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) {\n this.selected.clear();\n }\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) {\n this.selected.add(i);\n }\n }\n } else if (ctrlKey || (isCheckbox && multiSelect)) {\n // Ctrl+Click or checkbox click: Toggle individual row\n if (this.selected.has(rowIndex)) {\n this.selected.delete(rowIndex);\n } else {\n this.selected.add(rowIndex);\n }\n this.anchor = rowIndex;\n } else {\n // Plain click (or any click when multiSelect is false): select only clicked row\n if (this.selected.size === 1 && this.selected.has(rowIndex)) {\n return false; // Same row already selected\n }\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 selection, click starts new\n if (mode === 'range') {\n // Skip utility columns in range mode - don't start selection from them\n if (isUtility) {\n return false;\n }\n\n // Skip non-selectable cells in range mode\n if (!this.isCellSelectable(rowIndex, colIndex)) {\n return false;\n }\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)) {\n return false; // Same range already selected\n }\n\n if (ctrlKey) {\n if (this.ranges.length > 0) {\n this.ranges[this.ranges.length - 1] = newRange;\n } else {\n this.ranges.push(newRange);\n }\n } else {\n this.ranges = [newRange];\n }\n this.activeRange = newRange;\n } else if (ctrlKey) {\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n this.ranges.push(newRange);\n this.activeRange = newRange;\n this.cellAnchor = { row: rowIndex, col: colIndex };\n } else {\n // Plain click - check if same single-cell range already selected\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n\n // Only emit if selection actually changed\n if (this.ranges.length === 1 && rangesEqual(this.ranges[0], newRange)) {\n return false; // Same cell already selected\n }\n\n this.ranges = [newRange];\n this.activeRange = newRange;\n this.cellAnchor = { row: rowIndex, col: colIndex };\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n\n this.requestAfterRender();\n return false;\n }\n\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 const { mode } = this.config;\n const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Home', 'End', 'PageUp', 'PageDown'];\n const isNavKey = navKeys.includes(event.key);\n\n // Escape clears selection in all modes\n // But if editing is active, let the EditingPlugin handle Escape first\n if (event.key === 'Escape') {\n const isEditing = this.grid.query<boolean>('isEditing');\n if (isEditing.some(Boolean)) {\n return false; // Defer to EditingPlugin to cancel the active edit\n }\n\n 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 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 if (mode === 'cell' && isNavKey) {\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\n if (this.isCellSelectable(focusRow, focusCol)) {\n this.selectedCell = { row: focusRow, col: focusCol };\n } else {\n // Clear selection when navigating to non-selectable cell\n this.selectedCell = null;\n }\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/Home/End keys move selection, Shift extends, Ctrl+A selects all\n if (mode === 'row') {\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) {\n this.anchor = this.grid._focusRow;\n }\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 // Ctrl+A: Select all rows (skip when editing, skip when single-select)\n if (multiSelect && event.key === 'a' && (event.ctrlKey || event.metaKey)) {\n const isEditing = this.grid.query<boolean>('isEditing');\n if (isEditing.some(Boolean)) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectAll();\n return true;\n }\n }\n\n // RANGE MODE: Shift+Arrow extends, plain Arrow resets\n // Tab key always navigates without extending (even with Shift)\n if (mode === 'range' && isNavKey) {\n // Tab should not extend selection - it just navigates to the next/previous cell\n const isTabKey = event.key === 'Tab';\n const shouldExtend = event.shiftKey && !isTabKey;\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 // Ctrl+A selects all in range mode (skip when editing, skip when single-select)\n if (\n mode === 'range' &&\n this.config.multiSelect !== false &&\n event.key === 'a' &&\n (event.ctrlKey || event.metaKey)\n ) {\n const isEditing = this.grid.query<boolean>('isEditing');\n if (isEditing.some(Boolean)) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectAll();\n return true;\n }\n\n return false;\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 if (this.config.mode !== 'range') return;\n if (event.rowIndex === undefined || event.colIndex === undefined) return;\n if (event.rowIndex < 0) return; // Header\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 return true;\n }\n\n this.cellAnchor = { row: rowIndex, col: 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 /** @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.config.mode !== '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.config.mode !== 'range') return;\n if (this.isDragging) {\n this.isDragging = false;\n return true;\n }\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.config.mode === '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 const container = document.createElement('div');\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 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 container.appendChild(checkbox);\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 if (cellEl) {\n const rowIndex = parseInt(cellEl.getAttribute('data-row') ?? '-1', 10);\n if (rowIndex >= 0) {\n checkbox.checked = this.selected.has(rowIndex);\n }\n }\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 * 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 #syncSelectionToFocus(mode: string): void {\n const focusRow = this.grid._focusRow;\n const focusCol = this.grid._focusCol;\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.config;\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\n const allCells = gridEl.querySelectorAll('.cell');\n allCells.forEach((cell) => {\n cell.classList.remove(GridClasses.SELECTED, 'top', 'bottom', 'first', 'last');\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 // 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\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.config;\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 const newRange = createRangeFromAnchor(this.cellAnchor, { row: currentRow, col: currentCol });\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 }\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 return {\n mode: this.config.mode,\n ranges: this.#buildEvent().ranges,\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 columns.\n * 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, multiSelect } = this.config;\n\n // Single-select mode: selectAll is a no-op\n if (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.columns.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.config.mode !== '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.config;\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 * Clear all selection.\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.emit<SelectionChangeDetail>('selection-change', { mode: this.config.mode, ranges: [] });\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', {\n mode: this.config.mode,\n ranges: toPublicRanges(this.ranges),\n });\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Private Helpers\n\n #buildEvent(): SelectionChangeDetail {\n const event = buildSelectionEvent(\n this.config.mode,\n {\n selectedCell: this.selectedCell,\n selected: this.selected,\n ranges: this.ranges,\n },\n this.columns.length,\n );\n // Debounced screen reader announcement for selection changes\n if (this.announceTimer) clearTimeout(this.announceTimer);\n this.announceTimer = setTimeout(() => {\n const count = event.mode === 'row' ? this.selected.size : event.ranges.length;\n if (count > 0) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionChanged', count));\n }\n }, 150);\n return event;\n }\n\n // #endregion\n}\n"],"names":["normalizeRange","range","startRow","Math","min","endRow","startCol","endCol","max","toPublicRange","normalized","from","row","col","to","toPublicRanges","ranges","map","isCellInAnyRange","some","isCellInRange","getCellsInRange","cells","push","createRangeFromAnchor","anchor","current","rangesEqual","a","b","normA","normB","CHECKBOX_COLUMN_FIELD","SelectionPlugin","BaseGridPlugin","static","queries","type","description","configRules","id","severity","message","check","config","mode","triggerOn","name","styles","defaultConfig","enabled","multiSelect","selected","Set","lastSelected","activeRange","cellAnchor","isDragging","pendingKeyboardUpdate","pendingRowKeyUpdate","selectedCell","lastSyncedFocusRow","lastSyncedFocusCol","announceTimer","explicitSelection","isSelectionEnabled","this","grid","effectiveConfig","selectable","checkSelectable","rowIndex","colIndex","isSelectable","rows","visibleColumns","isRowSelectable","isCellSelectable","attach","super","on","clearSelectionSilent","handleQuery","query","getSelection","getSelectedRowIndices","getSelectedRows","selectRows","context","detach","rowsBodyEl","gridElement","querySelector","removeAttribute","clear","requestAfterRender","onCellClick","event","originalEvent","column","isUtility","isUtilityColumn","currentCell","emit","buildEvent","shiftKey","ctrlKey","metaKey","isCheckbox","checkboxColumn","start","end","i","add","has","delete","size","newRange","currentRange","length","onKeyDown","isNavKey","includes","key","Boolean","queueMicrotask","focusRow","_focusRow","focusCol","_focusCol","preventDefault","stopPropagation","selectAll","isTabKey","shouldExtend","onCellMouseDown","onCellMouseMove","targetCol","firstDataCol","findIndex","onCellMouseUp","_event","processColumns","columns","checkbox","field","checkboxCol","createCheckboxColumn","expanderIdx","isExpanderColumn","insertAt","slice","header","width","resizable","sortable","lockPosition","utility","headerRenderer","container","document","createElement","className","addEventListener","e","target","checked","clearSelection","appendChild","renderer","ctx","cellEl","parseInt","getAttribute","updateCheckboxStates","gridEl","querySelectorAll","forEach","cell","closest","getRowIndexFromCell","headerCheckbox","rowCount","selectableCount","allSelected","someSelected","indeterminate","syncSelectionToFocus","cur","applySelectionClasses","hasSelectableCallback","multi","setAttribute","classList","remove","GridClasses","SELECTED","allRows","clearCellFocus","firstCell","normalizedRanges","isInSelection","r","c","afterRender","currentRow","currentCol","toggle","onScrollRender","getSelectedCells","cellMap","Map","set","values","getAllCellsInRanges","isCellSelected","colCount","allRange","indices","effectiveIndices","idx","sort","filter","rowIndices","minRow","maxRow","setRanges","state","sorted","buildSelectionEvent","clearTimeout","setTimeout","count","announce","getA11yMessage"],"mappings":"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"}
1
+ {"version":3,"file":"selection.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/selection/range-selection.ts","../../../../../libs/grid/src/lib/plugins/selection/SelectionPlugin.ts"],"sourcesContent":["/**\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 (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 { clearCellFocus, getRowIndexFromCell } from '../../core/internal/utils';\nimport type { GridElement, 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 {\n createRangeFromAnchor,\n getAllCellsInRanges,\n isCellInAnyRange,\n normalizeRange,\n rangesEqual,\n toPublicRanges,\n} from './range-selection';\nimport styles from './selection.css?inline';\nimport type {\n CellRange,\n InternalCellRange,\n SelectionChangeDetail,\n SelectionConfig,\n SelectionMode,\n SelectionResult,\n} from './types';\n\n/** Special field name for the selection checkbox column */\nconst CHECKBOX_COLUMN_FIELD = '__tbw_checkbox';\n\n/**\n * Build the selection change event detail for the current state.\n */\nfunction buildSelectionEvent(\n mode: SelectionMode,\n state: {\n selectedCell: { row: number; col: number } | null;\n selected: Set<number>;\n ranges: InternalCellRange[];\n },\n colCount: number,\n): SelectionChangeDetail {\n if (mode === 'cell' && state.selectedCell) {\n return {\n mode,\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 };\n }\n\n if (mode === '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, ranges };\n }\n\n if (mode === 'range' && state.ranges.length > 0) {\n return { mode, ranges: toPublicRanges(state.ranges) };\n }\n\n return { mode, ranges: [] };\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 */\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 ],\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) => config.mode === 'range' && config.triggerOn === 'dblclick',\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 };\n }\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 /** 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\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 // 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.config.mode !== '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\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 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.selected.clear();\n this.ranges = [];\n this.activeRange = null;\n this.cellAnchor = null;\n this.isDragging = false;\n this.selectedCell = null;\n this.pendingKeyboardUpdate = null;\n this.pendingRowKeyUpdate = null;\n this.lastSyncedFocusRow = -1;\n this.lastSyncedFocusCol = -1;\n }\n\n /**\n * Clear selection without emitting an event.\n * Used when selection is invalidated by external changes (filtering, grouping, etc.)\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 this.requestAfterRender();\n }\n\n // #endregion\n\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 const { rowIndex, colIndex, originalEvent } = event;\n const { mode, triggerOn = 'click' } = this.config;\n\n // Skip if event type doesn't match configured trigger\n // This allows dblclick mode to only select on double-click\n if (originalEvent.type !== triggerOn) {\n return false;\n }\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 const column = event.column;\n const isUtility = column && isUtilityColumn(column);\n\n // CELL MODE: Single cell selection - skip utility columns and non-selectable cells\n if (mode === 'cell') {\n if (isUtility) {\n return false; // Allow event to propagate, but don't select utility cells\n }\n if (!this.isCellSelectable(rowIndex, colIndex)) {\n return false; // Cell is not selectable\n }\n // Only emit if selection actually changed\n const currentCell = this.selectedCell;\n if (currentCell && currentCell.row === rowIndex && currentCell.col === colIndex) {\n return false; // Same cell already selected\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 if (mode === 'row') {\n if (!this.isRowSelectable(rowIndex)) {\n return false; // Row is not selectable\n }\n\n const multiSelect = this.config.multiSelect !== false;\n const shiftKey = originalEvent.shiftKey && multiSelect;\n const ctrlKey = (originalEvent.ctrlKey || originalEvent.metaKey) && 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) {\n this.selected.clear();\n }\n for (let i = start; i <= end; i++) {\n if (this.isRowSelectable(i)) {\n this.selected.add(i);\n }\n }\n } else if (ctrlKey || (isCheckbox && multiSelect)) {\n // Ctrl+Click or checkbox click: Toggle individual row\n if (this.selected.has(rowIndex)) {\n this.selected.delete(rowIndex);\n } else {\n this.selected.add(rowIndex);\n }\n this.anchor = rowIndex;\n } else {\n // Plain click (or any click when multiSelect is false): select only clicked row\n if (this.selected.size === 1 && this.selected.has(rowIndex)) {\n return false; // Same row already selected\n }\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 selection, click starts new\n if (mode === 'range') {\n // Skip utility columns in range mode - don't start selection from them\n if (isUtility) {\n return false;\n }\n\n // Skip non-selectable cells in range mode\n if (!this.isCellSelectable(rowIndex, colIndex)) {\n return false;\n }\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)) {\n return false; // Same range already selected\n }\n\n if (ctrlKey) {\n if (this.ranges.length > 0) {\n this.ranges[this.ranges.length - 1] = newRange;\n } else {\n this.ranges.push(newRange);\n }\n } else {\n this.ranges = [newRange];\n }\n this.activeRange = newRange;\n } else if (ctrlKey) {\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n this.ranges.push(newRange);\n this.activeRange = newRange;\n this.cellAnchor = { row: rowIndex, col: colIndex };\n } else {\n // Plain click - check if same single-cell range already selected\n const newRange: InternalCellRange = {\n startRow: rowIndex,\n startCol: colIndex,\n endRow: rowIndex,\n endCol: colIndex,\n };\n\n // Only emit if selection actually changed\n if (this.ranges.length === 1 && rangesEqual(this.ranges[0], newRange)) {\n return false; // Same cell already selected\n }\n\n this.ranges = [newRange];\n this.activeRange = newRange;\n this.cellAnchor = { row: rowIndex, col: colIndex };\n }\n\n this.emit<SelectionChangeDetail>('selection-change', this.#buildEvent());\n\n this.requestAfterRender();\n return false;\n }\n\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 const { mode } = this.config;\n const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Home', 'End', 'PageUp', 'PageDown'];\n const isNavKey = navKeys.includes(event.key);\n\n // Escape clears selection in all modes\n // But if editing is active, let the EditingPlugin handle Escape first\n if (event.key === 'Escape') {\n const isEditing = this.grid.query<boolean>('isEditing');\n if (isEditing.some(Boolean)) {\n return false; // Defer to EditingPlugin to cancel the active edit\n }\n\n 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 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 if (mode === 'cell' && isNavKey) {\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\n if (this.isCellSelectable(focusRow, focusCol)) {\n this.selectedCell = { row: focusRow, col: focusCol };\n } else {\n // Clear selection when navigating to non-selectable cell\n this.selectedCell = null;\n }\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/Home/End keys move selection, Shift extends, Ctrl+A selects all\n if (mode === 'row') {\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) {\n this.anchor = this.grid._focusRow;\n }\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 // Ctrl+A: Select all rows (skip when editing, skip when single-select)\n if (multiSelect && event.key === 'a' && (event.ctrlKey || event.metaKey)) {\n const isEditing = this.grid.query<boolean>('isEditing');\n if (isEditing.some(Boolean)) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectAll();\n return true;\n }\n }\n\n // RANGE MODE: Shift+Arrow extends, plain Arrow resets\n // Tab key always navigates without extending (even with Shift)\n if (mode === 'range' && isNavKey) {\n // Tab should not extend selection - it just navigates to the next/previous cell\n const isTabKey = event.key === 'Tab';\n const shouldExtend = event.shiftKey && !isTabKey;\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 // Ctrl+A selects all in range mode (skip when editing, skip when single-select)\n if (\n mode === 'range' &&\n this.config.multiSelect !== false &&\n event.key === 'a' &&\n (event.ctrlKey || event.metaKey)\n ) {\n const isEditing = this.grid.query<boolean>('isEditing');\n if (isEditing.some(Boolean)) return false;\n event.preventDefault();\n event.stopPropagation();\n this.selectAll();\n return true;\n }\n\n return false;\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 if (this.config.mode !== 'range') return;\n if (event.rowIndex === undefined || event.colIndex === undefined) return;\n if (event.rowIndex < 0) return; // Header\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 return true;\n }\n\n this.cellAnchor = { row: rowIndex, col: 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 /** @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.config.mode !== '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.config.mode !== 'range') return;\n if (this.isDragging) {\n this.isDragging = false;\n return true;\n }\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.config.mode === '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 const container = document.createElement('div');\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 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 container.appendChild(checkbox);\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 if (cellEl) {\n const rowIndex = parseInt(cellEl.getAttribute('data-row') ?? '-1', 10);\n if (rowIndex >= 0) {\n checkbox.checked = this.selected.has(rowIndex);\n }\n }\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 * 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 #syncSelectionToFocus(mode: string): void {\n const focusRow = this.grid._focusRow;\n const focusCol = this.grid._focusCol;\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.config;\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\n const allCells = gridEl.querySelectorAll('.cell');\n allCells.forEach((cell) => {\n cell.classList.remove(GridClasses.SELECTED, 'top', 'bottom', 'first', 'last');\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 // 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\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.config;\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 const newRange = createRangeFromAnchor(this.cellAnchor, { row: currentRow, col: currentCol });\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 }\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 return {\n mode: this.config.mode,\n ranges: this.#buildEvent().ranges,\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 columns.\n * 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, multiSelect } = this.config;\n\n // Single-select mode: selectAll is a no-op\n if (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.columns.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.config.mode !== '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.config;\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 * Clear all selection.\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.emit<SelectionChangeDetail>('selection-change', { mode: this.config.mode, ranges: [] });\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', {\n mode: this.config.mode,\n ranges: toPublicRanges(this.ranges),\n });\n this.requestAfterRender();\n }\n\n // #endregion\n\n // #region Private Helpers\n\n #buildEvent(): SelectionChangeDetail {\n const event = buildSelectionEvent(\n this.config.mode,\n {\n selectedCell: this.selectedCell,\n selected: this.selected,\n ranges: this.ranges,\n },\n this.columns.length,\n );\n // Debounced screen reader announcement for selection changes\n if (this.announceTimer) clearTimeout(this.announceTimer);\n this.announceTimer = setTimeout(() => {\n const count = event.mode === 'row' ? this.selected.size : event.ranges.length;\n if (count > 0) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'selectionChanged', count));\n }\n }, 150);\n return event;\n }\n\n // #endregion\n}\n"],"names":["normalizeRange","range","startRow","Math","min","endRow","startCol","endCol","max","toPublicRange","normalized","from","row","col","to","toPublicRanges","ranges","map","isCellInAnyRange","some","isCellInRange","getCellsInRange","cells","push","createRangeFromAnchor","anchor","current","rangesEqual","a","b","normA","normB","CHECKBOX_COLUMN_FIELD","SelectionPlugin","BaseGridPlugin","static","queries","type","description","configRules","id","severity","message","check","config","mode","triggerOn","name","styles","defaultConfig","enabled","multiSelect","selected","Set","lastSelected","activeRange","cellAnchor","isDragging","pendingKeyboardUpdate","pendingRowKeyUpdate","selectedCell","lastSyncedFocusRow","lastSyncedFocusCol","announceTimer","explicitSelection","isSelectionEnabled","this","grid","effectiveConfig","selectable","checkSelectable","rowIndex","colIndex","isSelectable","rows","visibleColumns","isRowSelectable","isCellSelectable","attach","super","on","clearSelectionSilent","has","clear","add","emit","buildEvent","requestAfterRender","lastSourceRowCount","addEventListener","event","sourceRowCount","detail","hasSelection","size","length","signal","disconnectSignal","handleQuery","query","getSelection","getSelectedRowIndices","getSelectedRows","selectRows","context","detach","rowsBodyEl","gridElement","querySelector","removeAttribute","onCellClick","originalEvent","column","isUtility","isUtilityColumn","currentCell","shiftKey","ctrlKey","metaKey","isCheckbox","checkboxColumn","start","end","i","delete","newRange","currentRange","onKeyDown","isNavKey","includes","key","Boolean","queueMicrotask","focusRow","_focusRow","focusCol","_focusCol","preventDefault","stopPropagation","selectAll","isTabKey","shouldExtend","onCellMouseDown","onCellMouseMove","targetCol","firstDataCol","findIndex","onCellMouseUp","_event","processColumns","columns","checkbox","field","checkboxCol","createCheckboxColumn","expanderIdx","isExpanderColumn","insertAt","slice","header","width","resizable","sortable","lockPosition","utility","headerRenderer","container","document","createElement","className","e","target","checked","clearSelection","appendChild","renderer","ctx","cellEl","parseInt","getAttribute","updateCheckboxStates","gridEl","querySelectorAll","forEach","cell","closest","getRowIndexFromCell","headerCheckbox","rowCount","selectableCount","allSelected","someSelected","indeterminate","syncSelectionToFocus","cur","applySelectionClasses","hasSelectableCallback","multi","setAttribute","classList","remove","GridClasses","SELECTED","allRows","clearCellFocus","firstCell","normalizedRanges","isInSelection","r","c","afterRender","currentRow","currentCol","toggle","onScrollRender","getSelectedCells","cellMap","Map","set","values","getAllCellsInRanges","isCellSelected","colCount","allRange","indices","effectiveIndices","idx","sort","filter","rowIndices","minRow","maxRow","setRanges","state","sorted","buildSelectionEvent","clearTimeout","setTimeout","count","announce","getA11yMessage"],"mappings":"2oBAeO,SAASA,EAAeC,GAC7B,MAAO,CACLC,SAAUC,KAAKC,IAAIH,EAAMC,SAAUD,EAAMI,QACzCC,SAAUH,KAAKC,IAAIH,EAAMK,SAAUL,EAAMM,QACzCF,OAAQF,KAAKK,IAAIP,EAAMC,SAAUD,EAAMI,QACvCE,OAAQJ,KAAKK,IAAIP,EAAMK,SAAUL,EAAMM,QAE3C,CAQO,SAASE,EAAcR,GAC5B,MAAMS,EAAaV,EAAeC,GAClC,MAAO,CACLU,KAAM,CAAEC,IAAKF,EAAWR,SAAUW,IAAKH,EAAWJ,UAClDQ,GAAI,CAAEF,IAAKF,EAAWL,OAAQQ,IAAKH,EAAWH,QAElD,CAQO,SAASQ,EAAeC,GAC7B,OAAOA,EAAOC,IAAIR,EACpB,CAyBO,SAASS,EAAiBN,EAAaC,EAAaG,GACzD,OAAOA,EAAOG,KAAMlB,GAhBf,SAAuBW,EAAaC,EAAaZ,GACtD,MAAMS,EAAaV,EAAeC,GAClC,OACEW,GAAOF,EAAWR,UAAYU,GAAOF,EAAWL,QAAUQ,GAAOH,EAAWJ,UAAYO,GAAOH,EAAWH,MAE9G,CAWgCa,CAAcR,EAAKC,EAAKZ,GACxD,CAQO,SAASoB,EAAgBpB,GAC9B,MAAMqB,EAA6C,GAC7CZ,EAAaV,EAAeC,GAElC,IAAA,IAASW,EAAMF,EAAWR,SAAUU,GAAOF,EAAWL,OAAQO,IAC5D,IAAA,IAASC,EAAMH,EAAWJ,SAAUO,GAAOH,EAAWH,OAAQM,IAC5DS,EAAMC,KAAK,CAAEX,MAAKC,QAItB,OAAOS,CACT,CA0CO,SAASE,EACdC,EACAC,GAEA,MAAO,CACLxB,SAAUuB,EAAOb,IACjBN,SAAUmB,EAAOZ,IACjBR,OAAQqB,EAAQd,IAChBL,OAAQmB,EAAQb,IAEpB,CAsBO,SAASc,EAAYC,EAAsBC,GAChD,MAAMC,EAAQ9B,EAAe4B,GACvBG,EAAQ/B,EAAe6B,GAC7B,OACEC,EAAM5B,WAAa6B,EAAM7B,UACzB4B,EAAMxB,WAAayB,EAAMzB,UACzBwB,EAAMzB,SAAW0B,EAAM1B,QACvByB,EAAMvB,SAAWwB,EAAMxB,MAE3B,OC3IMyB,EAAwB,iBAqIvB,MAAMC,UAAwBC,EAAAA,eAKnCC,gBAAqE,CACnEC,QAAS,CACP,CAAEC,KAAM,eAAgBC,YAAa,mCACrC,CAAED,KAAM,aAAcC,YAAa,iDACnC,CAAED,KAAM,wBAAyBC,YAAa,4CAC9C,CAAED,KAAM,kBAAmBC,YAAa,0EAE1CC,YAAa,CACX,CACEC,GAAI,2BACJC,SAAU,OACVC,QACE,iOAGFC,MAAQC,GAA2B,UAAhBA,EAAOC,MAAyC,aAArBD,EAAOE,aAMlDC,KAAO,YAEEC,0yGAGlB,iBAAuBC,GACrB,MAAO,CACLJ,KAAM,OACNC,UAAW,QACXI,SAAS,EACTC,aAAa,EAEjB,CAIQC,aAAeC,IACfC,aAA8B,KAC9B7B,OAAwB,KAGxBT,OAA8B,GAC9BuC,YAAwC,KACxCC,WAAkD,KAClDC,YAAa,EAGbC,sBAAsD,KAGtDC,oBAAoD,KAGpDC,aAAoD,KAGpDC,oBAAqB,EAErBC,oBAAqB,EAGrBC,cAAsD,KAGtDC,mBAAoB,EAUpB,kBAAAC,GAEN,OAA4B,IAAxBC,KAAKtB,OAAOM,UAEiC,IAA1CgB,KAAKC,KAAKC,iBAAiBC,UACpC,CAUQ,eAAAC,CAAgBC,EAAkBC,GACxC,MAAMC,aAAEA,GAAiBP,KAAKtB,OAC9B,IAAK6B,EAAc,OAAO,EAE1B,MAAM7D,EAAMsD,KAAKQ,KAAKH,GACtB,IAAK3D,EAAK,OAAO,EAIjB,OAAO6D,EAAa7D,EAAK2D,OADG,IAAbC,EAAyBN,KAAKS,eAAeH,QAAY,EAC7BA,EAC7C,CAKQ,eAAAI,CAAgBL,GACtB,OAAOL,KAAKI,gBAAgBC,EAC9B,CAKQ,gBAAAM,CAAiBN,EAAkBC,GACzC,OAAON,KAAKI,gBAAgBC,EAAUC,EACxC,CAOS,MAAAM,CAAOX,GACdY,MAAMD,OAAOX,GAIbD,KAAKc,GAAG,gBAAiB,IAAMd,KAAKe,wBACpCf,KAAKc,GAAG,eAAgB,IAAMd,KAAKe,wBACnCf,KAAKc,GAAG,cAAe,IAAMd,KAAKe,wBAClCf,KAAKc,GAAG,cAAe,IAAMd,KAAKe,wBAUlCf,KAAKc,GAAuC,YAAa,EAAGT,WAAU3D,UAC/DsD,KAAKD,uBACC,MAAPrD,GAAe2D,EAAW,GACL,QAArBL,KAAKtB,OAAOC,MACXqB,KAAKU,gBAAgBL,KACtBL,KAAKd,SAAS8B,IAAIX,MAGU,IAA5BL,KAAKtB,OAAOO,aACde,KAAKd,SAAS+B,QAEhBjB,KAAKd,SAASgC,IAAIb,GAClBL,KAAKZ,aAAeiB,EACpBL,KAAKzC,OAAS8C,EACdL,KAAKF,mBAAoB,EACzBE,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,0BAQP,IAAIC,GAAqB,EACzBrB,EAAKsB,iBACH,cACEC,IACA,MAAMC,eAAEA,GAAmBD,EAAME,OAC3BC,EAAe3B,KAAKd,SAAS0C,KAAO,GAAK5B,KAAKlD,OAAO+E,OAAS,GAA2B,OAAtB7B,KAAKN,cACnD,IAAvB4B,GAA6BG,IAAmBH,GAAsBK,GACxE3B,KAAKe,uBAEPO,EAAqBG,CACvB,EACA,CAAEK,OAAQ9B,KAAK+B,kBAEnB,CAMS,WAAAC,CAAYC,GACnB,MAAmB,iBAAfA,EAAM9D,KACD6B,KAAKkC,eAEK,0BAAfD,EAAM9D,KACD6B,KAAKmC,wBAEK,oBAAfF,EAAM9D,KACD6B,KAAKoC,kBAEK,eAAfH,EAAM9D,MACR6B,KAAKqC,WAAWJ,EAAMK,UACf,QAFT,CAKF,CAGS,MAAAC,GAGP,MAAMC,EAAaxC,KAAKyC,aAAaC,cAAc,cACnDF,GAAYG,gBAAgB,wBAE5B3C,KAAKd,SAAS+B,QACdjB,KAAKlD,OAAS,GACdkD,KAAKX,YAAc,KACnBW,KAAKV,WAAa,KAClBU,KAAKT,YAAa,EAClBS,KAAKN,aAAe,KACpBM,KAAKR,sBAAwB,KAC7BQ,KAAKP,oBAAsB,KAC3BO,KAAKL,oBAAqB,EAC1BK,KAAKJ,oBAAqB,CAC5B,CAMQ,oBAAAmB,GACNf,KAAKd,SAAS+B,QACdjB,KAAKlD,OAAS,GACdkD,KAAKX,YAAc,KACnBW,KAAKV,WAAa,KAClBU,KAAKN,aAAe,KACpBM,KAAKZ,aAAe,KACpBY,KAAKzC,OAAS,KACdyC,KAAKL,oBAAqB,EAC1BK,KAAKJ,oBAAqB,EAC1BI,KAAKqB,oBACP,CAOS,WAAAuB,CAAYpB,GAEnB,IAAKxB,KAAKD,qBAAsB,OAAO,EAEvC,MAAMM,SAAEA,EAAAC,SAAUA,EAAAuC,cAAUA,GAAkBrB,GACxC7C,KAAEA,EAAAC,UAAMA,EAAY,SAAYoB,KAAKtB,OAI3C,GAAImE,EAAc1E,OAASS,EACzB,OAAO,EAKT,MAAMkE,EAAStB,EAAMsB,OACfC,EAAYD,GAAUE,EAAAA,gBAAgBF,GAG5C,GAAa,SAATnE,EAAiB,CACnB,GAAIoE,EACF,OAAO,EAET,IAAK/C,KAAKW,iBAAiBN,EAAUC,GACnC,OAAO,EAGT,MAAM2C,EAAcjD,KAAKN,aACzB,OAAIuD,GAAeA,EAAYvG,MAAQ2D,GAAY4C,EAAYtG,MAAQ2D,IAGvEN,KAAKN,aAAe,CAAEhD,IAAK2D,EAAU1D,IAAK2D,GAC1CN,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,uBAJI,CAMX,CAGA,GAAa,QAAT1C,EAAgB,CAClB,IAAKqB,KAAKU,gBAAgBL,GACxB,OAAO,EAGT,MAAMpB,GAA0C,IAA5Be,KAAKtB,OAAOO,YAC1BiE,EAAWL,EAAcK,UAAYjE,EACrCkE,GAAWN,EAAcM,SAAWN,EAAcO,UAAYnE,EAC9DoE,GAAwC,IAA3BP,GAAQQ,eAE3B,GAAIJ,GAA4B,OAAhBlD,KAAKzC,OAAiB,CAEpC,MAAMgG,EAAQtH,KAAKC,IAAI8D,KAAKzC,OAAQ8C,GAC9BmD,EAAMvH,KAAKK,IAAI0D,KAAKzC,OAAQ8C,GAC7B8C,GACHnD,KAAKd,SAAS+B,QAEhB,IAAA,IAASwC,EAAIF,EAAOE,GAAKD,EAAKC,IACxBzD,KAAKU,gBAAgB+C,IACvBzD,KAAKd,SAASgC,IAAIuC,EAGxB,MAAA,GAAWN,GAAYE,GAAcpE,EAE/Be,KAAKd,SAAS8B,IAAIX,GACpBL,KAAKd,SAASwE,OAAOrD,GAErBL,KAAKd,SAASgC,IAAIb,GAEpBL,KAAKzC,OAAS8C,MACT,CAEL,GAA2B,IAAvBL,KAAKd,SAAS0C,MAAc5B,KAAKd,SAAS8B,IAAIX,GAChD,OAAO,EAETL,KAAKd,SAAS+B,QACdjB,KAAKd,SAASgC,IAAIb,GAClBL,KAAKzC,OAAS8C,CAChB,CAMA,OAJAL,KAAKZ,aAAeiB,EACpBL,KAAKF,mBAAoB,EACzBE,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,sBACE,CACT,CAGA,GAAa,UAAT1C,EAAkB,CAEpB,GAAIoE,EACF,OAAO,EAIT,IAAK/C,KAAKW,iBAAiBN,EAAUC,GACnC,OAAO,EAGT,MAAM4C,EAAWL,EAAcK,SACzBC,GAAWN,EAAcM,SAAWN,EAAcO,WAAwC,IAA5BpD,KAAKtB,OAAOO,YAEhF,GAAIiE,GAAYlD,KAAKV,WAAY,CAE/B,MAAMqE,EAAWrG,EAAsB0C,KAAKV,WAAY,CAAE5C,IAAK2D,EAAU1D,IAAK2D,IAGxEsD,EAAe5D,KAAKlD,OAAO+E,OAAS,EAAI7B,KAAKlD,OAAOkD,KAAKlD,OAAO+E,OAAS,GAAK,KACpF,GAAI+B,GAAgBnG,EAAYmG,EAAcD,GAC5C,OAAO,EAGLR,EACEnD,KAAKlD,OAAO+E,OAAS,EACvB7B,KAAKlD,OAAOkD,KAAKlD,OAAO+E,OAAS,GAAK8B,EAEtC3D,KAAKlD,OAAOO,KAAKsG,GAGnB3D,KAAKlD,OAAS,CAAC6G,GAEjB3D,KAAKX,YAAcsE,CACrB,SAAWR,EAAS,CAClB,MAAMQ,EAA8B,CAClC3H,SAAUqE,EACVjE,SAAUkE,EACVnE,OAAQkE,EACRhE,OAAQiE,GAEVN,KAAKlD,OAAOO,KAAKsG,GACjB3D,KAAKX,YAAcsE,EACnB3D,KAAKV,WAAa,CAAE5C,IAAK2D,EAAU1D,IAAK2D,EAC1C,KAAO,CAEL,MAAMqD,EAA8B,CAClC3H,SAAUqE,EACVjE,SAAUkE,EACVnE,OAAQkE,EACRhE,OAAQiE,GAIV,GAA2B,IAAvBN,KAAKlD,OAAO+E,QAAgBpE,EAAYuC,KAAKlD,OAAO,GAAI6G,GAC1D,OAAO,EAGT3D,KAAKlD,OAAS,CAAC6G,GACf3D,KAAKX,YAAcsE,EACnB3D,KAAKV,WAAa,CAAE5C,IAAK2D,EAAU1D,IAAK2D,EAC1C,CAKA,OAHAN,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAE1DpB,KAAKqB,sBACE,CACT,CAEA,OAAO,CACT,CAGS,SAAAwC,CAAUrC,GAEjB,IAAKxB,KAAKD,qBAAsB,OAAO,EAEvC,MAAMpB,KAAEA,GAASqB,KAAKtB,OAEhBoF,EADU,CAAC,UAAW,YAAa,YAAa,aAAc,MAAO,OAAQ,MAAO,SAAU,YAC3EC,SAASvC,EAAMwC,KAIxC,GAAkB,WAAdxC,EAAMwC,IAAkB,CAE1B,OADkBhE,KAAKC,KAAKgC,MAAe,aAC7BhF,KAAKgH,WAIN,SAATtF,EACFqB,KAAKN,aAAe,KACF,QAATf,GACTqB,KAAKd,SAAS+B,QACdjB,KAAKzC,OAAS,MACI,UAAToB,IACTqB,KAAKlD,OAAS,GACdkD,KAAKX,YAAc,KACnBW,KAAKV,WAAa,MAEpBU,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,sBACE,EACT,CAGA,GAAa,SAAT1C,GAAmBmF,EAerB,OAbAI,eAAe,KACb,MAAMC,EAAWnE,KAAKC,KAAKmE,UACrBC,EAAWrE,KAAKC,KAAKqE,UAEvBtE,KAAKW,iBAAiBwD,EAAUE,GAClCrE,KAAKN,aAAe,CAAEhD,IAAKyH,EAAUxH,IAAK0H,GAG1CrE,KAAKN,aAAe,KAEtBM,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,wBAEA,EAIT,GAAa,QAAT1C,EAAgB,CAClB,MAAMM,GAA0C,IAA5Be,KAAKtB,OAAOO,YAQhC,GANgB,YAAduC,EAAMwC,KACQ,cAAdxC,EAAMwC,KACQ,WAAdxC,EAAMwC,KACQ,aAAdxC,EAAMwC,MACJxC,EAAM2B,SAAW3B,EAAM4B,WAA2B,SAAd5B,EAAMwC,KAAgC,QAAdxC,EAAMwC,KAErD,CACf,MAAMd,EAAW1B,EAAM0B,UAAYjE,EAgBnC,OAbIiE,GAA4B,OAAhBlD,KAAKzC,SACnByC,KAAKzC,OAASyC,KAAKC,KAAKmE,WAK1BpE,KAAKF,mBAAoB,EAGzBE,KAAKP,oBAAsB,CAAEyD,YAG7BgB,eAAe,IAAMlE,KAAKqB,uBACnB,CACT,CAGA,GAAIpC,GAA6B,MAAduC,EAAMwC,MAAgBxC,EAAM2B,SAAW3B,EAAM4B,SAAU,CAExE,OADkBpD,KAAKC,KAAKgC,MAAe,aAC7BhF,KAAKgH,WACnBzC,EAAM+C,iBACN/C,EAAMgD,kBACNxE,KAAKyE,aACE,EACT,CACF,CAIA,GAAa,UAAT9F,GAAoBmF,EAAU,CAEhC,MAAMY,EAAyB,QAAdlD,EAAMwC,IACjBW,EAAenD,EAAM0B,WAAawB,EAgBxC,OAZIC,IAAiB3E,KAAKV,aACxBU,KAAKV,WAAa,CAAE5C,IAAKsD,KAAKC,KAAKmE,UAAWzH,IAAKqD,KAAKC,KAAKqE,YAI/DtE,KAAKR,sBAAwB,CAAE0D,SAAUyB,GAKzCT,eAAe,IAAMlE,KAAKqB,uBAEnB,CACT,CAGA,GACW,UAAT1C,IAC4B,IAA5BqB,KAAKtB,OAAOO,aACE,MAAduC,EAAMwC,MACLxC,EAAM2B,SAAW3B,EAAM4B,SACxB,CAEA,OADkBpD,KAAKC,KAAKgC,MAAe,aAC7BhF,KAAKgH,WACnBzC,EAAM+C,iBACN/C,EAAMgD,kBACNxE,KAAKyE,aACE,EACT,CAEA,OAAO,CACT,CAGS,eAAAG,CAAgBpD,GAEvB,IAAKxB,KAAKD,qBAAsB,OAEhC,GAAyB,UAArBC,KAAKtB,OAAOC,KAAkB,OAClC,QAAuB,IAAnB6C,EAAMnB,eAA6C,IAAnBmB,EAAMlB,SAAwB,OAClE,GAAIkB,EAAMnB,SAAW,EAAG,OAIxB,GAAImB,EAAMsB,QAAUE,EAAAA,gBAAgBxB,EAAMsB,QACxC,OAIF,IAAK9C,KAAKW,iBAAiBa,EAAMnB,SAAUmB,EAAMlB,UAC/C,OAIF,GAAIkB,EAAMqB,cAAcK,UAAYlD,KAAKV,WACvC,OAIFU,KAAKT,YAAa,EAClB,MAAMc,EAAWmB,EAAMnB,SACjBC,EAAWkB,EAAMlB,SAGjB6C,GAAW3B,EAAMqB,cAAcM,SAAW3B,EAAMqB,cAAcO,WAAwC,IAA5BpD,KAAKtB,OAAOO,YAEtF0E,EAA8B,CAClC3H,SAAUqE,EACVjE,SAAUkE,EACVnE,OAAQkE,EACRhE,OAAQiE,GAIV,OAAK6C,GAAkC,IAAvBnD,KAAKlD,OAAO+E,QAAgBpE,EAAYuC,KAAKlD,OAAO,GAAI6G,IAEtE3D,KAAKV,WAAa,CAAE5C,IAAK2D,EAAU1D,IAAK2D,IACjC,IAGTN,KAAKV,WAAa,CAAE5C,IAAK2D,EAAU1D,IAAK2D,GAEnC6C,IACHnD,KAAKlD,OAAS,IAGhBkD,KAAKlD,OAAOO,KAAKsG,GACjB3D,KAAKX,YAAcsE,EAEnB3D,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,sBACE,EACT,CAGS,eAAAwD,CAAgBrD,GAEvB,IAAKxB,KAAKD,qBAAsB,OAEhC,GAAyB,UAArBC,KAAKtB,OAAOC,KAAkB,OAClC,IAAKqB,KAAKT,aAAeS,KAAKV,WAAY,OAC1C,QAAuB,IAAnBkC,EAAMnB,eAA6C,IAAnBmB,EAAMlB,SAAwB,OAClE,GAAIkB,EAAMnB,SAAW,EAAG,OAIxB,IAAIyE,EAAYtD,EAAMlB,SACtB,MAAMwC,EAAS9C,KAAKS,eAAeqE,GACnC,GAAIhC,GAAUE,kBAAgBF,GAAS,CAErC,MAAMiC,EAAe/E,KAAKS,eAAeuE,UAAWrI,IAASqG,kBAAgBrG,IACzEoI,GAAgB,IAClBD,EAAYC,EAEhB,CAEA,MAAMpB,EAAWrG,EAAsB0C,KAAKV,WAAY,CAAE5C,IAAK8E,EAAMnB,SAAU1D,IAAKmI,IAG9ElB,EAAe5D,KAAKlD,OAAO+E,OAAS,EAAI7B,KAAKlD,OAAOkD,KAAKlD,OAAO+E,OAAS,GAAK,KACpF,OAAI+B,GAAgBnG,EAAYmG,EAAcD,KAI1C3D,KAAKlD,OAAO+E,OAAS,EACvB7B,KAAKlD,OAAOkD,KAAKlD,OAAO+E,OAAS,GAAK8B,EAEtC3D,KAAKlD,OAAOO,KAAKsG,GAEnB3D,KAAKX,YAAcsE,EAEnB3D,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,uBAXI,CAaX,CAGS,aAAA4D,CAAcC,GAErB,GAAKlF,KAAKD,sBAEe,UAArBC,KAAKtB,OAAOC,KAChB,OAAIqB,KAAKT,YACPS,KAAKT,YAAa,GACX,QAFT,CAIF,CAQS,cAAA4F,CAAeC,GACtB,GAAIpF,KAAKtB,OAAO2G,UAAiC,QAArBrF,KAAKtB,OAAOC,KAAgB,CAEtD,GAAIyG,EAAQnI,KAAMN,GAAQA,EAAI2I,QAAUxH,GACtC,OAAOsH,EAET,MAAMG,EAAcvF,MAAKwF,IAEnBC,EAAcL,EAAQJ,UAAUU,oBAChCC,EAAWF,GAAe,EAAIA,EAAc,EAAI,EACtD,MAAO,IAAIL,EAAQQ,MAAM,EAAGD,GAAWJ,KAAgBH,EAAQQ,MAAMD,GACvE,CACA,OAAOP,CACT,CAKA,EAAAI,GACE,MAAO,CACLF,MAAOxH,EACP+H,OAAQ,GACRC,MAAO,GACPC,WAAW,EACXC,UAAU,EACVC,cAAc,EACdC,SAAS,EACT5C,gBAAgB,EAChB6C,eAAgB,KACd,MAAMC,EAAYC,SAASC,cAAc,OAGzC,GAFAF,EAAUG,UAAY,uBAEU,IAA5BvG,KAAKtB,OAAOO,YAAuB,OAAOmH,EAC9C,MAAMf,EAAWgB,SAASC,cAAc,SAYxC,OAXAjB,EAASlH,KAAO,WAChBkH,EAASkB,UAAY,0BACrBlB,EAAS9D,iBAAiB,QAAUiF,IAClCA,EAAEhC,kBACGgC,EAAEC,OAA4BC,QACjC1G,KAAKyE,YAELzE,KAAK2G,mBAGTP,EAAUQ,YAAYvB,GACfe,GAETS,SAAWC,IACT,MAAMzB,EAAWgB,SAASC,cAAc,SACxCjB,EAASlH,KAAO,WAChBkH,EAASkB,UAAY,0BAErB,MAAMQ,EAASD,EAAIC,OACnB,GAAIA,EAAQ,CACV,MAAM1G,EAAW2G,SAASD,EAAOE,aAAa,aAAe,KAAM,IAC/D5G,GAAY,IACdgF,EAASqB,QAAU1G,KAAKd,SAAS8B,IAAIX,GAEzC,CACA,OAAOgF,GAGb,CAMA,EAAA6B,CAAsBC,GAEEA,EAAOC,iBAAiB,4BAChCC,QAAShC,IACrB,MAAMiC,EAAOjC,EAASkC,QAAQ,SACxBlH,EAAWiH,EAAOE,sBAAoBF,IAAQ,EAChDjH,GAAY,IACdgF,EAASqB,QAAU1G,KAAKd,SAAS8B,IAAIX,MAKzC,MAAMoH,EAAiBN,EAAOzE,cAAc,4BAC5C,GAAI+E,EAAgB,CAClB,MAAMC,EAAW1H,KAAKQ,KAAKqB,OAC3B,IAAI8F,EAAkB,EACtB,GAAI3H,KAAKtB,OAAO6B,aACd,IAAA,IAASkD,EAAI,EAAGA,EAAIiE,EAAUjE,IACxBzD,KAAKU,gBAAgB+C,IAAIkE,SAG/BA,EAAkBD,EAEpB,MAAME,EAAcD,EAAkB,GAAK3H,KAAKd,SAAS0C,MAAQ+F,EAC3DE,EAAe7H,KAAKd,SAAS0C,KAAO,EAC1C6F,EAAef,QAAUkB,EACzBH,EAAeK,cAAgBD,IAAiBD,CAClD,CACF,CAWA,EAAAG,CAAsBpJ,GACpB,MAAMwF,EAAWnE,KAAKC,KAAKmE,UACrBC,EAAWrE,KAAKC,KAAKqE,UAE3B,GAAa,QAAT3F,EAAgB,CAElB,GAAIqB,KAAKF,kBAGP,OAFAE,KAAKF,mBAAoB,OACzBE,KAAKL,mBAAqBwE,GAIxBA,IAAanE,KAAKL,qBACpBK,KAAKL,mBAAqBwE,EACtBnE,KAAKU,gBAAgByD,KAClBnE,KAAKd,SAAS8B,IAAImD,IAAoC,IAAvBnE,KAAKd,SAAS0C,OAChD5B,KAAKd,SAAS+B,QACdjB,KAAKd,SAASgC,IAAIiD,GAClBnE,KAAKZ,aAAe+E,EACpBnE,KAAKzC,OAAS4G,EACdnE,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,OAIlE,CAEA,GAAa,SAATzC,EAAiB,CACnB,GAAIqB,KAAKF,kBAIP,OAHAE,KAAKF,mBAAoB,EACzBE,KAAKL,mBAAqBwE,OAC1BnE,KAAKJ,mBAAqByE,GAI5B,IAAIF,IAAanE,KAAKL,oBAAsB0E,IAAarE,KAAKJ,sBAC5DI,KAAKL,mBAAqBwE,EAC1BnE,KAAKJ,mBAAqByE,EACtBrE,KAAKW,iBAAiBwD,EAAUE,IAAW,CAC7C,MAAM2D,EAAMhI,KAAKN,aACZsI,GAAOA,EAAItL,MAAQyH,GAAY6D,EAAIrL,MAAQ0H,IAC9CrE,KAAKN,aAAe,CAAEhD,IAAKyH,EAAUxH,IAAK0H,GAC1CrE,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAE9D,CAEJ,CACF,CAMA,EAAA6G,GACE,MAAMd,EAASnH,KAAKyC,YACpB,IAAK0E,EAAQ,OAEb,MAAMxI,KAAEA,GAASqB,KAAKtB,OAChBwJ,IAA0BlI,KAAKtB,OAAO6B,aAMtCiC,EAAa2E,EAAOzE,cAAc,cACxC,GAAIF,EAAY,CACd,MAAM2F,GAAoC,IAA5BnI,KAAKtB,OAAOO,YAC1BuD,EAAW4F,aAAa,uBAAwBD,EAAQ,OAAS,QACnE,CAGiBhB,EAAOC,iBAAiB,SAChCC,QAASC,IAChBA,EAAKe,UAAUC,OAAOC,EAAAA,YAAYC,SAAU,MAAO,SAAU,QAAS,QAElEN,GACFZ,EAAK3E,gBAAgB,qBAIzB,MAAM8F,EAAUtB,EAAOC,iBAAiB,kBAqCxC,GApCAqB,EAAQpB,QAAS3K,IACfA,EAAI2L,UAAUC,OAAOC,EAAAA,YAAYC,SAAU,aAC3C9L,EAAI0L,aAAa,gBAAiB,SAE9BF,GACFxL,EAAIiG,gBAAgB,qBAKX,QAAThE,IAEF+J,EAAAA,eAAevB,GAEfsB,EAAQpB,QAAS3K,IACf,MAAMiM,EAAYjM,EAAIgG,cAAc,mBAC9BrC,EAAWmH,EAAAA,oBAAoBmB,GACjCtI,GAAY,IAEV6H,IAA0BlI,KAAKU,gBAAgBL,IACjD3D,EAAI0L,aAAa,kBAAmB,SAElCpI,KAAKd,SAAS8B,IAAIX,KACpB3D,EAAI2L,UAAUnH,IAAIqH,EAAAA,YAAYC,SAAU,aACxC9L,EAAI0L,aAAa,gBAAiB,YAMpCpI,KAAKtB,OAAO2G,UACdrF,MAAKkH,EAAsBC,KAKjB,SAATxI,GAA4B,UAATA,IAAqBuJ,EAAuB,CACpDf,EAAOC,iBAAiB,6BAChCC,QAASC,IACb,MAAMjH,EAAW2G,SAASM,EAAKL,aAAa,aAAe,KAAM,IAC3D3G,EAAW0G,SAASM,EAAKL,aAAa,aAAe,KAAM,IAC7D5G,GAAY,GAAKC,GAAY,IAC1BN,KAAKW,iBAAiBN,EAAUC,IACnCgH,EAAKc,aAAa,kBAAmB,WAI7C,CAIA,GAAa,UAATzJ,GAAoBqB,KAAKlD,OAAO+E,OAAS,EAAG,CAE9C6G,EAAAA,eAAevB,GAGf,MAAMyB,EAAmB5I,KAAKlD,OAAOC,IAAIjB,GAGnC+M,EAAgB,CAACC,EAAWC,KAChC,IAAA,MAAWhN,KAAS6M,EAClB,GAAIE,GAAK/M,EAAMC,UAAY8M,GAAK/M,EAAMI,QAAU4M,GAAKhN,EAAMK,UAAY2M,GAAKhN,EAAMM,OAChF,OAAO,EAGX,OAAO,GAGK8K,EAAOC,iBAAiB,6BAChCC,QAASC,IACb,MAAMjH,EAAW2G,SAASM,EAAKL,aAAa,aAAe,KAAM,IAC3D3G,EAAW0G,SAASM,EAAKL,aAAa,aAAe,KAAM,IACjE,GAAI5G,GAAY,GAAKC,GAAY,EAAG,CAGlC,MAAMwC,EAAS9C,KAAKS,eAAeH,GACnC,GAAIwC,GAAUE,kBAAgBF,GAC5B,OAGE+F,EAAcxI,EAAUC,KAC1BgH,EAAKe,UAAUnH,IAAIqH,EAAAA,YAAYC,UAC/BlB,EAAKc,aAAa,gBAAiB,QAI9BS,EAAcxI,EAAW,EAAGC,IAAWgH,EAAKe,UAAUnH,IAAI,OAC1D2H,EAAcxI,EAAW,EAAGC,IAAWgH,EAAKe,UAAUnH,IAAI,UAC1D2H,EAAcxI,EAAUC,EAAW,IAAIgH,EAAKe,UAAUnH,IAAI,SAC1D2H,EAAcxI,EAAUC,EAAW,IAAIgH,EAAKe,UAAUnH,IAAI,QAEnE,GAEJ,CAIF,CAGS,WAAA8H,GAEP,IAAKhJ,KAAKD,qBAAsB,OAEhC,MAAMoH,EAASnH,KAAKyC,YACpB,IAAK0E,EAAQ,OAEb,MAAMf,EAAYe,EAAOzE,cAAc,mBACjC/D,KAAEA,GAASqB,KAAKtB,OAItB,GAAIsB,KAAKP,qBAAgC,QAATd,EAAgB,CAC9C,MAAMuE,SAAEA,GAAalD,KAAKP,oBAC1BO,KAAKP,oBAAsB,KAE3B,MAAM0E,EAAWnE,KAAKC,KAAKmE,UAE3B,GAAIlB,GAA4B,OAAhBlD,KAAKzC,OAAiB,CAEpCyC,KAAKd,SAAS+B,QACd,MAAMsC,EAAQtH,KAAKC,IAAI8D,KAAKzC,OAAQ4G,GAC9BX,EAAMvH,KAAKK,IAAI0D,KAAKzC,OAAQ4G,GAClC,IAAA,IAASV,EAAIF,EAAOE,GAAKD,EAAKC,IACxBzD,KAAKU,gBAAgB+C,IACvBzD,KAAKd,SAASgC,IAAIuC,EAGxB,MAEMzD,KAAKU,gBAAgByD,IACvBnE,KAAKd,SAAS+B,QACdjB,KAAKd,SAASgC,IAAIiD,GAClBnE,KAAKzC,OAAS4G,GAEdnE,KAAKd,SAAS+B,QAIlBjB,KAAKZ,aAAe+E,EACpBnE,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,IAC5D,CAIA,GAAIpB,KAAKR,uBAAkC,UAATb,EAAkB,CAClD,MAAMuE,SAAEA,GAAalD,KAAKR,sBAC1BQ,KAAKR,sBAAwB,KAE7B,MAAMyJ,EAAajJ,KAAKC,KAAKmE,UACvB8E,EAAalJ,KAAKC,KAAKqE,UAE7B,GAAIpB,GAAYlD,KAAKV,WAAY,CAE/B,MAAMqE,EAAWrG,EAAsB0C,KAAKV,WAAY,CAAE5C,IAAKuM,EAAYtM,IAAKuM,IAChFlJ,KAAKlD,OAAS,CAAC6G,GACf3D,KAAKX,YAAcsE,CACrB,MAAYT,IAEVlD,KAAKlD,OAAS,GACdkD,KAAKX,YAAc,KACnBW,KAAKV,WAAa,CAAE5C,IAAKuM,EAAYtM,IAAKuM,IAG5ClJ,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,IAC5D,CAKApB,MAAK+H,EAAsBpJ,GAG3BqB,KAAKyC,YAAY2F,aAAa,sBAAuBzJ,GAGjDyH,GACFA,EAAUiC,UAAUc,OAAO,YAAanJ,KAAKT,YAG/CS,MAAKiI,GACP,CAOS,cAAAmB,GAEFpJ,KAAKD,sBAEVC,MAAKiI,GACP,CAqBA,YAAA/F,GACE,MAAO,CACLvD,KAAMqB,KAAKtB,OAAOC,KAClB7B,OAAQkD,MAAKoB,IAActE,OAC3BS,OAAQyC,KAAKV,WAEjB,CAKA,gBAAA+J,GACE,ODnnCG,SAA6BvM,GAClC,MAAMwM,MAAcC,IAEpB,IAAA,MAAWxN,KAASe,EAClB,IAAA,MAAWwK,KAAQnK,EAAgBpB,GACjCuN,EAAQE,IAAI,GAAGlC,EAAK5K,OAAO4K,EAAK3K,MAAO2K,GAI3C,MAAO,IAAIgC,EAAQG,SACrB,CCymCWC,CAAoB1J,KAAKlD,OAClC,CAKA,cAAA6M,CAAejN,EAAaC,GAC1B,OAAOK,EAAiBN,EAAKC,EAAKqD,KAAKlD,OACzC,CAeA,SAAA2H,GACE,MAAM9F,KAAEA,EAAAM,YAAMA,GAAgBe,KAAKtB,OAGnC,IAAoB,IAAhBO,EAEJ,GAAa,QAATN,EAAgB,CAClBqB,KAAKd,SAAS+B,QACd,IAAA,IAASwC,EAAI,EAAGA,EAAIzD,KAAKQ,KAAKqB,OAAQ4B,IAChCzD,KAAKU,gBAAgB+C,IACvBzD,KAAKd,SAASgC,IAAIuC,GAGtBzD,KAAKF,mBAAoB,EACzBE,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,oBACP,MAAA,GAAoB,UAAT1C,EAAkB,CAC3B,MAAM+I,EAAW1H,KAAKQ,KAAKqB,OACrB+H,EAAW5J,KAAKoF,QAAQvD,OAC9B,GAAI6F,EAAW,GAAKkC,EAAW,EAAG,CAChC,MAAMC,EAA8B,CAClC7N,SAAU,EACVI,SAAU,EACVD,OAAQuL,EAAW,EACnBrL,OAAQuN,EAAW,GAErB5J,KAAKlD,OAAS,CAAC+M,GACf7J,KAAKX,YAAcwK,EACnB7J,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,oBACP,CACF,CACF,CAeA,UAAAgB,CAAWyH,GACT,GAAyB,QAArB9J,KAAKtB,OAAOC,KAAgB,OAEhC,MAAMoL,GACwB,IAA5B/J,KAAKtB,OAAOO,aAAyB6K,EAAQjI,OAAS,EAAI,CAACiI,EAAQA,EAAQjI,OAAS,IAAMiI,EAC5F9J,KAAKd,SAAS+B,QACd,IAAA,MAAW+I,KAAOD,EACZC,GAAO,GAAKA,EAAMhK,KAAKQ,KAAKqB,QAAU7B,KAAKU,gBAAgBsJ,IAC7DhK,KAAKd,SAASgC,IAAI8I,GAGtBhK,KAAKzC,OAASwM,EAAiBlI,OAAS,EAAIkI,EAAiBA,EAAiBlI,OAAS,GAAK,KAC5F7B,KAAKF,mBAAoB,EACzBE,KAAKmB,KAA4B,mBAAoBnB,MAAKoB,KAC1DpB,KAAKqB,oBACP,CAYA,qBAAAc,GACE,MAAO,IAAInC,KAAKd,UAAU+K,KAAK,CAACvM,EAAGC,IAAMD,EAAIC,EAC/C,CAmBA,eAAAyE,GACE,MAAMzD,KAAEA,GAASqB,KAAKtB,OAChB8B,EAAOR,KAAKQ,KAElB,GAAa,QAAT7B,EACF,OAAOqB,KAAKmC,wBACT+H,OAAQzG,GAAMA,GAAK,GAAKA,EAAIjD,EAAKqB,QACjC9E,IAAK0G,GAAMjD,EAAKiD,IAGrB,GAAa,SAAT9E,GAAmBqB,KAAKN,aAAc,CACxC,MAAMhD,IAAEA,GAAQsD,KAAKN,aACrB,OAAOhD,GAAO,GAAKA,EAAM8D,EAAKqB,OAAS,CAACrB,EAAK9D,IAAa,EAC5D,CAEA,GAAa,UAATiC,GAAoBqB,KAAKlD,OAAO+E,OAAS,EAAG,CAE9C,MAAMsI,MAAiBhL,IACvB,IAAA,MAAWpD,KAASiE,KAAKlD,OAAQ,CAC/B,MAAMsN,EAASnO,KAAKK,IAAI,EAAGL,KAAKC,IAAIH,EAAMC,SAAUD,EAAMI,SACpDkO,EAASpO,KAAKC,IAAIsE,EAAKqB,OAAS,EAAG5F,KAAKK,IAAIP,EAAMC,SAAUD,EAAMI,SACxE,IAAA,IAAS2M,EAAIsB,EAAQtB,GAAKuB,EAAQvB,IAChCqB,EAAWjJ,IAAI4H,EAEnB,CACA,MAAO,IAAIqB,GAAYF,KAAK,CAACvM,EAAGC,IAAMD,EAAIC,GAAGZ,IAAK0G,GAAMjD,EAAKiD,GAC/D,CAEA,MAAO,EACT,CAKA,cAAAkD,GACE3G,KAAKN,aAAe,KACpBM,KAAKd,SAAS+B,QACdjB,KAAKzC,OAAS,KACdyC,KAAKlD,OAAS,GACdkD,KAAKX,YAAc,KACnBW,KAAKV,WAAa,KAClBU,KAAKmB,KAA4B,mBAAoB,CAAExC,KAAMqB,KAAKtB,OAAOC,KAAM7B,OAAQ,KACvFkD,KAAKqB,oBACP,CAKA,SAAAiJ,CAAUxN,GACRkD,KAAKlD,OAASA,EAAOC,IAAK+L,IAAA,CACxB9M,SAAU8M,EAAErM,KAAKC,IACjBN,SAAU0M,EAAErM,KAAKE,IACjBR,OAAQ2M,EAAElM,GAAGF,IACbL,OAAQyM,EAAElM,GAAGD,OAEfqD,KAAKX,YAAcW,KAAKlD,OAAO+E,OAAS,EAAI7B,KAAKlD,OAAOkD,KAAKlD,OAAO+E,OAAS,GAAK,KAClF7B,KAAKmB,KAA4B,mBAAoB,CACnDxC,KAAMqB,KAAKtB,OAAOC,KAClB7B,OAAQD,EAAemD,KAAKlD,UAE9BkD,KAAKqB,oBACP,CAMA,EAAAD,GACE,MAAMI,EAz2CV,SACE7C,EACA4L,EAKAX,GAEA,GAAa,SAATjL,GAAmB4L,EAAM7K,aAC3B,MAAO,CACLf,OACA7B,OAAQ,CACN,CACEL,KAAM,CAAEC,IAAK6N,EAAM7K,aAAahD,IAAKC,IAAK4N,EAAM7K,aAAa/C,KAC7DC,GAAI,CAAEF,IAAK6N,EAAM7K,aAAahD,IAAKC,IAAK4N,EAAM7K,aAAa/C,QAMnE,GAAa,QAATgC,GAAkB4L,EAAMrL,SAAS0C,KAAO,EAAG,CAE7C,MAAM4I,EAAS,IAAID,EAAMrL,UAAU+K,KAAK,CAACvM,EAAGC,IAAMD,EAAIC,GAChDb,EAAsB,GAC5B,IAAIyG,EAAQiH,EAAO,GACfhH,EAAMD,EACV,IAAA,IAASE,EAAI,EAAGA,EAAI+G,EAAO3I,OAAQ4B,IAC7B+G,EAAO/G,KAAOD,EAAM,EACtBA,EAAMgH,EAAO/G,IAEb3G,EAAOO,KAAK,CAAEZ,KAAM,CAAEC,IAAK6G,EAAO5G,IAAK,GAAKC,GAAI,CAAEF,IAAK8G,EAAK7G,IAAKiN,EAAW,KAC5ErG,EAAQiH,EAAO/G,GACfD,EAAMD,GAIV,OADAzG,EAAOO,KAAK,CAAEZ,KAAM,CAAEC,IAAK6G,EAAO5G,IAAK,GAAKC,GAAI,CAAEF,IAAK8G,EAAK7G,IAAKiN,EAAW,KACrE,CAAEjL,OAAM7B,SACjB,CAEA,MAAa,UAAT6B,GAAoB4L,EAAMzN,OAAO+E,OAAS,EACrC,CAAElD,OAAM7B,OAAQD,EAAe0N,EAAMzN,SAGvC,CAAE6B,OAAM7B,OAAQ,GACzB,CA4zCkB2N,CACZzK,KAAKtB,OAAOC,KACZ,CACEe,aAAcM,KAAKN,aACnBR,SAAUc,KAAKd,SACfpC,OAAQkD,KAAKlD,QAEfkD,KAAKoF,QAAQvD,QAUf,OAPI7B,KAAKH,eAAe6K,aAAa1K,KAAKH,eAC1CG,KAAKH,cAAgB8K,WAAW,KAC9B,MAAMC,EAAuB,QAAfpJ,EAAM7C,KAAiBqB,KAAKd,SAAS0C,KAAOJ,EAAM1E,OAAO+E,OACnE+I,EAAQ,GACVC,WAAS7K,KAAKyC,YAAaqI,EAAAA,eAAe9K,KAAKyC,YAAa,mBAAoBmI,KAEjF,KACIpJ,CACT"}
@@ -1,2 +1,2 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/constants"),require("../../core/internal/diagnostics"),require("../../core/internal/rows"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/constants","../../core/internal/diagnostics","../../core/internal/rows","../../core/plugin/base-plugin"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_undoRedo={},t.TbwGrid,t.TbwGrid,t.TbwGrid,t.TbwGrid)}(this,function(t,e,o,n,i){"use strict";function c(t,e,o){const n=[...t.undoStack,e];for(;n.length>o;)n.shift();return{undoStack:n,redoStack:[]}}function a(t){if(0===t.undoStack.length)return{newState:t,action:null};const e=[...t.undoStack],o=e.pop();return o?{newState:{undoStack:e,redoStack:[...t.redoStack,o]},action:o}:{newState:t,action:null}}function r(t){if(0===t.redoStack.length)return{newState:t,action:null};const e=[...t.redoStack],o=e.pop();return o?{newState:{undoStack:[...t.undoStack,o],redoStack:e},action:o}:{newState:t,action:null}}class d extends i.BaseGridPlugin{static dependencies=[{name:"editing",required:!0,reason:"UndoRedoPlugin tracks cell edit history"}];name="undoRedo";get defaultConfig(){return{maxHistorySize:100}}undoStack=[];redoStack=[];#t=!1;#e=null;#o(t,e){const o=this.rows[t.rowIndex];if(o){try{const n=this.grid.getRowId(o);if(n)return void this.grid.updateRow(n,{[t.field]:e})}catch{}o[t.field]=e}}#n(t){const o=this.grid,i=o._visibleColumns?.findIndex(e=>e.field===t.field)??-1;if(i<0)return;o._focusRow=t.rowIndex,o._focusCol=i;const c=o.findRenderedRowElement?.(t.rowIndex);if(!c)return;const a=c.querySelector(`.cell[data-col="${i}"]`);if(a?.classList.contains(e.GridClasses.EDITING)){const t=a.querySelector(n.FOCUSABLE_EDITOR_SELECTOR);t?.focus({preventScroll:!0})}}#i(t,e){if(this.#t=!0,"compound"===t.type){const o="undo"===e?[...t.actions].reverse():t.actions;for(const t of o)this.#o(t,"undo"===e?t.oldValue:t.newValue)}else this.#o(t,"undo"===e?t.oldValue:t.newValue);this.#t=!1}#c(t){const e="compound"===t.type?t.actions[t.actions.length-1]:t;e&&this.#n(e)}attach(t){super.attach(t),this.on("cell-edit-committed",t=>{this.#t||this.recordEdit(t.rowIndex,t.field,t.oldValue,t.newValue)})}detach(){this.undoStack=[],this.redoStack=[],this.#e=null}onKeyDown(t){const e=(t.ctrlKey||t.metaKey)&&"z"===t.key&&!t.shiftKey,o=(t.ctrlKey||t.metaKey)&&("y"===t.key||"z"===t.key&&t.shiftKey);if(e){t.preventDefault();const e=a({undoStack:this.undoStack,redoStack:this.redoStack});return e.action&&(this.#i(e.action,"undo"),this.undoStack=e.newState.undoStack,this.redoStack=e.newState.redoStack,this.emit("undo",{action:e.action,type:"undo"}),this.#c(e.action),this.requestRenderWithFocus()),!0}if(o){t.preventDefault();const e=r({undoStack:this.undoStack,redoStack:this.redoStack});return e.action&&(this.#i(e.action,"redo"),this.undoStack=e.newState.undoStack,this.redoStack=e.newState.redoStack,this.emit("redo",{action:e.action,type:"redo"}),this.#c(e.action),this.requestRenderWithFocus()),!0}return!1}recordEdit(t,e,o,n){const i=function(t,e,o,n){return{type:"cell-edit",rowIndex:t,field:e,oldValue:o,newValue:n,timestamp:Date.now()}}(t,e,o,n);if(this.#e)return void this.#e.push(i);const a=c({undoStack:this.undoStack,redoStack:this.redoStack},i,this.config.maxHistorySize??100);this.undoStack=a.undoStack,this.redoStack=a.redoStack}beginTransaction(){this.#e&&o.throwDiagnostic(o.TRANSACTION_IN_PROGRESS,"Transaction already in progress. Call endTransaction() first."),this.#e=[]}endTransaction(){const t=this.#e;if(t||o.throwDiagnostic(o.NO_TRANSACTION,"No transaction in progress. Call beginTransaction() first."),this.#e=null,0===t.length)return;const e=1===t.length?t[0]:{type:"compound",actions:t,timestamp:Date.now()};const n=c({undoStack:this.undoStack,redoStack:this.redoStack},e,this.config.maxHistorySize??100);this.undoStack=n.undoStack,this.redoStack=n.redoStack}undo(){const t=a({undoStack:this.undoStack,redoStack:this.redoStack});return t.action&&(this.#i(t.action,"undo"),this.undoStack=t.newState.undoStack,this.redoStack=t.newState.redoStack,this.#c(t.action),this.requestRenderWithFocus()),t.action}redo(){const t=r({undoStack:this.undoStack,redoStack:this.redoStack});return t.action&&(this.#i(t.action,"redo"),this.undoStack=t.newState.undoStack,this.redoStack=t.newState.redoStack,this.#c(t.action),this.requestRenderWithFocus()),t.action}canUndo(){return{undoStack:this.undoStack,redoStack:this.redoStack}.undoStack.length>0}canRedo(){return{undoStack:this.undoStack,redoStack:this.redoStack}.redoStack.length>0}clearHistory(){const t={undoStack:[],redoStack:[]};this.undoStack=t.undoStack,this.redoStack=t.redoStack,this.#e=null}getUndoStack(){return[...this.undoStack]}getRedoStack(){return[...this.redoStack]}}t.UndoRedoPlugin=d,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("../../core/constants"),require("../../core/internal/diagnostics"),require("../../core/internal/rows"),require("../../core/plugin/base-plugin")):"function"==typeof define&&define.amd?define(["exports","../../core/constants","../../core/internal/diagnostics","../../core/internal/rows","../../core/plugin/base-plugin"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).TbwGridPlugin_undoRedo={},t.TbwGrid,t.TbwGrid,t.TbwGrid,t.TbwGrid)}(this,function(t,e,o,n,i){"use strict";function r(t,e,o){const n=[...t.undoStack,e];for(;n.length>o;)n.shift();return{undoStack:n,redoStack:[]}}function a(t){if(0===t.undoStack.length)return{newState:t,action:null};const e=[...t.undoStack],o=e.pop();return o?{newState:{undoStack:e,redoStack:[...t.redoStack,o]},action:o}:{newState:t,action:null}}function c(t){if(0===t.redoStack.length)return{newState:t,action:null};const e=[...t.redoStack],o=e.pop();return o?{newState:{undoStack:[...t.undoStack,o],redoStack:e},action:o}:{newState:t,action:null}}class s extends i.BaseGridPlugin{static dependencies=[{name:"editing",required:!0,reason:"UndoRedoPlugin tracks cell edit history"}];name="undoRedo";get defaultConfig(){return{maxHistorySize:100}}undoStack=[];redoStack=[];#t=!1;#e=null;#o(t,e){const o=this.rows[t.rowIndex];if(o){try{const n=this.grid.getRowId(o);if(n)return void this.grid.updateRow(n,{[t.field]:e})}catch{}o[t.field]=e}}#n(t){const o=this.grid,i=o._visibleColumns?.findIndex(e=>e.field===t.field)??-1;if(i<0)return;o._focusRow=t.rowIndex,o._focusCol=i;const r=o.findRenderedRowElement?.(t.rowIndex);if(!r)return;const a=r.querySelector(`.cell[data-col="${i}"]`);if(a?.classList.contains(e.GridClasses.EDITING)){const t=a.querySelector(n.FOCUSABLE_EDITOR_SELECTOR);t?.focus({preventScroll:!0})}}#i(t,e){if(this.#t=!0,"compound"===t.type){const o="undo"===e?[...t.actions].reverse():t.actions;for(const t of o)this.#o(t,"undo"===e?t.oldValue:t.newValue)}else this.#o(t,"undo"===e?t.oldValue:t.newValue);this.#t=!1}#r(t){const e="compound"===t.type?t.actions[t.actions.length-1]:t;e&&this.#n(e)}attach(t){super.attach(t),this.on("cell-edit-committed",t=>{this.#t||this.recordEdit(t.rowIndex,t.field,t.oldValue,t.newValue)})}detach(){this.undoStack=[],this.redoStack=[],this.#e=null}onKeyDown(t){const e=(t.ctrlKey||t.metaKey)&&"z"===t.key&&!t.shiftKey,o=(t.ctrlKey||t.metaKey)&&("y"===t.key||"z"===t.key&&t.shiftKey);if(!e&&!o)return!1;const n=e?"undo":"redo";return this.#a(t.target)?(this.#c(t.target,n),!0):(t.preventDefault(),this.#s(n),!0)}#s(t){const e={undoStack:this.undoStack,redoStack:this.redoStack},o="undo"===t?a(e):c(e);o.action&&(this.#i(o.action,t),this.undoStack=o.newState.undoStack,this.redoStack=o.newState.redoStack,this.emit(t,{action:o.action,type:t}),this.#r(o.action),this.requestRenderWithFocus())}#a(t){return t instanceof Element&&!!t.closest('input, textarea, [contenteditable=""], [contenteditable="true"]')}#c(t,e){const o="undo"===e?"historyUndo":"historyRedo";let n=!1;const i=t=>{t.inputType===o&&(n=!0)};t.addEventListener("beforeinput",i,{capture:!0}),queueMicrotask(()=>{t.removeEventListener("beforeinput",i,{capture:!0}),n||this.#s(e)})}recordEdit(t,e,o,n){const i=function(t,e,o,n){return{type:"cell-edit",rowIndex:t,field:e,oldValue:o,newValue:n,timestamp:Date.now()}}(t,e,o,n);if(this.#e)return void this.#e.push(i);const a=r({undoStack:this.undoStack,redoStack:this.redoStack},i,this.config.maxHistorySize??100);this.undoStack=a.undoStack,this.redoStack=a.redoStack}beginTransaction(){this.#e&&o.throwDiagnostic(o.TRANSACTION_IN_PROGRESS,"Transaction already in progress. Call endTransaction() first."),this.#e=[]}endTransaction(){const t=this.#e;if(t||o.throwDiagnostic(o.NO_TRANSACTION,"No transaction in progress. Call beginTransaction() first."),this.#e=null,0===t.length)return;const e=1===t.length?t[0]:{type:"compound",actions:t,timestamp:Date.now()};const n=r({undoStack:this.undoStack,redoStack:this.redoStack},e,this.config.maxHistorySize??100);this.undoStack=n.undoStack,this.redoStack=n.redoStack}undo(){const t=a({undoStack:this.undoStack,redoStack:this.redoStack});return t.action&&(this.#i(t.action,"undo"),this.undoStack=t.newState.undoStack,this.redoStack=t.newState.redoStack,this.#r(t.action),this.requestRenderWithFocus()),t.action}redo(){const t=c({undoStack:this.undoStack,redoStack:this.redoStack});return t.action&&(this.#i(t.action,"redo"),this.undoStack=t.newState.undoStack,this.redoStack=t.newState.redoStack,this.#r(t.action),this.requestRenderWithFocus()),t.action}canUndo(){return{undoStack:this.undoStack,redoStack:this.redoStack}.undoStack.length>0}canRedo(){return{undoStack:this.undoStack,redoStack:this.redoStack}.redoStack.length>0}clearHistory(){const t={undoStack:[],redoStack:[]};this.undoStack=t.undoStack,this.redoStack=t.redoStack,this.#e=null}getUndoStack(){return[...this.undoStack]}getRedoStack(){return[...this.redoStack]}}t.UndoRedoPlugin=s,Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})});
2
2
  //# sourceMappingURL=undo-redo.umd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"undo-redo.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/undo-redo/history.ts","../../../../../libs/grid/src/lib/plugins/undo-redo/UndoRedoPlugin.ts"],"sourcesContent":["/**\n * Undo/Redo History Management\n *\n * Pure functions for managing the undo/redo stacks.\n * These functions are stateless and return new state objects.\n */\n\nimport type { CompoundEditAction, EditAction, UndoRedoAction, UndoRedoState } from './types';\n\n/**\n * Push a new action onto the undo stack.\n * Clears the redo stack since new actions invalidate redo history.\n *\n * @param state - Current undo/redo state\n * @param action - The action to add\n * @param maxSize - Maximum history size\n * @returns New state with the action added\n */\nexport function pushAction(state: UndoRedoState, action: UndoRedoAction, maxSize: number): UndoRedoState {\n const undoStack = [...state.undoStack, action];\n\n // Trim oldest actions if over max size\n while (undoStack.length > maxSize) {\n undoStack.shift();\n }\n\n return {\n undoStack,\n redoStack: [], // Clear redo on new action\n };\n}\n\n/**\n * Undo the most recent action.\n * Moves the action from undo stack to redo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was undone (or null if nothing to undo)\n */\nexport function undo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.undoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const undoStack = [...state.undoStack];\n const action = undoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack,\n redoStack: [...state.redoStack, action],\n },\n action,\n };\n}\n\n/**\n * Redo the most recently undone action.\n * Moves the action from redo stack back to undo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was redone (or null if nothing to redo)\n */\nexport function redo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.redoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const redoStack = [...state.redoStack];\n const action = redoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack: [...state.undoStack, action],\n redoStack,\n },\n action,\n };\n}\n\n/**\n * Check if there are any actions that can be undone.\n *\n * @param state - Current undo/redo state\n * @returns True if undo is available\n */\nexport function canUndo(state: UndoRedoState): boolean {\n return state.undoStack.length > 0;\n}\n\n/**\n * Check if there are any actions that can be redone.\n *\n * @param state - Current undo/redo state\n * @returns True if redo is available\n */\nexport function canRedo(state: UndoRedoState): boolean {\n return state.redoStack.length > 0;\n}\n\n/**\n * Clear all history, returning an empty state.\n *\n * @returns Fresh empty state\n */\nexport function clearHistory(): UndoRedoState {\n return { undoStack: [], redoStack: [] };\n}\n\n/**\n * Create a new edit action with the current timestamp.\n *\n * @param rowIndex - The row index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n * @returns A new EditAction object\n */\nexport function createEditAction(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): EditAction {\n return {\n type: 'cell-edit',\n rowIndex,\n field,\n oldValue,\n newValue,\n timestamp: Date.now(),\n };\n}\n\n/**\n * Create a compound action grouping multiple edits into a single undo/redo unit.\n *\n * @param actions - The individual edit actions to group (in chronological order)\n * @returns A CompoundEditAction wrapping all provided edits\n */\nexport function createCompoundAction(actions: EditAction[]): CompoundEditAction {\n return {\n type: 'compound',\n actions,\n timestamp: Date.now(),\n };\n}\n","/**\n * Undo/Redo Plugin (Class-based)\n *\n * Provides undo/redo functionality for cell edits in tbw-grid.\n * Supports Ctrl+Z/Cmd+Z for undo and Ctrl+Y/Cmd+Y (or Ctrl+Shift+Z) for redo.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { NO_TRANSACTION, TRANSACTION_IN_PROGRESS, throwDiagnostic } from '../../core/internal/diagnostics';\nimport { FOCUSABLE_EDITOR_SELECTOR } from '../../core/internal/rows';\nimport { BaseGridPlugin, type GridElement, type PluginDependency } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport {\n canRedo,\n canUndo,\n clearHistory,\n createCompoundAction,\n createEditAction,\n pushAction,\n redo,\n undo,\n} from './history';\nimport type { EditAction, UndoRedoAction, UndoRedoConfig, UndoRedoDetail } from './types';\n\n/**\n * Undo/Redo Plugin for tbw-grid\n *\n * Tracks all cell edits and lets users revert or replay changes with familiar keyboard\n * shortcuts (Ctrl+Z / Ctrl+Y). Maintains an in-memory history stack with configurable\n * depth—perfect for data entry workflows where mistakes happen.\n *\n * > **Required Dependency:** This plugin requires EditingPlugin to be loaded first.\n * > UndoRedo tracks the edit history that EditingPlugin creates.\n *\n * ## Installation\n *\n * ```ts\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Ctrl+Z` / `Cmd+Z` | Undo last edit |\n * | `Ctrl+Y` / `Cmd+Shift+Z` | Redo last undone edit |\n *\n * @example Basic Usage with EditingPlugin\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', editable: true },\n * { field: 'price', header: 'Price', type: 'number', editable: true },\n * ],\n * plugins: [\n * new EditingPlugin({ editOn: 'dblclick' }), // Required - must be first\n * new UndoRedoPlugin({ maxHistorySize: 50 }),\n * ],\n * };\n * ```\n *\n * @see {@link UndoRedoConfig} for configuration options\n * @see EditingPlugin for the required dependency\n *\n * @internal Extends BaseGridPlugin\n */\nexport class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> {\n /**\n * Plugin dependencies - UndoRedoPlugin requires EditingPlugin to track edits.\n *\n * The EditingPlugin must be loaded BEFORE this plugin in the plugins array.\n * @internal\n */\n static override readonly dependencies: PluginDependency[] = [\n { name: 'editing', required: true, reason: 'UndoRedoPlugin tracks cell edit history' },\n ];\n\n /** @internal */\n readonly name = 'undoRedo';\n\n /** @internal */\n protected override get defaultConfig(): Partial<UndoRedoConfig> {\n return {\n maxHistorySize: 100,\n };\n }\n\n // State as class properties\n private undoStack: UndoRedoAction[] = [];\n private redoStack: UndoRedoAction[] = [];\n\n /** Suppresses recording during undo/redo to prevent feedback loops. */\n #suppressRecording = false;\n\n /** Accumulates edits during a transaction; `null` when no transaction is active. */\n #transactionBuffer: EditAction[] | null = null;\n\n /**\n * Apply a value to a row cell, using `updateRow()` when possible so that\n * active editors (during row-edit mode) are notified via the `cell-change`\n * → `onValueChange` pipeline. Falls back to direct mutation when the row\n * has no ID.\n */\n #applyValue(action: EditAction, value: unknown): void {\n const rows = this.rows as Record<string, unknown>[];\n const row = rows[action.rowIndex];\n if (!row) return;\n\n // Prefer updateRow() — it emits `cell-change` events which notify active\n // editors via their `onValueChange` callbacks. Without this, undo/redo\n // during row-edit mode is invisible because the render pipeline skips\n // cells that have active editors.\n try {\n const rowId = this.grid.getRowId(row);\n if (rowId) {\n this.grid.updateRow(rowId, { [action.field]: value });\n return;\n }\n } catch {\n // No row ID configured — fall back to direct mutation\n }\n\n // Fallback: direct mutation (editors won't see the change during editing)\n row[action.field] = value;\n }\n\n /**\n * Move keyboard focus to the cell targeted by an undo/redo action.\n * If the grid is in row-edit mode and the cell has an active editor,\n * the editor input is focused so the user can continue editing.\n */\n #focusActionCell(action: EditAction): void {\n const internalGrid: GridHost = this.grid as unknown as GridHost;\n\n // Map field name → visible column index\n const colIdx = internalGrid._visibleColumns?.findIndex((c) => c.field === action.field) ?? -1;\n if (colIdx < 0) return;\n\n internalGrid._focusRow = action.rowIndex;\n internalGrid._focusCol = colIdx;\n\n // If we're in row-edit mode, focus the editor input in the target cell\n const rowEl = internalGrid.findRenderedRowElement?.(action.rowIndex);\n if (!rowEl) return;\n\n const cellEl = rowEl.querySelector(`.cell[data-col=\"${colIdx}\"]`) as HTMLElement | null;\n if (cellEl?.classList.contains(GridClasses.EDITING)) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n }\n\n /**\n * Apply value changes for a single or compound action.\n * Wraps `#applyValue` calls with `#suppressRecording` to prevent feedback loops.\n */\n #applyUndoRedoAction(action: UndoRedoAction, direction: 'undo' | 'redo'): void {\n this.#suppressRecording = true;\n if (action.type === 'compound') {\n const subActions = direction === 'undo' ? [...action.actions].reverse() : action.actions;\n for (const sub of subActions) {\n this.#applyValue(sub, direction === 'undo' ? sub.oldValue : sub.newValue);\n }\n } else {\n this.#applyValue(action, direction === 'undo' ? action.oldValue : action.newValue);\n }\n this.#suppressRecording = false;\n }\n\n /**\n * Focus the cell associated with an undo/redo action.\n * For compound actions, focuses the **last** action's cell. When consumers\n * use `beginTransaction()` + `recordEdit()` (cascaded fields) followed by\n * `queueMicrotask(() => endTransaction())`, the grid's auto-recorded\n * primary field edit is appended last. Focusing it ensures the cursor\n * lands on the field the user originally edited, not on a cascaded field\n * whose column may not even be visible.\n */\n #focusUndoRedoAction(action: UndoRedoAction): void {\n const target = action.type === 'compound' ? action.actions[action.actions.length - 1] : action;\n if (target) this.#focusActionCell(target);\n }\n\n /**\n * Subscribe to cell-edit-committed events from EditingPlugin.\n * @internal\n */\n override attach(grid: GridElement): void {\n super.attach(grid);\n // Auto-record edits via Event Bus\n this.on(\n 'cell-edit-committed',\n (detail: { rowIndex: number; field: string; oldValue: unknown; newValue: unknown }) => {\n // Skip recording during undo/redo operations. When undo/redo applies a\n // value via updateRow, two things can cause re-entry:\n // 1. updateRow → cell-change → onValueChange → editor triggers commit\n // 2. Browser native undo (if not fully suppressed) fires input event → commit\n // The suppress flag prevents these from corrupting the history stacks.\n if (this.#suppressRecording) return;\n this.recordEdit(detail.rowIndex, detail.field, detail.oldValue, detail.newValue);\n },\n );\n }\n\n /**\n * Clean up state when plugin is detached.\n * @internal\n */\n override detach(): void {\n this.undoStack = [];\n this.redoStack = [];\n this.#transactionBuffer = null;\n }\n\n /**\n * Handle keyboard shortcuts for undo/redo.\n * - Ctrl+Z / Cmd+Z: Undo\n * - Ctrl+Y / Cmd+Y / Ctrl+Shift+Z / Cmd+Shift+Z: Redo\n * @internal\n */\n override onKeyDown(event: KeyboardEvent): boolean {\n const isUndo = (event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey;\n const isRedo = (event.ctrlKey || event.metaKey) && (event.key === 'y' || (event.key === 'z' && event.shiftKey));\n\n if (isUndo) {\n // Prevent browser native undo on text inputs — it would conflict\n // with the grid's undo by mutating the input text independently,\n // triggering re-commits that cancel the grid undo.\n event.preventDefault();\n\n const result = undo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'undo');\n\n // Update state from result\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n\n this.emit<UndoRedoDetail>('undo', {\n action: result.action,\n type: 'undo',\n });\n\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return true;\n }\n\n if (isRedo) {\n // Prevent browser native redo — same reason as undo above\n event.preventDefault();\n\n const result = redo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'redo');\n\n // Update state from result\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n\n this.emit<UndoRedoDetail>('redo', {\n action: result.action,\n type: 'redo',\n });\n\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return true;\n }\n\n return false;\n }\n\n // #region Public API Methods\n\n /**\n * Record a cell edit for undo/redo tracking.\n * Call this when a cell value changes.\n *\n * @param rowIndex - The row index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n */\n recordEdit(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): void {\n const action = createEditAction(rowIndex, field, oldValue, newValue);\n\n // Buffer during transactions instead of pushing to undo stack\n if (this.#transactionBuffer) {\n this.#transactionBuffer.push(action);\n return;\n }\n\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Begin grouping subsequent edits into a single compound action.\n *\n * While a transaction is active, all `recordEdit()` calls (both manual\n * and auto-recorded from `cell-edit-committed`) are buffered instead of\n * pushed to the undo stack. Call `endTransaction()` to finalize the group.\n *\n * **Typical usage** — group a user edit with its cascaded side-effects:\n *\n * ```ts\n * grid.on('cell-commit', () => {\n * const undoRedo = grid.getPluginByName('undoRedo');\n * undoRedo.beginTransaction();\n *\n * // Record cascaded updates (these won't auto-record)\n * const oldB = row.fieldB;\n * undoRedo.recordEdit(rowIndex, 'fieldB', oldB, computedB);\n * grid.updateRow(rowId, { fieldB: computedB });\n *\n * // End after the auto-recorded original edit is captured\n * queueMicrotask(() => undoRedo.endTransaction());\n * });\n * ```\n *\n * @throws Error if a transaction is already in progress\n */\n beginTransaction(): void {\n if (this.#transactionBuffer) {\n throwDiagnostic(TRANSACTION_IN_PROGRESS, 'Transaction already in progress. Call endTransaction() first.');\n }\n this.#transactionBuffer = [];\n }\n\n /**\n * Finalize the current transaction, wrapping all buffered edits into a\n * single compound action on the undo stack.\n *\n * - If the buffer contains multiple edits, they are wrapped in a `CompoundEditAction`.\n * - If the buffer contains a single edit, it is pushed as a regular `EditAction`.\n * - If the buffer is empty, this is a no-op.\n *\n * Undoing a compound action reverts all edits in reverse order; redoing\n * replays them in forward order.\n *\n * @throws Error if no transaction is in progress\n */\n endTransaction(): void {\n const buffer = this.#transactionBuffer;\n if (!buffer) {\n throwDiagnostic(NO_TRANSACTION, 'No transaction in progress. Call beginTransaction() first.');\n }\n this.#transactionBuffer = null;\n\n if (buffer.length === 0) return;\n\n const action: UndoRedoAction = buffer.length === 1 ? buffer[0] : createCompoundAction(buffer);\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Programmatically undo the last action.\n *\n * @returns The undone action, or null if nothing to undo\n */\n undo(): UndoRedoAction | null {\n const result = undo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'undo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Programmatically redo the last undone action.\n *\n * @returns The redone action, or null if nothing to redo\n */\n redo(): UndoRedoAction | null {\n const result = redo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'redo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Check if there are any actions that can be undone.\n */\n canUndo(): boolean {\n return canUndo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Check if there are any actions that can be redone.\n */\n canRedo(): boolean {\n return canRedo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Clear all undo/redo history.\n */\n clearHistory(): void {\n const newState = clearHistory();\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n this.#transactionBuffer = null;\n }\n\n /**\n * Get a copy of the current undo stack.\n */\n getUndoStack(): UndoRedoAction[] {\n return [...this.undoStack];\n }\n\n /**\n * Get a copy of the current redo stack.\n */\n getRedoStack(): UndoRedoAction[] {\n return [...this.redoStack];\n }\n // #endregion\n}\n"],"names":["pushAction","state","action","maxSize","undoStack","length","shift","redoStack","undo","newState","pop","redo","UndoRedoPlugin","BaseGridPlugin","static","name","required","reason","defaultConfig","maxHistorySize","suppressRecording","transactionBuffer","applyValue","value","row","this","rows","rowIndex","rowId","grid","getRowId","updateRow","field","focusActionCell","internalGrid","colIdx","_visibleColumns","findIndex","c","_focusRow","_focusCol","rowEl","findRenderedRowElement","cellEl","querySelector","classList","contains","GridClasses","EDITING","editor","FOCUSABLE_EDITOR_SELECTOR","focus","preventScroll","applyUndoRedoAction","direction","type","subActions","actions","reverse","sub","oldValue","newValue","focusUndoRedoAction","target","attach","super","on","detail","recordEdit","detach","onKeyDown","event","isUndo","ctrlKey","metaKey","key","shiftKey","isRedo","preventDefault","result","emit","requestRenderWithFocus","timestamp","Date","now","createEditAction","push","config","beginTransaction","throwDiagnostic","TRANSACTION_IN_PROGRESS","endTransaction","buffer","NO_TRANSACTION","canUndo","canRedo","clearHistory","getUndoStack","getRedoStack"],"mappings":"yjBAkBO,SAASA,EAAWC,EAAsBC,EAAwBC,GACvE,MAAMC,EAAY,IAAIH,EAAMG,UAAWF,GAGvC,KAAOE,EAAUC,OAASF,GACxBC,EAAUE,QAGZ,MAAO,CACLF,YACAG,UAAW,GAEf,CASO,SAASC,EAAKP,GAInB,GAA+B,IAA3BA,EAAMG,UAAUC,OAClB,MAAO,CAAEI,SAAUR,EAAOC,OAAQ,MAGpC,MAAME,EAAY,IAAIH,EAAMG,WACtBF,EAASE,EAAUM,MAIzB,OAAKR,EAIE,CACLO,SAAU,CACRL,YACAG,UAAW,IAAIN,EAAMM,UAAWL,IAElCA,UARO,CAAEO,SAAUR,EAAOC,OAAQ,KAUtC,CASO,SAASS,EAAKV,GAInB,GAA+B,IAA3BA,EAAMM,UAAUF,OAClB,MAAO,CAAEI,SAAUR,EAAOC,OAAQ,MAGpC,MAAMK,EAAY,IAAIN,EAAMM,WACtBL,EAASK,EAAUG,MAIzB,OAAKR,EAIE,CACLO,SAAU,CACRL,UAAW,IAAIH,EAAMG,UAAWF,GAChCK,aAEFL,UARO,CAAEO,SAAUR,EAAOC,OAAQ,KAUtC,CCxBO,MAAMU,UAAuBC,EAAAA,eAOlCC,oBAA4D,CAC1D,CAAEC,KAAM,UAAWC,UAAU,EAAMC,OAAQ,4CAIpCF,KAAO,WAGhB,iBAAuBG,GACrB,MAAO,CACLC,eAAgB,IAEpB,CAGQf,UAA8B,GAC9BG,UAA8B,GAGtCa,IAAqB,EAGrBC,GAA0C,KAQ1C,EAAAC,CAAYpB,EAAoBqB,GAC9B,MACMC,EADOC,KAAKC,KACDxB,EAAOyB,UACxB,GAAKH,EAAL,CAMA,IACE,MAAMI,EAAQH,KAAKI,KAAKC,SAASN,GACjC,GAAII,EAEF,YADAH,KAAKI,KAAKE,UAAUH,EAAO,CAAE,CAAC1B,EAAO8B,OAAQT,GAGjD,CAAA,MAEA,CAGAC,EAAItB,EAAO8B,OAAST,CAjBV,CAkBZ,CAOA,EAAAU,CAAiB/B,GACf,MAAMgC,EAAyBT,KAAKI,KAG9BM,EAASD,EAAaE,iBAAiBC,UAAWC,GAAMA,EAAEN,QAAU9B,EAAO8B,SAAU,EAC3F,GAAIG,EAAS,EAAG,OAEhBD,EAAaK,UAAYrC,EAAOyB,SAChCO,EAAaM,UAAYL,EAGzB,MAAMM,EAAQP,EAAaQ,yBAAyBxC,EAAOyB,UAC3D,IAAKc,EAAO,OAEZ,MAAME,EAASF,EAAMG,cAAc,mBAAmBT,OACtD,GAAIQ,GAAQE,UAAUC,SAASC,EAAAA,YAAYC,SAAU,CACnD,MAAMC,EAASN,EAAOC,cAAcM,6BACpCD,GAAQE,MAAM,CAAEC,eAAe,GACjC,CACF,CAMA,EAAAC,CAAqBnD,EAAwBoD,GAE3C,GADA7B,MAAKL,GAAqB,EACN,aAAhBlB,EAAOqD,KAAqB,CAC9B,MAAMC,EAA2B,SAAdF,EAAuB,IAAIpD,EAAOuD,SAASC,UAAYxD,EAAOuD,QACjF,IAAA,MAAWE,KAAOH,EAChB/B,MAAKH,EAAYqC,EAAmB,SAAdL,EAAuBK,EAAIC,SAAWD,EAAIE,SAEpE,MACEpC,MAAKH,EAAYpB,EAAsB,SAAdoD,EAAuBpD,EAAO0D,SAAW1D,EAAO2D,UAE3EpC,MAAKL,GAAqB,CAC5B,CAWA,EAAA0C,CAAqB5D,GACnB,MAAM6D,EAAyB,aAAhB7D,EAAOqD,KAAsBrD,EAAOuD,QAAQvD,EAAOuD,QAAQpD,OAAS,GAAKH,EACpF6D,GAAQtC,MAAKQ,EAAiB8B,EACpC,CAMS,MAAAC,CAAOnC,GACdoC,MAAMD,OAAOnC,GAEbJ,KAAKyC,GACH,sBACCC,IAMK1C,MAAKL,GACTK,KAAK2C,WAAWD,EAAOxC,SAAUwC,EAAOnC,MAAOmC,EAAOP,SAAUO,EAAON,WAG7E,CAMS,MAAAQ,GACP5C,KAAKrB,UAAY,GACjBqB,KAAKlB,UAAY,GACjBkB,MAAKJ,EAAqB,IAC5B,CAQS,SAAAiD,CAAUC,GACjB,MAAMC,GAAUD,EAAME,SAAWF,EAAMG,UAA0B,MAAdH,EAAMI,MAAgBJ,EAAMK,SACzEC,GAAUN,EAAME,SAAWF,EAAMG,WAA2B,MAAdH,EAAMI,KAA8B,MAAdJ,EAAMI,KAAeJ,EAAMK,UAErG,GAAIJ,EAAQ,CAIVD,EAAMO,iBAEN,MAAMC,EAASvE,EAAK,CAAEJ,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,YAgBjE,OAfIwE,EAAO7E,SACTuB,MAAK4B,EAAqB0B,EAAO7E,OAAQ,QAGzCuB,KAAKrB,UAAY2E,EAAOtE,SAASL,UACjCqB,KAAKlB,UAAYwE,EAAOtE,SAASF,UAEjCkB,KAAKuD,KAAqB,OAAQ,CAChC9E,OAAQ6E,EAAO7E,OACfqD,KAAM,SAGR9B,MAAKqC,EAAqBiB,EAAO7E,QACjCuB,KAAKwD,2BAEA,CACT,CAEA,GAAIJ,EAAQ,CAEVN,EAAMO,iBAEN,MAAMC,EAASpE,EAAK,CAAEP,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,YAgBjE,OAfIwE,EAAO7E,SACTuB,MAAK4B,EAAqB0B,EAAO7E,OAAQ,QAGzCuB,KAAKrB,UAAY2E,EAAOtE,SAASL,UACjCqB,KAAKlB,UAAYwE,EAAOtE,SAASF,UAEjCkB,KAAKuD,KAAqB,OAAQ,CAChC9E,OAAQ6E,EAAO7E,OACfqD,KAAM,SAGR9B,MAAKqC,EAAqBiB,EAAO7E,QACjCuB,KAAKwD,2BAEA,CACT,CAEA,OAAO,CACT,CAaA,UAAAb,CAAWzC,EAAkBK,EAAe4B,EAAmBC,GAC7D,MAAM3D,ED7JH,SAA0ByB,EAAkBK,EAAe4B,EAAmBC,GACnF,MAAO,CACLN,KAAM,YACN5B,WACAK,QACA4B,WACAC,WACAqB,UAAWC,KAAKC,MAEpB,CCoJmBC,CAAiB1D,EAAUK,EAAO4B,EAAUC,GAG3D,GAAIpC,MAAKJ,EAEP,YADAI,MAAKJ,EAAmBiE,KAAKpF,GAI/B,MAAMO,EAAWT,EACf,CAAEI,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,WAC7CL,EACAuB,KAAK8D,OAAOpE,gBAAkB,KAEhCM,KAAKrB,UAAYK,EAASL,UAC1BqB,KAAKlB,UAAYE,EAASF,SAC5B,CA4BA,gBAAAiF,GACM/D,MAAKJ,GACPoE,EAAAA,gBAAgBC,EAAAA,wBAAyB,iEAE3CjE,MAAKJ,EAAqB,EAC5B,CAeA,cAAAsE,GACE,MAAMC,EAASnE,MAAKJ,EAMpB,GALKuE,GACHH,EAAAA,gBAAgBI,EAAAA,eAAgB,8DAElCpE,MAAKJ,EAAqB,KAEJ,IAAlBuE,EAAOvF,OAAc,OAEzB,MAAMH,EAA2C,IAAlB0F,EAAOvF,OAAeuF,EAAO,GDnNvD,CACLrC,KAAM,WACNE,QCiNsFmC,EDhNtFV,UAAWC,KAAKC,OCiNhB,MAAM3E,EAAWT,EACf,CAAEI,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,WAC7CL,EACAuB,KAAK8D,OAAOpE,gBAAkB,KAEhCM,KAAKrB,UAAYK,EAASL,UAC1BqB,KAAKlB,UAAYE,EAASF,SAC5B,CAOA,IAAAC,GACE,MAAMuE,EAASvE,EAAK,CAAEJ,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,YAQjE,OAPIwE,EAAO7E,SACTuB,MAAK4B,EAAqB0B,EAAO7E,OAAQ,QACzCuB,KAAKrB,UAAY2E,EAAOtE,SAASL,UACjCqB,KAAKlB,UAAYwE,EAAOtE,SAASF,UACjCkB,MAAKqC,EAAqBiB,EAAO7E,QACjCuB,KAAKwD,0BAEAF,EAAO7E,MAChB,CAOA,IAAAS,GACE,MAAMoE,EAASpE,EAAK,CAAEP,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,YAQjE,OAPIwE,EAAO7E,SACTuB,MAAK4B,EAAqB0B,EAAO7E,OAAQ,QACzCuB,KAAKrB,UAAY2E,EAAOtE,SAASL,UACjCqB,KAAKlB,UAAYwE,EAAOtE,SAASF,UACjCkB,MAAKqC,EAAqBiB,EAAO7E,QACjCuB,KAAKwD,0BAEAF,EAAO7E,MAChB,CAKA,OAAA4F,GACE,MAAe,CAAE1F,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,WDpTjDH,UAAUC,OAAS,CCqThC,CAKA,OAAA0F,GACE,MAAe,CAAE3F,UAAWqB,KAAKrB,UAAWG,UAAWkB,KAAKlB,WDjTjDA,UAAUF,OAAS,CCkThC,CAKA,YAAA2F,GACE,MAAMvF,ED/SD,CAAEL,UAAW,GAAIG,UAAW,ICgTjCkB,KAAKrB,UAAYK,EAASL,UAC1BqB,KAAKlB,UAAYE,EAASF,UAC1BkB,MAAKJ,EAAqB,IAC5B,CAKA,YAAA4E,GACE,MAAO,IAAIxE,KAAKrB,UAClB,CAKA,YAAA8F,GACE,MAAO,IAAIzE,KAAKlB,UAClB"}
1
+ {"version":3,"file":"undo-redo.umd.js","sources":["../../../../../libs/grid/src/lib/plugins/undo-redo/history.ts","../../../../../libs/grid/src/lib/plugins/undo-redo/UndoRedoPlugin.ts"],"sourcesContent":["/**\n * Undo/Redo History Management\n *\n * Pure functions for managing the undo/redo stacks.\n * These functions are stateless and return new state objects.\n */\n\nimport type { CompoundEditAction, EditAction, UndoRedoAction, UndoRedoState } from './types';\n\n/**\n * Push a new action onto the undo stack.\n * Clears the redo stack since new actions invalidate redo history.\n *\n * @param state - Current undo/redo state\n * @param action - The action to add\n * @param maxSize - Maximum history size\n * @returns New state with the action added\n */\nexport function pushAction(state: UndoRedoState, action: UndoRedoAction, maxSize: number): UndoRedoState {\n const undoStack = [...state.undoStack, action];\n\n // Trim oldest actions if over max size\n while (undoStack.length > maxSize) {\n undoStack.shift();\n }\n\n return {\n undoStack,\n redoStack: [], // Clear redo on new action\n };\n}\n\n/**\n * Undo the most recent action.\n * Moves the action from undo stack to redo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was undone (or null if nothing to undo)\n */\nexport function undo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.undoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const undoStack = [...state.undoStack];\n const action = undoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack,\n redoStack: [...state.redoStack, action],\n },\n action,\n };\n}\n\n/**\n * Redo the most recently undone action.\n * Moves the action from redo stack back to undo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was redone (or null if nothing to redo)\n */\nexport function redo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.redoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const redoStack = [...state.redoStack];\n const action = redoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack: [...state.undoStack, action],\n redoStack,\n },\n action,\n };\n}\n\n/**\n * Check if there are any actions that can be undone.\n *\n * @param state - Current undo/redo state\n * @returns True if undo is available\n */\nexport function canUndo(state: UndoRedoState): boolean {\n return state.undoStack.length > 0;\n}\n\n/**\n * Check if there are any actions that can be redone.\n *\n * @param state - Current undo/redo state\n * @returns True if redo is available\n */\nexport function canRedo(state: UndoRedoState): boolean {\n return state.redoStack.length > 0;\n}\n\n/**\n * Clear all history, returning an empty state.\n *\n * @returns Fresh empty state\n */\nexport function clearHistory(): UndoRedoState {\n return { undoStack: [], redoStack: [] };\n}\n\n/**\n * Create a new edit action with the current timestamp.\n *\n * @param rowIndex - The row index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n * @returns A new EditAction object\n */\nexport function createEditAction(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): EditAction {\n return {\n type: 'cell-edit',\n rowIndex,\n field,\n oldValue,\n newValue,\n timestamp: Date.now(),\n };\n}\n\n/**\n * Create a compound action grouping multiple edits into a single undo/redo unit.\n *\n * @param actions - The individual edit actions to group (in chronological order)\n * @returns A CompoundEditAction wrapping all provided edits\n */\nexport function createCompoundAction(actions: EditAction[]): CompoundEditAction {\n return {\n type: 'compound',\n actions,\n timestamp: Date.now(),\n };\n}\n","/**\n * Undo/Redo Plugin (Class-based)\n *\n * Provides undo/redo functionality for cell edits in tbw-grid.\n * Supports Ctrl+Z/Cmd+Z for undo and Ctrl+Y/Cmd+Y (or Ctrl+Shift+Z) for redo.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { NO_TRANSACTION, TRANSACTION_IN_PROGRESS, throwDiagnostic } from '../../core/internal/diagnostics';\nimport { FOCUSABLE_EDITOR_SELECTOR } from '../../core/internal/rows';\nimport { BaseGridPlugin, type GridElement, type PluginDependency } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport {\n canRedo,\n canUndo,\n clearHistory,\n createCompoundAction,\n createEditAction,\n pushAction,\n redo,\n undo,\n} from './history';\nimport type { EditAction, UndoRedoAction, UndoRedoConfig, UndoRedoDetail } from './types';\n\n/**\n * Undo/Redo Plugin for tbw-grid\n *\n * Tracks all cell edits and lets users revert or replay changes with familiar keyboard\n * shortcuts (Ctrl+Z / Ctrl+Y). Maintains an in-memory history stack with configurable\n * depth—perfect for data entry workflows where mistakes happen.\n *\n * > **Required Dependency:** This plugin requires EditingPlugin to be loaded first.\n * > UndoRedo tracks the edit history that EditingPlugin creates.\n *\n * ## Installation\n *\n * ```ts\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Ctrl+Z` / `Cmd+Z` | Undo last edit |\n * | `Ctrl+Y` / `Cmd+Shift+Z` | Redo last undone edit |\n *\n * @example Basic Usage with EditingPlugin\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', editable: true },\n * { field: 'price', header: 'Price', type: 'number', editable: true },\n * ],\n * plugins: [\n * new EditingPlugin({ editOn: 'dblclick' }), // Required - must be first\n * new UndoRedoPlugin({ maxHistorySize: 50 }),\n * ],\n * };\n * ```\n *\n * @see {@link UndoRedoConfig} for configuration options\n * @see EditingPlugin for the required dependency\n *\n * @internal Extends BaseGridPlugin\n */\nexport class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> {\n /**\n * Plugin dependencies - UndoRedoPlugin requires EditingPlugin to track edits.\n *\n * The EditingPlugin must be loaded BEFORE this plugin in the plugins array.\n * @internal\n */\n static override readonly dependencies: PluginDependency[] = [\n { name: 'editing', required: true, reason: 'UndoRedoPlugin tracks cell edit history' },\n ];\n\n /** @internal */\n readonly name = 'undoRedo';\n\n /** @internal */\n protected override get defaultConfig(): Partial<UndoRedoConfig> {\n return {\n maxHistorySize: 100,\n };\n }\n\n // State as class properties\n private undoStack: UndoRedoAction[] = [];\n private redoStack: UndoRedoAction[] = [];\n\n /** Suppresses recording during undo/redo to prevent feedback loops. */\n #suppressRecording = false;\n\n /** Accumulates edits during a transaction; `null` when no transaction is active. */\n #transactionBuffer: EditAction[] | null = null;\n\n /**\n * Apply a value to a row cell, using `updateRow()` when possible so that\n * active editors (during row-edit mode) are notified via the `cell-change`\n * → `onValueChange` pipeline. Falls back to direct mutation when the row\n * has no ID.\n */\n #applyValue(action: EditAction, value: unknown): void {\n const rows = this.rows as Record<string, unknown>[];\n const row = rows[action.rowIndex];\n if (!row) return;\n\n // Prefer updateRow() — it emits `cell-change` events which notify active\n // editors via their `onValueChange` callbacks. Without this, undo/redo\n // during row-edit mode is invisible because the render pipeline skips\n // cells that have active editors.\n try {\n const rowId = this.grid.getRowId(row);\n if (rowId) {\n this.grid.updateRow(rowId, { [action.field]: value });\n return;\n }\n } catch {\n // No row ID configured — fall back to direct mutation\n }\n\n // Fallback: direct mutation (editors won't see the change during editing)\n row[action.field] = value;\n }\n\n /**\n * Move keyboard focus to the cell targeted by an undo/redo action.\n * If the grid is in row-edit mode and the cell has an active editor,\n * the editor input is focused so the user can continue editing.\n */\n #focusActionCell(action: EditAction): void {\n const internalGrid: GridHost = this.grid as unknown as GridHost;\n\n // Map field name → visible column index\n const colIdx = internalGrid._visibleColumns?.findIndex((c) => c.field === action.field) ?? -1;\n if (colIdx < 0) return;\n\n internalGrid._focusRow = action.rowIndex;\n internalGrid._focusCol = colIdx;\n\n // If we're in row-edit mode, focus the editor input in the target cell\n const rowEl = internalGrid.findRenderedRowElement?.(action.rowIndex);\n if (!rowEl) return;\n\n const cellEl = rowEl.querySelector(`.cell[data-col=\"${colIdx}\"]`) as HTMLElement | null;\n if (cellEl?.classList.contains(GridClasses.EDITING)) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n }\n\n /**\n * Apply value changes for a single or compound action.\n * Wraps `#applyValue` calls with `#suppressRecording` to prevent feedback loops.\n */\n #applyUndoRedoAction(action: UndoRedoAction, direction: 'undo' | 'redo'): void {\n this.#suppressRecording = true;\n if (action.type === 'compound') {\n const subActions = direction === 'undo' ? [...action.actions].reverse() : action.actions;\n for (const sub of subActions) {\n this.#applyValue(sub, direction === 'undo' ? sub.oldValue : sub.newValue);\n }\n } else {\n this.#applyValue(action, direction === 'undo' ? action.oldValue : action.newValue);\n }\n this.#suppressRecording = false;\n }\n\n /**\n * Focus the cell associated with an undo/redo action.\n * For compound actions, focuses the **last** action's cell. When consumers\n * use `beginTransaction()` + `recordEdit()` (cascaded fields) followed by\n * `queueMicrotask(() => endTransaction())`, the grid's auto-recorded\n * primary field edit is appended last. Focusing it ensures the cursor\n * lands on the field the user originally edited, not on a cascaded field\n * whose column may not even be visible.\n */\n #focusUndoRedoAction(action: UndoRedoAction): void {\n const target = action.type === 'compound' ? action.actions[action.actions.length - 1] : action;\n if (target) this.#focusActionCell(target);\n }\n\n /**\n * Subscribe to cell-edit-committed events from EditingPlugin.\n * @internal\n */\n override attach(grid: GridElement): void {\n super.attach(grid);\n // Auto-record edits via Event Bus\n this.on(\n 'cell-edit-committed',\n (detail: { rowIndex: number; field: string; oldValue: unknown; newValue: unknown }) => {\n // Skip recording during undo/redo operations. When undo/redo applies a\n // value via updateRow, two things can cause re-entry:\n // 1. updateRow → cell-change → onValueChange → editor triggers commit\n // 2. Browser native undo (if not fully suppressed) fires input event → commit\n // The suppress flag prevents these from corrupting the history stacks.\n if (this.#suppressRecording) return;\n this.recordEdit(detail.rowIndex, detail.field, detail.oldValue, detail.newValue);\n },\n );\n }\n\n /**\n * Clean up state when plugin is detached.\n * @internal\n */\n override detach(): void {\n this.undoStack = [];\n this.redoStack = [];\n this.#transactionBuffer = null;\n }\n\n /**\n * Handle keyboard shortcuts for undo/redo.\n * - Ctrl+Z / Cmd+Z: Undo\n * - Ctrl+Y / Cmd+Y / Ctrl+Shift+Z / Cmd+Shift+Z: Redo\n *\n * When the keystroke originates from a focusable form control (e.g. an\n * active cell editor input, a filter text box) we defer to the browser's\n * native undo/redo first. We watch for a `beforeinput` event with\n * `inputType === 'historyUndo'` / `'historyRedo'` on the target — if the\n * browser had something to undo, it consumes the keystroke and we do\n * nothing; if it didn't (history depleted), we fall back to the grid's\n * own undo/redo. This lets users undo their typing/paste inside an open\n * editor, then continue undoing committed cell edits with the same key.\n * @internal\n */\n override onKeyDown(event: KeyboardEvent): boolean {\n const isUndo = (event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey;\n const isRedo = (event.ctrlKey || event.metaKey) && (event.key === 'y' || (event.key === 'z' && event.shiftKey));\n\n if (!isUndo && !isRedo) return false;\n\n const kind: 'undo' | 'redo' = isUndo ? 'undo' : 'redo';\n\n if (this.#isNativeHistoryTarget(event.target)) {\n // Let the browser try its native undo/redo first. Only run ours if\n // the browser had nothing left in its own history.\n this.#deferToNativeHistory(event.target as HTMLElement, kind);\n return true;\n }\n\n // Prevent browser native undo on text inputs — it would conflict with\n // the grid's undo by mutating the input text independently, triggering\n // re-commits that cancel the grid undo.\n event.preventDefault();\n this.#performHistory(kind);\n return true;\n }\n\n /**\n * Apply an undo or redo against the grid's own history stack.\n */\n #performHistory(kind: 'undo' | 'redo'): void {\n const state = { undoStack: this.undoStack, redoStack: this.redoStack };\n const result = kind === 'undo' ? undo(state) : redo(state);\n if (!result.action) return;\n\n this.#applyUndoRedoAction(result.action, kind);\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n\n this.emit<UndoRedoDetail>(kind, { action: result.action, type: kind });\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n\n /**\n * True when the keystroke target is a form control with its own native\n * undo history (text inputs, textareas, contenteditable). `<select>` is\n * intentionally excluded — it has no undo history.\n */\n #isNativeHistoryTarget(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n return !!target.closest('input, textarea, [contenteditable=\"\"], [contenteditable=\"true\"]');\n }\n\n /**\n * Watch for the browser's native undo/redo on `target`. The browser fires\n * `beforeinput` with `inputType: 'historyUndo' | 'historyRedo'` when it\n * actually has something to undo/redo. If we don't observe that within a\n * microtask of the keydown returning, the native history is depleted and\n * we run the grid's own undo/redo to extend the chain.\n */\n #deferToNativeHistory(target: HTMLElement, kind: 'undo' | 'redo'): void {\n const wantedInputType = kind === 'undo' ? 'historyUndo' : 'historyRedo';\n let nativeHandled = false;\n\n const onBeforeInput = (e: Event) => {\n if ((e as InputEvent).inputType === wantedInputType) nativeHandled = true;\n };\n target.addEventListener('beforeinput', onBeforeInput, { capture: true });\n\n queueMicrotask(() => {\n target.removeEventListener('beforeinput', onBeforeInput, { capture: true } as EventListenerOptions);\n if (!nativeHandled) this.#performHistory(kind);\n });\n }\n\n // #region Public API Methods\n\n /**\n * Record a cell edit for undo/redo tracking.\n * Call this when a cell value changes.\n *\n * @param rowIndex - The row index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n */\n recordEdit(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): void {\n const action = createEditAction(rowIndex, field, oldValue, newValue);\n\n // Buffer during transactions instead of pushing to undo stack\n if (this.#transactionBuffer) {\n this.#transactionBuffer.push(action);\n return;\n }\n\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Begin grouping subsequent edits into a single compound action.\n *\n * While a transaction is active, all `recordEdit()` calls (both manual\n * and auto-recorded from `cell-edit-committed`) are buffered instead of\n * pushed to the undo stack. Call `endTransaction()` to finalize the group.\n *\n * **Typical usage** — group a user edit with its cascaded side-effects:\n *\n * ```ts\n * grid.on('cell-commit', () => {\n * const undoRedo = grid.getPluginByName('undoRedo');\n * undoRedo.beginTransaction();\n *\n * // Record cascaded updates (these won't auto-record)\n * const oldB = row.fieldB;\n * undoRedo.recordEdit(rowIndex, 'fieldB', oldB, computedB);\n * grid.updateRow(rowId, { fieldB: computedB });\n *\n * // End after the auto-recorded original edit is captured\n * queueMicrotask(() => undoRedo.endTransaction());\n * });\n * ```\n *\n * @throws Error if a transaction is already in progress\n */\n beginTransaction(): void {\n if (this.#transactionBuffer) {\n throwDiagnostic(TRANSACTION_IN_PROGRESS, 'Transaction already in progress. Call endTransaction() first.');\n }\n this.#transactionBuffer = [];\n }\n\n /**\n * Finalize the current transaction, wrapping all buffered edits into a\n * single compound action on the undo stack.\n *\n * - If the buffer contains multiple edits, they are wrapped in a `CompoundEditAction`.\n * - If the buffer contains a single edit, it is pushed as a regular `EditAction`.\n * - If the buffer is empty, this is a no-op.\n *\n * Undoing a compound action reverts all edits in reverse order; redoing\n * replays them in forward order.\n *\n * @throws Error if no transaction is in progress\n */\n endTransaction(): void {\n const buffer = this.#transactionBuffer;\n if (!buffer) {\n throwDiagnostic(NO_TRANSACTION, 'No transaction in progress. Call beginTransaction() first.');\n }\n this.#transactionBuffer = null;\n\n if (buffer.length === 0) return;\n\n const action: UndoRedoAction = buffer.length === 1 ? buffer[0] : createCompoundAction(buffer);\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Programmatically undo the last action.\n *\n * @returns The undone action, or null if nothing to undo\n */\n undo(): UndoRedoAction | null {\n const result = undo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'undo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Programmatically redo the last undone action.\n *\n * @returns The redone action, or null if nothing to redo\n */\n redo(): UndoRedoAction | null {\n const result = redo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'redo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Check if there are any actions that can be undone.\n */\n canUndo(): boolean {\n return canUndo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Check if there are any actions that can be redone.\n */\n canRedo(): boolean {\n return canRedo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Clear all undo/redo history.\n */\n clearHistory(): void {\n const newState = clearHistory();\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n this.#transactionBuffer = null;\n }\n\n /**\n * Get a copy of the current undo stack.\n */\n getUndoStack(): UndoRedoAction[] {\n return [...this.undoStack];\n }\n\n /**\n * Get a copy of the current redo stack.\n */\n getRedoStack(): UndoRedoAction[] {\n return [...this.redoStack];\n }\n // #endregion\n}\n"],"names":["pushAction","state","action","maxSize","undoStack","length","shift","redoStack","undo","newState","pop","redo","UndoRedoPlugin","BaseGridPlugin","static","name","required","reason","defaultConfig","maxHistorySize","suppressRecording","transactionBuffer","applyValue","value","row","this","rows","rowIndex","rowId","grid","getRowId","updateRow","field","focusActionCell","internalGrid","colIdx","_visibleColumns","findIndex","c","_focusRow","_focusCol","rowEl","findRenderedRowElement","cellEl","querySelector","classList","contains","GridClasses","EDITING","editor","FOCUSABLE_EDITOR_SELECTOR","focus","preventScroll","applyUndoRedoAction","direction","type","subActions","actions","reverse","sub","oldValue","newValue","focusUndoRedoAction","target","attach","super","on","detail","recordEdit","detach","onKeyDown","event","isUndo","ctrlKey","metaKey","key","shiftKey","isRedo","kind","isNativeHistoryTarget","deferToNativeHistory","preventDefault","performHistory","result","emit","requestRenderWithFocus","Element","closest","wantedInputType","nativeHandled","onBeforeInput","e","inputType","addEventListener","capture","queueMicrotask","removeEventListener","timestamp","Date","now","createEditAction","push","config","beginTransaction","throwDiagnostic","TRANSACTION_IN_PROGRESS","endTransaction","buffer","NO_TRANSACTION","canUndo","canRedo","clearHistory","getUndoStack","getRedoStack"],"mappings":"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"}