@toolbox-web/grid 3.9.2 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/all.js +1 -1
  2. package/all.js.map +1 -1
  3. package/index.js +1 -1
  4. package/index.js.map +1 -1
  5. package/lib/core/types.d.ts +6 -3
  6. package/lib/features/clipboard.js.map +1 -1
  7. package/lib/features/column-virtualization.js.map +1 -1
  8. package/lib/features/context-menu.js.map +1 -1
  9. package/lib/features/editing.js.map +1 -1
  10. package/lib/features/export.js.map +1 -1
  11. package/lib/features/filtering.js.map +1 -1
  12. package/lib/features/grouping-columns.js.map +1 -1
  13. package/lib/features/grouping-rows.js.map +1 -1
  14. package/lib/features/master-detail.js.map +1 -1
  15. package/lib/features/multi-sort.js.map +1 -1
  16. package/lib/features/pinned-columns.js.map +1 -1
  17. package/lib/features/pinned-rows.js.map +1 -1
  18. package/lib/features/pivot.js.map +1 -1
  19. package/lib/features/print.js.map +1 -1
  20. package/lib/features/registry.js +1 -1
  21. package/lib/features/registry.js.map +1 -1
  22. package/lib/features/reorder-columns.js.map +1 -1
  23. package/lib/features/responsive.js.map +1 -1
  24. package/lib/features/row-drag-drop.js.map +1 -1
  25. package/lib/features/selection.js.map +1 -1
  26. package/lib/features/server-side.js.map +1 -1
  27. package/lib/features/shell.js.map +1 -1
  28. package/lib/features/sticky-rows.js.map +1 -1
  29. package/lib/features/tooltip.js.map +1 -1
  30. package/lib/features/tree.js.map +1 -1
  31. package/lib/features/undo-redo.js.map +1 -1
  32. package/lib/features/visibility.js.map +1 -1
  33. package/lib/plugins/clipboard/index.js.map +1 -1
  34. package/lib/plugins/column-virtualization/index.js.map +1 -1
  35. package/lib/plugins/context-menu/index.js.map +1 -1
  36. package/lib/plugins/editing/editors.d.ts +15 -0
  37. package/lib/plugins/editing/index.js +1 -1
  38. package/lib/plugins/editing/index.js.map +1 -1
  39. package/lib/plugins/editing/types.d.ts +5 -1
  40. package/lib/plugins/export/index.js +1 -1
  41. package/lib/plugins/export/index.js.map +1 -1
  42. package/lib/plugins/filtering/filter-model.d.ts +20 -1
  43. package/lib/plugins/filtering/filter-panel-default.d.ts +15 -1
  44. package/lib/plugins/filtering/index.d.ts +1 -1
  45. package/lib/plugins/filtering/index.js +1 -1
  46. package/lib/plugins/filtering/index.js.map +1 -1
  47. package/lib/plugins/filtering/types.d.ts +58 -3
  48. package/lib/plugins/grouping-columns/index.js.map +1 -1
  49. package/lib/plugins/grouping-rows/index.js +1 -1
  50. package/lib/plugins/grouping-rows/index.js.map +1 -1
  51. package/lib/plugins/hierarchy/index.js.map +1 -1
  52. package/lib/plugins/master-detail/index.js +1 -1
  53. package/lib/plugins/master-detail/index.js.map +1 -1
  54. package/lib/plugins/multi-sort/index.js.map +1 -1
  55. package/lib/plugins/pinned-columns/index.js.map +1 -1
  56. package/lib/plugins/pinned-rows/index.js.map +1 -1
  57. package/lib/plugins/pivot/index.js +1 -1
  58. package/lib/plugins/pivot/index.js.map +1 -1
  59. package/lib/plugins/pivot/pivot-plugin.d.ts +2 -0
  60. package/lib/plugins/pivot/types.d.ts +2 -0
  61. package/lib/plugins/print/index.js.map +1 -1
  62. package/lib/plugins/reorder-columns/index.js.map +1 -1
  63. package/lib/plugins/responsive/index.js.map +1 -1
  64. package/lib/plugins/row-drag-drop/index.js.map +1 -1
  65. package/lib/plugins/selection/index.js +1 -1
  66. package/lib/plugins/selection/index.js.map +1 -1
  67. package/lib/plugins/server-side/index.js.map +1 -1
  68. package/lib/plugins/shared/form-field-target.d.ts +10 -0
  69. package/lib/plugins/shared/format-value.d.ts +12 -0
  70. package/lib/plugins/shell/index.js.map +1 -1
  71. package/lib/plugins/sticky-rows/index.js.map +1 -1
  72. package/lib/plugins/tooltip/index.js.map +1 -1
  73. package/lib/plugins/tree/index.js +1 -1
  74. package/lib/plugins/tree/index.js.map +1 -1
  75. package/lib/plugins/undo-redo/index.js.map +1 -1
  76. package/lib/plugins/visibility/index.js.map +1 -1
  77. package/package.json +1 -1
  78. package/umd/grid.all.umd.js +1 -1
  79. package/umd/grid.all.umd.js.map +1 -1
  80. package/umd/grid.umd.js +1 -1
  81. package/umd/grid.umd.js.map +1 -1
  82. package/umd/plugins/clipboard.umd.js.map +1 -1
  83. package/umd/plugins/column-virtualization.umd.js.map +1 -1
  84. package/umd/plugins/context-menu.umd.js.map +1 -1
  85. package/umd/plugins/editing.umd.js +1 -1
  86. package/umd/plugins/editing.umd.js.map +1 -1
  87. package/umd/plugins/export.umd.js +1 -1
  88. package/umd/plugins/export.umd.js.map +1 -1
  89. package/umd/plugins/filtering.umd.js +1 -1
  90. package/umd/plugins/filtering.umd.js.map +1 -1
  91. package/umd/plugins/grouping-columns.umd.js.map +1 -1
  92. package/umd/plugins/grouping-rows.umd.js +1 -1
  93. package/umd/plugins/grouping-rows.umd.js.map +1 -1
  94. package/umd/plugins/hierarchy.umd.js.map +1 -1
  95. package/umd/plugins/master-detail.umd.js +1 -1
  96. package/umd/plugins/master-detail.umd.js.map +1 -1
  97. package/umd/plugins/multi-sort.umd.js.map +1 -1
  98. package/umd/plugins/pinned-columns.umd.js.map +1 -1
  99. package/umd/plugins/pinned-rows.umd.js.map +1 -1
  100. package/umd/plugins/pivot.umd.js +1 -1
  101. package/umd/plugins/pivot.umd.js.map +1 -1
  102. package/umd/plugins/print.umd.js.map +1 -1
  103. package/umd/plugins/reorder-columns.umd.js.map +1 -1
  104. package/umd/plugins/responsive.umd.js.map +1 -1
  105. package/umd/plugins/row-drag-drop.umd.js.map +1 -1
  106. package/umd/plugins/selection.umd.js +1 -1
  107. package/umd/plugins/selection.umd.js.map +1 -1
  108. package/umd/plugins/server-side.umd.js.map +1 -1
  109. package/umd/plugins/shell.umd.js.map +1 -1
  110. package/umd/plugins/sticky-rows.umd.js.map +1 -1
  111. package/umd/plugins/tooltip.umd.js.map +1 -1
  112. package/umd/plugins/tree.umd.js +1 -1
  113. package/umd/plugins/tree.umd.js.map +1 -1
  114. package/umd/plugins/undo-redo.umd.js.map +1 -1
  115. package/umd/plugins/visibility.umd.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../../libs/grid/src/lib/core/internal/utils.ts","../../../../../../libs/grid/src/lib/core/internal/keyboard.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/internal/loading.ts","../../../../../../libs/grid/src/lib/plugins/editing/editing.css?inline","../../../../../../libs/grid/src/lib/plugins/editing/editors.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/cell-validation.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/dirty-tracking.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/dirty-tracking-manager.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/helpers.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/editor-injection.ts","../../../../../../libs/grid/src/lib/plugins/editing/editing-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","// #region Environment Helpers\n\n/**\n * Check if we're running in a development environment.\n * Returns true for localhost or when NODE_ENV !== 'production'.\n * Used to show warnings only in development.\n */\nexport function isDevelopment(): boolean {\n // Check for localhost (browser environment)\n if (typeof window !== 'undefined' && window.location) {\n const hostname = window.location.hostname;\n if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {\n return true;\n }\n }\n // Check for NODE_ENV (build-time or SSR)\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Cell Rendering Helpers\n\n/**\n * Generate accessible HTML for a boolean cell.\n * Uses role=\"checkbox\" with proper aria attributes.\n */\nexport function booleanCellHTML(value: boolean): string {\n return `<span role=\"checkbox\" aria-checked=\"${value}\" aria-label=\"${value}\">${value ? '&#x1F5F9;' : '&#9744;'}</span>`;\n}\n\n/**\n * Format a date value for display.\n * Handles Date objects, timestamps, and date strings.\n * Returns empty string for invalid dates.\n */\nexport function formatDateValue(value: unknown): string {\n if (value == null || value === '') return '';\n if (value instanceof Date) {\n return isNaN(value.getTime()) ? '' : value.toLocaleDateString();\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return isNaN(d.getTime()) ? '' : d.toLocaleDateString();\n }\n return '';\n}\n\n/**\n * Get the row index from a cell element's data-row attribute.\n * Falls back to calculating from parent row's DOM position if data-row is missing.\n * Returns -1 if no valid row index is found.\n */\nexport function getRowIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-row');\n if (attr) return parseInt(attr, 10);\n\n // Fallback: find the parent .data-grid-row and calculate index from siblings\n const rowEl = cell.closest('.data-grid-row');\n if (!rowEl) return -1;\n\n const parent = rowEl.parentElement;\n if (!parent) return -1;\n\n // Get all data-grid-row siblings and find this row's index\n const rows = parent.querySelectorAll(':scope > .data-grid-row');\n for (let i = 0; i < rows.length; i++) {\n if (rows[i] === rowEl) return i;\n }\n return -1;\n}\n\n/**\n * Get the column index from a cell element's data-col attribute.\n * Returns -1 if no valid column index is found.\n */\nexport function getColIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-col');\n return attr ? parseInt(attr, 10) : -1;\n}\n\n/**\n * Clear all cell-focus styling from a root element.\n * Used when changing focus or when selection plugin takes over focus management.\n */\nexport function clearCellFocus(root: Element | null): void {\n if (!root) return;\n root.querySelectorAll('.cell-focus').forEach((el) => el.classList.remove('cell-focus'));\n}\n// #endregion\n\n// #region RTL Helpers\n\n/** Text direction */\nexport type TextDirection = 'ltr' | 'rtl';\n\n/**\n * Get the text direction for an element.\n * Reads from the computed style, which respects the `dir` attribute on the element\n * or any ancestor, as well as CSS `direction` property.\n *\n * @param element - The element to check direction for\n * @returns 'ltr' or 'rtl'\n *\n * @example\n * ```typescript\n * // Detect grid's direction\n * const dir = getDirection(gridElement);\n * if (dir === 'rtl') {\n * // Handle RTL layout\n * }\n * ```\n */\nexport function getDirection(element: Element): TextDirection {\n // Try computed style first (works in real browsers)\n try {\n const computedDir = getComputedStyle(element).direction;\n if (computedDir === 'rtl') return 'rtl';\n } catch {\n // getComputedStyle may fail in some test environments\n }\n\n // Fallback: check dir attribute on element or ancestors\n // This handles test environments where getComputedStyle may not reflect dir attribute\n try {\n const dirAttr = element.closest?.('[dir]')?.getAttribute('dir');\n if (dirAttr === 'rtl') return 'rtl';\n } catch {\n // closest may not be available on mock elements\n }\n\n return 'ltr';\n}\n\n/**\n * Check if an element is in RTL mode.\n *\n * @param element - The element to check\n * @returns true if the element's text direction is right-to-left\n */\nexport function isRTL(element: Element): boolean {\n return getDirection(element) === 'rtl';\n}\n\n/**\n * Resolve a logical inline position to a physical position based on text direction.\n *\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n * - `'left'` / `'right'` → unchanged (physical values)\n *\n * @param position - Logical or physical position\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical position ('left' or 'right')\n *\n * @example\n * ```typescript\n * resolveInlinePosition('start', 'ltr'); // 'left'\n * resolveInlinePosition('start', 'rtl'); // 'right'\n * resolveInlinePosition('left', 'rtl'); // 'left' (unchanged)\n * ```\n */\nexport function resolveInlinePosition(\n position: 'left' | 'right' | 'start' | 'end',\n direction: TextDirection,\n): 'left' | 'right' {\n if (position === 'left' || position === 'right') {\n return position;\n }\n if (direction === 'rtl') {\n return position === 'start' ? 'right' : 'left';\n }\n return position === 'start' ? 'left' : 'right';\n}\n// #endregion\n","/**\n * Central keyboard handler attached to the host element. Manages navigation, paging,\n * and edit lifecycle triggers while respecting active form field interactions.\n */\nimport { FOCUSABLE_EDITOR_SELECTOR, GridClasses } from '../constants';\nimport type { GridHost } from '../types';\nimport { clearCellFocus, isRTL } from './utils';\nimport { readCellField } from './value-accessor';\nimport { fromVirtualScrollTop, getRowIndexAtOffset, toVirtualScrollTop } from './virtualization';\n\n/** Commit active row edit if the editing plugin provides this method. */\nfunction tryCommitEdit(grid: GridHost): void {\n if (typeof grid.commitActiveRowEdit === 'function') grid.commitActiveRowEdit();\n}\n\nfunction isFormField(el: HTMLElement | null): boolean {\n if (!el) return false;\n const tag = el.tagName;\n if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return true;\n if (el.isContentEditable) return true;\n return false;\n}\n\n/** True while a row-scoped edit session is active. */\nfunction isRowEditing(grid: GridHost): boolean {\n return grid._activeEditRows !== undefined && grid._activeEditRows !== -1;\n}\n\n// #region Keyboard Handler\n/**\n * Guard chain deciding whether this keydown belongs to something other than\n * cell navigation and must be left alone.\n */\nfunction shouldIgnoreKeyDown(grid: GridHost, e: KeyboardEvent, target: HTMLElement | null): boolean {\n // Keyboard events whose target lives inside the grid host but OUTSIDE the\n // rows body (toolpanel inputs/buttons, shell-header controls, light-DOM\n // children of <tbw-grid>) own their own keyboard interactions. Treating\n // ArrowUp/ArrowDown/Enter on such a target as cell navigation steals the\n // user's actual interaction target on the very next render cycle.\n // Cells and the grid host itself remain valid navigation targets.\n if (target && target !== grid && !target.closest?.('.rows-body')) return true;\n\n if (isFormField(target)) {\n switch (e.key) {\n // Home/End and horizontal arrows navigate within the text input; Enter\n // and Escape are handled by the input's own handlers first.\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n case 'Enter':\n case 'Escape':\n return true;\n case 'ArrowUp':\n case 'ArrowDown':\n // Number spinners own the vertical arrows; other fields fall through.\n if ((target as HTMLInputElement).tagName === 'INPUT' && (target as HTMLInputElement).type === 'number') {\n return true;\n }\n break;\n }\n }\n\n const colType = grid._visibleColumns[grid._focusCol]?.type;\n return isRowEditing(grid) && colType === 'select' && (e.key === 'ArrowDown' || e.key === 'ArrowUp');\n}\n\n/** Tab / Shift+Tab: advance one cell, wrapping to the next/previous row. */\nfunction navigateTab(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number): void {\n e.preventDefault();\n if (!e.shiftKey) {\n if (grid._focusCol < maxCol) {\n grid._focusCol += 1;\n return;\n }\n tryCommitEdit(grid);\n if (grid._focusRow < maxRow) {\n grid._focusRow += 1;\n grid._focusCol = 0;\n }\n return;\n }\n if (grid._focusCol > 0) {\n grid._focusCol -= 1;\n } else if (grid._focusRow > 0) {\n if (grid._activeEditRows === grid._focusRow) tryCommitEdit(grid);\n grid._focusRow -= 1;\n grid._focusCol = maxCol;\n }\n}\n\n/**\n * ArrowLeft/ArrowRight. `towardEnd` is the LOGICAL direction (ArrowRight); in\n * RTL the physical arrow maps to the opposite column index.\n */\nfunction navigateHorizontal(grid: GridHost, e: KeyboardEvent, maxCol: number, towardEnd: boolean): void {\n const forward = isRTL(grid) ? !towardEnd : towardEnd;\n grid._focusCol = forward ? Math.min(maxCol, grid._focusCol + 1) : Math.max(0, grid._focusCol - 1);\n e.preventDefault();\n}\n\n/** Commit any active row edit, then focus the first/last row (column unchanged). */\nfunction jumpToRowEdge(grid: GridHost, maxRow: number, toEnd: boolean): void {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = toEnd ? maxRow : 0;\n}\n\n/** ArrowUp/ArrowDown: one row, or the first/last row with Ctrl/Cmd. */\nfunction navigateVertical(grid: GridHost, e: KeyboardEvent, maxRow: number, down: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, down);\n else {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = down ? Math.min(maxRow, grid._focusRow + 1) : Math.max(0, grid._focusRow - 1);\n }\n e.preventDefault();\n}\n\n/**\n * PageUp/PageDown target: one viewport of rows (less any overlay band), so an\n * unconstrained grid pages straight to its edge. 20 rows until layout exists.\n */\nfunction pageTargetRow(grid: GridHost, maxRow: number, down: boolean): number {\n const from = grid._focusRow;\n const virt = grid._virtualization;\n const viewportH = virt?.viewportEl?.clientHeight ?? virt?.container?.clientHeight ?? 0;\n const offsets = grid._getVerticalScrollOffsets?.(from);\n const usable = viewportH - Math.max(0, offsets?.top ?? 0) - Math.max(0, offsets?.bottom ?? 0);\n const pc = virt?.variableHeights ? virt.positionCache : null;\n let target: number;\n if (usable > 0 && pc?.[from]) {\n target = getRowIndexAtOffset(pc, pc[from].offset + (down ? usable : -usable));\n // A row taller than the viewport would otherwise pin focus in place.\n if (target === from) target = from + (down ? 1 : -1);\n } else {\n const rowH = virt?.rowHeight ?? 0;\n const step = usable > 0 && rowH > 0 ? Math.max(1, Math.floor(usable / rowH)) : 20;\n target = from + (down ? step : -step);\n }\n return Math.max(0, Math.min(maxRow, target));\n}\n\n/** Home/End (plus Ctrl/Cmd variants which also jump to the first/last row). */\nfunction navigateRowEdge(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number, toEnd: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, toEnd);\n grid._focusCol = toEnd ? maxCol : 0;\n e.preventDefault();\n ensureCellVisible(grid, toEnd ? { forceScrollRight: true } : { forceScrollLeft: true });\n}\n\n/** Dispatch the unified `cell-activate` event. Returns true when cancelled. */\nfunction emitCellActivate(grid: GridHost, e: KeyboardEvent): boolean {\n const rowIndex = grid._focusRow;\n const colIndex = grid._focusCol;\n const column = grid._visibleColumns[colIndex];\n const row = grid._rows[rowIndex];\n const field = column?.field ?? '';\n const activateEvent = new CustomEvent('cell-activate', {\n cancelable: true,\n detail: {\n rowIndex,\n colIndex,\n column,\n field,\n value: field && row ? readCellField(row, field) : undefined,\n row,\n cellEl: grid.querySelector(`[data-row=\"${rowIndex}\"][data-col=\"${colIndex}\"]`) as HTMLElement | undefined,\n trigger: 'keyboard' as const,\n originalEvent: e,\n },\n });\n grid.dispatchEvent(activateEvent);\n return activateEvent.defaultPrevented;\n}\n\nexport function handleGridKeyDown(grid: GridHost, e: KeyboardEvent): void {\n // Dispatch to plugin system first - if any plugin handles it, stop here\n if (grid._dispatchKeyDown?.(e)) return;\n\n const path = e.composedPath?.() ?? [];\n const target = (path.length ? path[0] : e.target) as HTMLElement | null;\n if (shouldIgnoreKeyDown(grid, e, target)) return;\n\n const maxRow = grid._rows.length - 1;\n const maxCol = grid._visibleColumns.length - 1;\n\n switch (e.key) {\n case 'Tab':\n navigateTab(grid, e, maxRow, maxCol);\n break;\n case 'ArrowDown':\n navigateVertical(grid, e, maxRow, true);\n break;\n case 'ArrowUp':\n navigateVertical(grid, e, maxRow, false);\n break;\n case 'ArrowRight':\n navigateHorizontal(grid, e, maxCol, true);\n break;\n case 'ArrowLeft':\n navigateHorizontal(grid, e, maxCol, false);\n break;\n case 'Home':\n navigateRowEdge(grid, e, maxRow, maxCol, false);\n return;\n case 'End':\n navigateRowEdge(grid, e, maxRow, maxCol, true);\n return;\n case 'PageDown':\n grid._focusRow = pageTargetRow(grid, maxRow, true);\n e.preventDefault();\n break;\n case 'PageUp':\n grid._focusRow = pageTargetRow(grid, maxRow, false);\n e.preventDefault();\n break;\n // NOTE: Enter is normally handled by EditingPlugin. If no plugin handled\n // it, dispatch the unified cell-activate event for custom handling.\n case 'Enter':\n if (emitCellActivate(grid, e)) {\n e.preventDefault();\n return;\n }\n break;\n default:\n return;\n }\n ensureCellVisible(grid);\n}\n// #endregion\n\n// #region Cell Visibility\n/**\n * Options for ensureCellVisible to control scroll behavior.\n */\ninterface EnsureCellVisibleOptions {\n /** Force scroll to the leftmost position (for Home key) */\n forceScrollLeft?: boolean;\n /** Force scroll to the rightmost position (for End key) */\n forceScrollRight?: boolean;\n /** Force horizontal scroll even in edit mode (for Tab navigation) */\n forceHorizontalScroll?: boolean;\n}\n\n/**\n * Vertical: scroll the virtualized container so the focused row is in view.\n *\n * `y` is in raw row-content space (rowHeight × index). Above the browser height\n * cap (#326), `scrollEl.scrollTop` is in clamped spacer space — the two\n * coordinate systems are not interchangeable, so translate scrollTop into\n * virtual space for the comparison and back into native space when writing.\n * For sub-cap datasets the mapping is identity, so this is a no-op.\n *\n * Plugins that paint over the viewport (sticky rows) report the space they\n * obscure via `_getVerticalScrollOffsets`, so the row lands clear of the\n * overlay instead of underneath it — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n */\nfunction scrollFocusedRowIntoView(grid: GridHost): void {\n if (!grid._virtualization?.enabled) return;\n const { rowHeight, container, viewportEl, scrollMapping } = grid._virtualization;\n // container is the faux scrollbar element that handles actual scrolling;\n // viewportEl is the visible area element that has the correct height.\n const scrollEl = container as HTMLElement | undefined;\n const visibleHeight = viewportEl?.clientHeight ?? scrollEl?.clientHeight ?? 0;\n if (!scrollEl || visibleHeight <= 0) return;\n\n const offsets = grid._getVerticalScrollOffsets?.(grid._focusRow) ?? { top: 0, bottom: 0 };\n if (offsets.skipScroll) return;\n // A malformed or oversized report must never invert the visible band.\n const top = Math.max(0, offsets.top);\n const bottom = Math.max(0, offsets.bottom);\n const usableHeight = visibleHeight - top - bottom;\n if (usableHeight <= 0) return;\n\n const y = grid._focusRow * rowHeight;\n const virtualScrollTop = scrollMapping ? toVirtualScrollTop(scrollEl.scrollTop, scrollMapping) : scrollEl.scrollTop;\n if (y < virtualScrollTop + top) {\n const target = Math.max(0, y - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n } else if (y + rowHeight > virtualScrollTop + top + usableHeight) {\n const target = Math.max(0, y - usableHeight + rowHeight - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n }\n}\n\n/**\n * Resolve the rendered cell for the focused row/col, or `undefined` when the\n * focused row is outside the current virtual window.\n */\nfunction findFocusedCell(grid: GridHost): { rowEl: HTMLElement | null; cell: HTMLElement } | undefined {\n const rowIndex = grid._focusRow;\n const vStart = grid._virtualization.start ?? 0;\n const vEnd = grid._virtualization.end ?? grid._rows.length;\n if (rowIndex < vStart || rowIndex >= vEnd) return undefined;\n\n const rowEl = grid._bodyEl.querySelectorAll('.data-grid-row')[rowIndex - vStart] as HTMLElement | null;\n // Try exact column match first, then query by data-col, then fall back to the\n // first cell (full-width group rows).\n let cell = rowEl?.children[grid._focusCol] as HTMLElement | undefined;\n if (!cell || !cell.classList?.contains('cell')) {\n cell = (rowEl?.querySelector(`.cell[data-col=\"${grid._focusCol}\"]`) ?? rowEl?.querySelector('.cell[data-col]')) as\n HTMLElement | undefined;\n }\n return cell ? { rowEl: rowEl ?? null, cell } : undefined;\n}\n\n/** Horizontal: bring the focused cell into the `.tbw-scroll-area` viewport. */\nfunction scrollCellIntoViewHorizontally(\n grid: GridHost,\n rowEl: HTMLElement | null,\n cell: HTMLElement,\n options?: EnsureCellVisibleOptions,\n): void {\n const scrollArea = grid.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (!scrollArea) return;\n\n // Home/End always scroll to the edge.\n if (options?.forceScrollLeft) {\n scrollArea.scrollLeft = 0;\n return;\n }\n if (options?.forceScrollRight) {\n scrollArea.scrollLeft = scrollArea.scrollWidth - scrollArea.clientWidth;\n return;\n }\n\n // Plugins (e.g. pinned columns) report how much of the scroll area they\n // obscure, and whether the focused cell should skip scrolling entirely\n // (pinned cells are always visible).\n const offsets = grid._getHorizontalScrollOffsets?.(rowEl ?? undefined, cell) ?? { left: 0, right: 0 };\n if (offsets.skipScroll) return;\n\n const cellRect = cell.getBoundingClientRect();\n const scrollAreaRect = scrollArea.getBoundingClientRect();\n const cellLeft = cellRect.left - scrollAreaRect.left + scrollArea.scrollLeft;\n const cellRight = cellLeft + cellRect.width;\n const visibleLeft = scrollArea.scrollLeft + offsets.left;\n const visibleRight = scrollArea.scrollLeft + scrollArea.clientWidth - offsets.right;\n if (cellLeft < visibleLeft) {\n scrollArea.scrollLeft = cellLeft - offsets.left;\n } else if (cellRight > visibleRight) {\n scrollArea.scrollLeft = cellRight - scrollArea.clientWidth + offsets.right;\n }\n}\n\n/** Move real DOM focus to the editor, the cell, or the grid host. */\nfunction applyFocusTarget(grid: GridHost, cell: HTMLElement, isEditing: boolean): void {\n if (isEditing && cell.classList.contains(GridClasses.EDITING)) {\n // Editing cell: focus the editor input inside it\n const focusTarget = cell.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (focusTarget && document.activeElement !== focusTarget) {\n try {\n focusTarget.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n }\n return;\n }\n\n if (isEditing) {\n // Active edit row but this cell isn't the editing cell — focus it so Tab\n // navigation within the row can attach editors.\n if (cell.contains(document.activeElement)) return;\n if (!cell.hasAttribute('tabindex')) cell.setAttribute('tabindex', '-1');\n try {\n cell.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n return;\n }\n\n // NOT editing: keep focus on the grid element (tabindex=0) rather than\n // individual cells. In a virtualized grid, cells can be detached by\n // subsequent render cycles (e.g., SelectionPlugin's requestAfterRender\n // → RAF → row recycling). A detached focused cell causes activeElement\n // to revert to <body>, breaking keyboard navigation.\n // Visual focus is managed by the .cell-focus CSS class + data-has-focus.\n //\n // BUT: don't steal focus from a real focused descendant (toolpanel\n // input, shell-header button, registered overlay control). Cell focus\n // is virtual; if the user is actively typing in a non-cell input, we\n // must leave that input alone. We only reclaim focus when nothing\n // meaningful inside the grid currently holds it (active element is\n // outside grid, on the grid host itself, or on a bare cell).\n const active = document.activeElement;\n const meaningful =\n active instanceof HTMLElement &&\n active !== grid &&\n typeof grid.contains === 'function' &&\n grid.contains(active) &&\n !active.classList.contains('cell') &&\n !active.closest('.cell');\n if (!meaningful && active !== grid) {\n grid.focus({ preventScroll: true });\n }\n}\n\n/**\n * Scroll the viewport (virtualized or static) so the focused cell's row is visible\n * and apply visual focus styling / tabindex management.\n */\nexport function ensureCellVisible(grid: GridHost, options?: EnsureCellVisibleOptions): void {\n scrollFocusedRowIntoView(grid);\n\n const isEditing = isRowEditing(grid) || !!grid._isGridEditMode;\n // Skip refreshVirtualWindow when in edit mode to avoid wiping editors\n if (!isEditing) grid.refreshVirtualWindow(false);\n\n clearCellFocus(grid._bodyEl);\n // Clear previous aria-selected markers\n Array.from(grid._bodyEl.querySelectorAll('[aria-selected=\"true\"]')).forEach((el) => {\n el.setAttribute('aria-selected', 'false');\n });\n\n const focused = findFocusedCell(grid);\n if (!focused) return;\n const { rowEl, cell } = focused;\n\n cell.classList.add('cell-focus');\n cell.setAttribute('aria-selected', 'true');\n\n // Skip horizontal scrolling in edit mode to prevent scroll jumps when editors\n // are created — unless forceHorizontalScroll is set (Tab while editing).\n if (!isEditing || options?.forceHorizontalScroll) {\n scrollCellIntoViewHorizontally(grid, rowEl, cell, options);\n }\n\n applyFocusTarget(grid, cell, isEditing);\n}\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Loading State Module\n *\n * Handles loading overlays, row loading, and cell loading states.\n * Provides DOM manipulation helpers for loading indicators.\n *\n * @module internal/loading\n */\n\nimport type { FrameworkAdapter, GridConfig, LoadingContext, RowElementInternal } from '../types';\nimport { setSanitizedHTML } from './sanitize';\n\n/**\n * Create the default spinner element.\n * @param size - 'large' for grid overlay, 'small' for row/cell\n */\nexport function createDefaultSpinner(size: 'large' | 'small'): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = `tbw-spinner tbw-spinner--${size}`;\n spinner.setAttribute('role', 'progressbar');\n spinner.setAttribute('aria-label', 'Loading');\n return spinner;\n}\n\n/**\n * Create loading content using custom renderer or default spinner.\n *\n * Strings returned by a custom renderer are passed through `setSanitizedHTML()`\n * — mirroring the cell, header and empty-state render paths — so a message\n * interpolating server data (e.g. `Loading ${dataset.name}…`) is not an XSS\n * sink.\n *\n * @param size - 'large' for grid overlay, 'small' for row/cell\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingContent(size: 'large' | 'small', renderer?: GridConfig['loadingRenderer']): HTMLElement {\n if (renderer) {\n const context: LoadingContext = { size };\n const result = renderer(context);\n if (typeof result === 'string') {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, result);\n return wrapper;\n }\n return result;\n }\n\n return createDefaultSpinner(size);\n}\n\n/**\n * Create or update the loading overlay element.\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingOverlay(renderer?: GridConfig['loadingRenderer']): HTMLElement {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-loading-overlay';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n overlay.appendChild(createLoadingContent('large', renderer));\n return overlay;\n}\n\n/**\n * Show the loading overlay on the grid root element.\n * @param gridRoot - The .tbw-grid-root element\n * @param overlayEl - The overlay element (will be cached by caller)\n */\nexport function showLoadingOverlay(gridRoot: Element, overlayEl: HTMLElement): void {\n gridRoot.appendChild(overlayEl);\n}\n\n/**\n * Hide the loading overlay.\n * @param overlayEl - The overlay element to remove\n */\nexport function hideLoadingOverlay(overlayEl: HTMLElement | undefined): void {\n overlayEl?.remove();\n}\n\n/** Remove a cell's owned loading overlay and release adapter-managed content. */\nexport function removeCellLoadingOverlay(cellEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!cellEl.firstElementChild) return;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Move a cell's owned loading overlay in front of the cell while its content is rewritten,\n * so the still-active loader survives adapter release and content replacement. The caller\n * appends it back. The overlay stays connected because React prunes detached portal containers.\n */\nexport function parkCellLoadingOverlay(cellEl: HTMLElement): HTMLElement | null {\n if (!cellEl.firstElementChild) return null;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return null;\n const parent = cellEl.parentElement;\n if (parent) parent.insertBefore(overlay, cellEl);\n else overlay.remove();\n return overlay;\n}\n\n/** Remove a row's owned loading overlay and release adapter-managed content. */\nexport function removeRowLoadingOverlay(rowEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!rowEl.firstElementChild) return;\n const overlay = rowEl.querySelector<HTMLElement>(':scope > .tbw-row-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Update a row element's loading state.\n * Uses real DOM elements instead of ::before/::after pseudo-elements\n * because the selection plugin already uses ::after on rows for border styling.\n * @param rowEl - The row element\n * @param loading - Whether the row is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * in place of the default spinner. Only invoked when the overlay is created.\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setRowLoadingState(\n rowEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n rowEl.classList.add('tbw-row-loading');\n rowEl.setAttribute('aria-busy', 'true');\n\n // Create overlay + spinner DOM elements if not already present.\n // Direct children only — a nested grid's row overlay must not be mistaken for this row's.\n if (!rowEl.querySelector(':scope > .tbw-row-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-row-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n\n if (renderer) {\n overlay.appendChild(createLoadingContent('small', renderer));\n } else {\n const spinner = document.createElement('div');\n spinner.className = 'tbw-row-loading-spinner';\n overlay.appendChild(spinner);\n }\n\n rowEl.appendChild(overlay);\n }\n } else {\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n // Remove overlay + spinner DOM elements\n removeRowLoadingOverlay(rowEl, adapter);\n }\n}\n\n/**\n * Update a cell element's loading state.\n * @param cellEl - The cell element\n * @param loading - Whether the cell is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setCellLoadingState(\n cellEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n cellEl.classList.add('tbw-cell-loading');\n cellEl.setAttribute('aria-busy', 'true');\n\n // Breadcrumb so the render hot path can skip the per-cell scan on the\n // overwhelming majority of rows. Conservative: only ever set here, so it can\n // read stale-true after the last loading cell clears (costs one extra scan).\n const rowEl = cellEl.parentElement as RowElementInternal | null;\n if (rowEl) rowEl.__hasCellLoading = true;\n\n if (renderer) {\n cellEl.classList.add('tbw-cell-loading-custom');\n if (!cellEl.querySelector(':scope > .tbw-cell-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-cell-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n overlay.appendChild(createLoadingContent('small', renderer));\n cellEl.appendChild(overlay);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading-custom');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading');\n cellEl.classList.remove('tbw-cell-loading-custom');\n cellEl.removeAttribute('aria-busy');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n}\n","/**\n * Editing Plugin Styles\n *\n * These styles are injected into the document when the EditingPlugin is loaded.\n * CSS custom properties use fallbacks to core grid variables for consistency.\n * Uses CSS nesting for scoping to tbw-grid elements.\n */\n\n@layer tbw-plugins {\n /* ============================================================================\n CSS Custom Properties for Editing\n ============================================================================\n These are defined here so they can be overridden by consumers.\n They reference core grid variables with sensible defaults.\n ============================================================================ */\n tbw-grid {\n /* Editing cell/row backgrounds */\n --tbw-editing-bg: var(--tbw-color-selection);\n --tbw-editing-row-bg: var(--tbw-editing-bg);\n\n /* Editing input styling */\n --tbw-editing-border: var(--tbw-border-input, 1px solid var(--tbw-color-border-strong));\n --tbw-padding-editing-input: var(--tbw-cell-padding-input, 2px 6px);\n --tbw-font-size-editor: inherit;\n\n /* Row outline when editing */\n --tbw-editing-row-outline-color: var(--tbw-color-accent);\n --tbw-editing-row-outline-width: 1px;\n\n /* ============================================================================\n Row Editing State\n ============================================================================ */\n\n /* Highlight entire row when any cell is being edited (only in row mode, not grid mode) */\n &:not(.tbw-grid-mode) .data-grid-row:has(.editing) {\n background: var(--tbw-editing-row-bg);\n outline: var(--tbw-editing-row-outline-width) solid var(--tbw-editing-row-outline-color);\n outline-offset: calc(-1 * var(--tbw-editing-row-outline-width));\n }\n\n /* ============================================================================\n Cell Editing State\n ============================================================================ */\n\n /* Cell in edit mode */\n .data-grid-row > .cell.editing {\n overflow: hidden;\n padding: 0;\n /* Override block to flex for editing inputs */\n display: flex;\n /* Match the non-editing cell height exactly. Non-editing height is:\n max(--tbw-row-height [border-box], line-height + padding + border [content-driven])\n Since editing cells have padding:0, min-height must compensate for both cases:\n - Content-driven: line-height (1lh) + original vertical padding + border\n - Min-height-driven: just var(--tbw-row-height) (border-box already includes padding+border) */\n min-height: max(\n var(--tbw-row-height),\n calc(1lh + 2 * var(--tbw-cell-padding-v, 0.25em) + var(--tbw-border-width, 1px))\n );\n align-items: center;\n justify-content: center;\n }\n\n /* Standard form inputs inside editing cells */\n .data-grid-row > .cell.editing input:not([type='checkbox']),\n .data-grid-row > .cell.editing select,\n .data-grid-row > .cell.editing textarea {\n width: 100%;\n height: 100%;\n flex: 1 1 auto;\n align-self: stretch;\n min-width: 0;\n border: var(--tbw-editing-border);\n padding: var(--tbw-padding-editing-input);\n font-size: var(--tbw-font-size-editor);\n }\n\n /* ============================================================================\n Editor Host\n ============================================================================ */\n\n /* Editor host wrapper uses display:contents to let editor fill cell */\n .tbw-editor-host {\n display: contents;\n }\n\n /* ============================================================================\n Changed Row Indicator\n ============================================================================ */\n\n /* Visual indicator for rows that have been modified */\n .data-grid-row.changed {\n /* Could add a left border or background tint here */\n }\n\n /* ============================================================================\n Invalid Cell Indicator\n ============================================================================ */\n\n /* Default invalid cell color - can be overridden by consumers */\n --tbw-invalid-bg: light-dark(#fef2f2, #450a0a);\n --tbw-invalid-border-color: light-dark(#ef4444, #f87171);\n\n /* Visual indicator for cells marked as invalid */\n .data-grid-row > .cell[data-invalid='true'] {\n background: var(--tbw-invalid-bg);\n outline: 1px solid var(--tbw-invalid-border-color);\n outline-offset: -1px;\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Default Editors Module\n *\n * Provides built-in editor factories for different column types.\n * Each editor type has its own factory function for consistency and readability.\n *\n * IMPORTANT: Editor factories should NOT call focus() on elements - they are called\n * before the element is appended to the DOM. The calling code (beginBulkEdit,\n * inlineEnterEdit) is responsible for focusing the correct editor after insertion.\n */\n\nimport type { ColumnConfig, ColumnEditorContext } from '../../core/types';\nimport type { DateEditorParams, NumberEditorParams, SelectEditorParams, TextEditorParams } from './types';\n\n// ============================================================================\n// Type Aliases\n// ============================================================================\n\n/** Option shape used by select editor (matches column.options) */\ntype ColumnOption = { label: string; value: unknown };\n\n/** Column with any row type (used for editor factories) */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyColumn = ColumnConfig<any>;\n\n// ============================================================================\n// Utilities\n// ============================================================================\n\n/** Resolve column.options (handles both array and function forms) */\nfunction resolveOptions(column: AnyColumn): ColumnOption[] {\n const raw = column.options;\n if (!raw) return [];\n return typeof raw === 'function' ? raw() : raw;\n}\n\n/** Format a Date as YYYY-MM-DD string */\nfunction toISODate(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\n// ============================================================================\n// Editor Factories\n// ============================================================================\n\n/** Creates a number input editor */\nfunction createNumberEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as NumberEditorParams | undefined;\n const input = document.createElement('input');\n input.type = 'number';\n input.value = ctx.value != null ? String(ctx.value) : '';\n\n if (params?.min !== undefined) input.min = String(params.min);\n if (params?.max !== undefined) input.max = String(params.max);\n if (params?.step !== undefined) input.step = String(params.step);\n if (params?.placeholder) input.placeholder = params.placeholder;\n\n const commit = () => {\n if (input.value === '') {\n if (column.nullable) {\n ctx.commit(null);\n } else {\n // Non-nullable: fall back to min value or 0\n ctx.commit(params?.min ?? 0);\n }\n } else {\n ctx.commit(Number(input.value));\n }\n };\n input.addEventListener('blur', commit);\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') commit();\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return input;\n };\n}\n\n/** Creates a checkbox editor for boolean values */\nfunction createBooleanEditor(): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const input = document.createElement('input');\n input.type = 'checkbox';\n input.checked = !!ctx.value;\n input.addEventListener('change', () => ctx.commit(input.checked));\n return input;\n };\n}\n\n/** Creates a date input editor */\nfunction createDateEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as DateEditorParams | undefined;\n const input = document.createElement('input');\n input.type = 'date';\n\n // Set initial value - handle both Date objects and string dates\n if (ctx.value instanceof Date) {\n input.valueAsDate = ctx.value;\n } else if (typeof ctx.value === 'string' && ctx.value) {\n // String date like \"2019-10-09\" - set directly as value\n input.value = ctx.value.split('T')[0]; // Handle ISO strings too\n }\n if (params?.min) input.min = params.min;\n if (params?.max) input.max = params.max;\n if (params?.placeholder) input.placeholder = params.placeholder;\n\n // Commit function preserves original type (string vs Date)\n const commit = () => {\n if (!input.value) {\n if (column.nullable) {\n ctx.commit(null);\n } else {\n // Non-nullable: use configured default or today\n const fallback = params?.default;\n if (typeof ctx.value === 'string' || typeof fallback === 'string') {\n // String mode: use default string or today as YYYY-MM-DD\n ctx.commit(typeof fallback === 'string' ? fallback : toISODate(fallback ?? new Date()));\n } else {\n // Date object mode\n ctx.commit(fallback instanceof Date ? fallback : new Date());\n }\n }\n return;\n }\n if (typeof ctx.value === 'string') {\n // Original was string, return string in YYYY-MM-DD format\n ctx.commit(input.value);\n } else {\n ctx.commit(input.valueAsDate);\n }\n };\n\n input.addEventListener('change', commit);\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return input;\n };\n}\n\n/** Sentinel value used internally to identify the nullable \"(Blank)\" option */\nconst NULLABLE_BLANK_VALUE = '__tbw_null__';\n\n/** Creates a select dropdown editor */\nfunction createSelectEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as SelectEditorParams | undefined;\n const select = document.createElement('select');\n if (column.multi) select.multiple = true;\n\n // Add blank option: nullable columns always get one, otherwise respect includeEmpty\n const showBlank = column.nullable || params?.includeEmpty;\n if (showBlank) {\n const emptyOpt = document.createElement('option');\n emptyOpt.value = column.nullable ? NULLABLE_BLANK_VALUE : '';\n emptyOpt.textContent = column.nullable ? (params?.emptyLabel ?? '(Blank)') : (params?.emptyLabel ?? '');\n if (ctx.value == null) emptyOpt.selected = true;\n select.appendChild(emptyOpt);\n }\n\n // Populate options from column.options\n const options = resolveOptions(column);\n options.forEach((opt) => {\n const o = document.createElement('option');\n o.value = String(opt.value);\n o.textContent = opt.label;\n if (column.multi && Array.isArray(ctx.value) && ctx.value.includes(opt.value)) {\n o.selected = true;\n } else if (!column.multi && ctx.value === opt.value) {\n o.selected = true;\n }\n select.appendChild(o);\n });\n\n const commit = () => {\n if (column.multi) {\n const values = Array.from(select.selectedOptions).map((o) => o.value);\n ctx.commit(values);\n } else if (column.nullable && select.value === NULLABLE_BLANK_VALUE) {\n ctx.commit(null);\n } else {\n ctx.commit(select.value);\n }\n };\n\n select.addEventListener('change', commit);\n select.addEventListener('blur', commit);\n select.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return select;\n };\n}\n\n/** Creates a text input editor (default) */\nfunction createTextEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as TextEditorParams | undefined;\n const input = document.createElement('input');\n input.type = 'text';\n input.value = ctx.value != null ? String(ctx.value) : '';\n\n if (params?.maxLength !== undefined) input.maxLength = params.maxLength;\n if (params?.pattern) input.pattern = params.pattern;\n if (params?.placeholder) input.placeholder = params.placeholder;\n\n // Commit function preserves original type when possible\n const commit = () => {\n const inputVal = input.value;\n\n // Empty input handling\n if (inputVal === '') {\n if (column.nullable) {\n ctx.commit(null);\n } else {\n // Non-nullable: commit empty string so the field is never null\n ctx.commit('');\n }\n return;\n }\n\n // Preserve values with characters that <input> can't represent (newlines, etc.).\n // If stripping those characters produces the same string, the user didn't change anything.\n if (typeof ctx.value === 'string' && inputVal === ctx.value.replace(/[\\n\\r]/g, '')) {\n return;\n }\n // Preserve numeric type for custom column types (e.g., currency)\n if (typeof ctx.value === 'number') {\n ctx.commit(Number(inputVal));\n } else {\n ctx.commit(inputVal);\n }\n };\n\n input.addEventListener('blur', commit);\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') commit();\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return input;\n };\n}\n\n// ============================================================================\n// Main Entry Point\n// ============================================================================\n\n/**\n * Returns a default editor factory function for the given column type.\n * Each editor handles commit on blur/Enter, and cancel on Escape.\n *\n * Note: Focus is NOT called here - the calling code handles focusing after DOM insertion.\n * @since 0.4.0\n */\nexport function defaultEditorFor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement | string {\n switch (column.type) {\n case 'number':\n return createNumberEditor(column);\n case 'boolean':\n return createBooleanEditor();\n case 'date':\n return createDateEditor(column);\n case 'select':\n return createSelectEditor(column);\n default:\n return createTextEditor(column);\n }\n}\n\n// ============================================================================\n// Utility Export (used by EditingPlugin)\n// ============================================================================\n\n/**\n * Resolve a numeric editor value. Empty input falls back to `null` for nullable\n * columns, otherwise to the configured `min` (or `0`).\n */\nfunction readNumericValue(raw: string, column?: AnyColumn): unknown {\n if (raw === '') {\n if (column?.nullable) return null;\n const params = column?.editorParams as NumberEditorParams | undefined;\n return params?.min ?? 0;\n }\n return Number(raw);\n}\n\n/**\n * Resolve a `<input type=\"date\">` value, preserving the original value's type\n * (string dates stay `YYYY-MM-DD` strings, Date objects stay `Date`).\n */\nfunction readDateValue(input: HTMLInputElement, column?: AnyColumn, originalValue?: unknown): unknown {\n if (!input.value) {\n if (column?.nullable) return null;\n // Non-nullable: preserve original or fall back to today\n if (typeof originalValue === 'string') return originalValue || new Date().toISOString().slice(0, 10);\n return (originalValue as Date) ?? new Date();\n }\n // Preserve original type: if original was a string, return string (YYYY-MM-DD format)\n if (typeof originalValue === 'string') {\n return input.value; // input.value is already in YYYY-MM-DD format\n }\n return input.valueAsDate;\n}\n\n/**\n * Resolve a plain text `<input>` value, preserving numeric originals and values\n * containing characters `<input>` cannot represent.\n */\nfunction readTextInputValue(input: HTMLInputElement, column?: AnyColumn, originalValue?: unknown): unknown {\n // For text inputs, check if original value was a number to preserve type\n if (typeof originalValue === 'number') return readNumericValue(input.value, column);\n // Nullable text: empty → null; non-nullable: empty → ''\n if (input.value === '' && (originalValue === null || originalValue === undefined)) {\n return column?.nullable ? null : '';\n }\n // Preserve values with characters <input> can't represent (newlines, etc.)\n if (typeof originalValue === 'string' && input.value === originalValue.replace(/[\\n\\r]/g, '')) {\n return originalValue;\n }\n return input.value;\n}\n\n/**\n * Resolve a `<textarea>` / `<select>` value using the column type or the\n * original value's type.\n */\nfunction readTextControlValue(\n input: HTMLTextAreaElement | HTMLSelectElement,\n column?: AnyColumn,\n originalValue?: unknown,\n): unknown {\n // Check column type OR original value type — preserves numeric type for\n // custom column types (e.g., currency).\n if (input.value !== '' && (column?.type === 'number' || typeof originalValue === 'number')) {\n return Number(input.value);\n }\n // Nullable: empty → null; non-nullable: empty → ''\n if ((originalValue === null || originalValue === undefined) && input.value === '') {\n return column?.nullable ? null : '';\n }\n return input.value;\n}\n\n/**\n * Get the typed value from an input element based on its type, column config, and original value.\n * Preserves the type of the original value (e.g., numeric currency values stay as numbers,\n * string dates stay as strings, null/undefined for empty fields).\n */\nexport function getInputValue(\n input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,\n column?: AnyColumn,\n originalValue?: unknown,\n): unknown {\n if (!(input instanceof HTMLInputElement)) return readTextControlValue(input, column, originalValue);\n switch (input.type) {\n case 'checkbox':\n return input.checked;\n case 'number':\n return readNumericValue(input.value, column);\n case 'date':\n return readDateValue(input, column, originalValue);\n default:\n return readTextInputValue(input, column, originalValue);\n }\n}\n","/**\n * Cell Validation Manager\n *\n * Manages invalid-cell state for the Editing Plugin.\n * Extracted from EditingPlugin to reduce the main plugin file size.\n *\n * The manager owns the `Map<rowId, Map<field, message>>` state and exposes\n * read/write methods. DOM synchronization is handled via an injected callback.\n *\n * @internal\n */\n\n/** Callback to sync the data-invalid attribute on a cell element in the DOM. */\nexport type SyncInvalidAttributeFn = (rowId: string, field: string, invalid: boolean) => void;\n\n/**\n * Manages validation state for grid cells.\n *\n * Tracks which cells are marked invalid and their validation messages.\n * The DOM side-effect (adding/removing `data-invalid` attribute) is delegated\n * to the `syncAttribute` callback provided at construction time.\n */\nexport class CellValidationManager {\n /** Invalid cell tracking: Map<rowId, Map<field, message>> */\n readonly #cells = new Map<string, Map<string, string>>();\n\n /** Callback to sync DOM attributes when validation state changes */\n readonly #syncAttribute: SyncInvalidAttributeFn;\n\n constructor(syncAttribute: SyncInvalidAttributeFn) {\n this.#syncAttribute = syncAttribute;\n }\n\n // #region Write Operations\n\n /**\n * Mark a cell as invalid with an optional validation message.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @param message - Optional validation message (for tooltips or display)\n */\n setInvalid(rowId: string, field: string, message = ''): void {\n let rowInvalids = this.#cells.get(rowId);\n if (!rowInvalids) {\n rowInvalids = new Map();\n this.#cells.set(rowId, rowInvalids);\n }\n rowInvalids.set(field, message);\n this.#syncAttribute(rowId, field, true);\n }\n\n /**\n * Clear the invalid state for a specific cell.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n */\n clearInvalid(rowId: string, field: string): void {\n const rowInvalids = this.#cells.get(rowId);\n if (rowInvalids) {\n rowInvalids.delete(field);\n if (rowInvalids.size === 0) {\n this.#cells.delete(rowId);\n }\n }\n this.#syncAttribute(rowId, field, false);\n }\n\n /**\n * Clear all invalid cells for a specific row.\n *\n * @param rowId - The row ID (from getRowId)\n */\n clearRowInvalid(rowId: string): void {\n const rowInvalids = this.#cells.get(rowId);\n if (rowInvalids) {\n const fields = Array.from(rowInvalids.keys());\n this.#cells.delete(rowId);\n fields.forEach((field) => this.#syncAttribute(rowId, field, false));\n }\n }\n\n /**\n * Clear all invalid cell states across all rows.\n */\n clearAllInvalid(): void {\n const entries = Array.from(this.#cells.entries());\n this.#cells.clear();\n entries.forEach(([rowId, fields]) => {\n fields.forEach((_, field) => this.#syncAttribute(rowId, field, false));\n });\n }\n\n // #endregion\n\n // #region Read Operations\n\n /**\n * Check if a specific cell is marked as invalid.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @returns True if the cell is marked as invalid\n */\n isCellInvalid(rowId: string, field: string): boolean {\n return this.#cells.get(rowId)?.has(field) ?? false;\n }\n\n /**\n * Get the validation message for an invalid cell.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @returns The validation message, or undefined if cell is valid\n */\n getInvalidMessage(rowId: string, field: string): string | undefined {\n return this.#cells.get(rowId)?.get(field);\n }\n\n /**\n * Check if a row has any invalid cells.\n *\n * @param rowId - The row ID (from getRowId)\n * @returns True if the row has at least one invalid cell\n */\n hasInvalidCells(rowId: string): boolean {\n const rowInvalids = this.#cells.get(rowId);\n return rowInvalids ? rowInvalids.size > 0 : false;\n }\n\n /**\n * Get all invalid fields for a row.\n *\n * @param rowId - The row ID (from getRowId)\n * @returns Map of field names to validation messages\n */\n getInvalidFields(rowId: string): Map<string, string> {\n return new Map(this.#cells.get(rowId) ?? []);\n }\n\n // #endregion\n}\n","/**\n * Pure functions for dirty tracking baseline management.\n *\n * Extracted from EditingPlugin to keep the plugin under the 2,000 line target.\n * All functions are stateless — the caller owns the baseline `Map`.\n *\n * @internal\n */\n\nimport { readCellField } from '../../../core/internal/value-accessor';\n\n// #region Types\n\n/**\n * Detail for `dirty-change` custom events.\n * @since 1.23.0\n */\nexport interface DirtyChangeDetail<T = unknown> {\n /** Row ID (from getRowId) */\n rowId: string;\n /** Current row data */\n row: T;\n /** Baseline (original) row data, or undefined for newly inserted rows */\n original: T | undefined;\n /**\n * Transition type:\n * - `'modified'` — row differs from baseline\n * - `'new'` — row was inserted via `insertRow()` and has no baseline\n * - `'reverted'` — row was reverted to baseline via `revertRow()`\n * - `'pristine'` — row was explicitly marked pristine via `markAsPristine()`\n */\n type: 'modified' | 'new' | 'reverted' | 'pristine';\n}\n\n/**\n * Result of getDirtyRows(): each entry has the row ID, original (baseline),\n * and current data.\n * @since 1.23.0\n */\nexport interface DirtyRowEntry<T = unknown> {\n id: string;\n /** The original (baseline) row data, or `undefined` for newly inserted rows. */\n original: T | undefined;\n current: T;\n}\n\n/**\n * Detail for `baselines-captured` custom events.\n *\n * Emitted after the render pipeline completes when new baseline snapshots\n * were captured during `processRows`.\n * @since 1.23.0\n */\nexport interface BaselinesCapturedDetail {\n /** Total number of tracked baselines (not just newly captured). */\n count: number;\n}\n\n// #endregion\n\n// #region Baseline Capture\n\n/**\n * Capture baselines for rows not already tracked (first-write-wins).\n *\n * Uses `structuredClone` for deep copy so nested objects cannot be mutated\n * through shared references.\n *\n * @param baselines - Map of rowId → deep-cloned baseline row data\n * @param rows - Rows to snapshot\n * @param getRowId - Function to resolve row ID (may throw; exceptions are swallowed)\n */\nexport function captureBaselines<T>(\n baselines: Map<string, T>,\n rows: readonly T[],\n getRowId: (row: T) => string | undefined,\n): void {\n for (const row of rows) {\n // Loading placeholders (ServerSide blocks in flight) have a stable internal id but\n // hold no data — snapshotting them would retain one clone per unloaded slot.\n if ((row as { __loading?: unknown } | null)?.__loading === true) continue;\n try {\n const id = getRowId(row);\n if (id != null && !baselines.has(id)) {\n baselines.set(id, structuredClone(row));\n }\n } catch {\n // Row has no resolvable ID — skip\n }\n }\n}\n\n// #endregion\n\n// #region Dirty Detection\n\n/**\n * Check whether a row's current data differs from its baseline.\n *\n * Uses deep property comparison so that `structuredClone`'d baselines\n * with nested objects/arrays/Dates compare correctly.\n */\nexport function isRowDirty<T>(baselines: Map<string, T>, rowId: string, currentRow: T): boolean {\n const baseline = baselines.get(rowId);\n if (!baseline) return false; // No baseline → row is \"new\" or untracked\n return !deepEqual(baseline, currentRow);\n}\n\n/**\n * Check whether a single cell (field) differs from its baseline value.\n *\n * Returns `false` when no baseline exists for the row.\n */\nexport function isCellDirty<T>(baselines: Map<string, T>, rowId: string, currentRow: T, field: string): boolean {\n const baseline = baselines.get(rowId);\n if (!baseline) return false;\n const baselineValue = readCellField(baseline, field);\n const currentValue = readCellField(currentRow, field);\n return !deepEqual(baselineValue, currentValue);\n}\n\n/**\n * Deep comparison of two values. Handles primitives, plain objects, arrays,\n * and Dates — the value types produced by `structuredClone` on row data.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return false;\n if (typeof a !== typeof b) return false;\n\n // Date comparison\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();\n\n // Array comparison\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n\n // Object comparison\n if (typeof a === 'object') {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const keysA = Object.keys(aObj);\n const keysB = Object.keys(bObj);\n if (keysA.length !== keysB.length) return false;\n for (const key of keysA) {\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n// #endregion\n\n// #region State Transitions\n\n/**\n * Mark a row as pristine: re-snapshot baseline from current data.\n *\n * After calling this, `isRowDirty` returns `false` for the row (until it\n * is edited again).\n */\nexport function markPristine<T>(baselines: Map<string, T>, rowId: string, currentRow: T): void {\n baselines.set(rowId, structuredClone(currentRow));\n}\n\n/**\n * Get the original (baseline) row data as a deep clone.\n *\n * Returns `undefined` if no baseline exists (e.g. newly inserted row).\n */\nexport function getOriginalRow<T>(baselines: Map<string, T>, rowId: string): T | undefined {\n const baseline = baselines.get(rowId);\n return baseline ? structuredClone(baseline) : undefined;\n}\n\n/**\n * Revert a row to its baseline values by mutating the current row in-place.\n *\n * Returns `true` if a baseline existed and the row was reverted, `false` otherwise.\n */\nexport function revertToBaseline<T>(baselines: Map<string, T>, rowId: string, currentRow: T): boolean {\n const baseline = baselines.get(rowId);\n if (!baseline) return false;\n const baselineObj = baseline as Record<string, unknown>;\n const currentObj = currentRow as Record<string, unknown>;\n for (const key of Object.keys(baselineObj)) {\n currentObj[key] = baselineObj[key];\n }\n return true;\n}\n\n// #endregion\n","/**\n * Stateful manager for dirty tracking in the EditingPlugin.\n *\n * Owns all dirty-tracking-related state (baselines, changed/new/committed sets)\n * and exposes high-level methods that coordinate pure functions from\n * `dirty-tracking.ts`.\n *\n * The EditingPlugin delegates all dirty tracking operations to this manager,\n * keeping the plugin class focused on orchestration (row resolution, event\n * emission, render scheduling).\n *\n * @internal\n */\n\nimport { writeCellField } from '../../../core/internal/value-accessor';\nimport {\n captureBaselines,\n getOriginalRow,\n isCellDirty,\n isRowDirty,\n markPristine,\n revertToBaseline,\n type DirtyRowEntry,\n} from './dirty-tracking';\n\n// #region Types\n\n/**\n * Callback that resolves a row object by its ID.\n * Returns `undefined` when the row is not found.\n */\nexport type RowResolver<T> = (rowId: string) => T | undefined;\n\n// #endregion\n\n// #region DirtyTrackingManager\n\nexport class DirtyTrackingManager<T> {\n // --- Owned state ---\n\n /** Baseline snapshots: rowId → deep-cloned original row data (first-write-wins) */\n readonly baselines = new Map<string, T>();\n\n /** Whether new baselines were captured during the current processRows cycle */\n private baselinesWereCaptured = false;\n\n /** Row IDs inserted via `insertRow()` (no baseline available) */\n readonly newRowIds = new Set<string>();\n\n /** Row IDs that have been modified (edit committed) */\n readonly changedRowIds = new Set<string>();\n\n /** Row IDs whose edit session was committed (gates `tbw-row-dirty` CSS class) */\n readonly committedDirtyRowIds = new Set<string>();\n\n // --- Lifecycle ---\n\n /** Reset all dirty tracking state (called from detach / resetChangedRows). */\n clear(): void {\n this.changedRowIds.clear();\n this.committedDirtyRowIds.clear();\n this.baselines.clear();\n this.newRowIds.clear();\n this.baselinesWereCaptured = false;\n }\n\n // --- Baseline capture (called from processRows) ---\n\n /**\n * Capture baselines for rows not already tracked (first-write-wins).\n * Sets the `baselinesWereCaptured` flag when new rows are snapshotted.\n * @returns whether any new baseline was captured\n */\n capture(rows: readonly T[], getRowId: (r: T) => string | undefined): boolean {\n const sizeBefore = this.baselines.size;\n captureBaselines(this.baselines, rows, getRowId);\n if (this.baselines.size > sizeBefore) {\n this.baselinesWereCaptured = true;\n return true;\n }\n return false;\n }\n\n /**\n * Drain the baselines-captured flag (called from afterRender).\n * Returns `null` when no new captures occurred, or the baseline count.\n */\n drainCapturedFlag(): number | null {\n if (!this.baselinesWereCaptured) return null;\n this.baselinesWereCaptured = false;\n return this.baselines.size;\n }\n\n // --- Row dirty queries ---\n\n /** Check if a specific row differs from its baseline. */\n isRowDirty(rowId: string, row: T): boolean {\n return isRowDirty(this.baselines, rowId, row);\n }\n\n /** Check if ANY row is dirty (requires row resolver for baseline iteration). */\n hasAnyDirty(resolveRow: RowResolver<T>): boolean {\n if (this.newRowIds.size > 0) return true;\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row && isRowDirty(this.baselines, rowId, row)) return true;\n }\n return false;\n }\n\n // --- Cell dirty queries ---\n\n /** Check if a single cell differs from its baseline value. */\n isCellDirty(rowId: string, row: T, field: string): boolean {\n return isCellDirty(this.baselines, rowId, row, field);\n }\n\n // --- Render-time helpers ---\n\n /**\n * Get the dirty-state tuple needed by afterRowRender to toggle CSS classes.\n *\n * `isCommittedDirty` requires BOTH committed status AND actual data divergence\n * from baseline (handles undo: after CTRL+Z, row is no longer visually dirty).\n */\n getRowDirtyState(rowId: string, row: T): { isNew: boolean; isCommittedDirty: boolean; hasBaseline: boolean } {\n const isNew = this.newRowIds.has(rowId);\n const isCommittedDirty = !isNew && this.committedDirtyRowIds.has(rowId) && isRowDirty(this.baselines, rowId, row);\n return { isNew, isCommittedDirty, hasBaseline: this.baselines.has(rowId) };\n }\n\n // --- Mark operations ---\n\n /** Re-snapshot baseline from current data and remove from all sets. */\n markPristine(rowId: string, row: T): void {\n markPristine(this.baselines, rowId, row);\n this.newRowIds.delete(rowId);\n this.changedRowIds.delete(rowId);\n this.committedDirtyRowIds.delete(rowId);\n }\n\n /**\n * Update a single field's baseline to an externally-synced value.\n *\n * Used for `source: 'sync'` (declarative host data replacement): the incoming\n * value becomes the new pristine truth for that cell without touching other\n * fields' baselines or the row's dirty membership sets. No-op when the row has\n * no captured baseline (an untracked row is not dirty anyway).\n */\n rebaselineCell(rowId: string, field: string, value: unknown): void {\n const baseline = this.baselines.get(rowId);\n if (baseline) writeCellField(baseline, field, value);\n }\n\n /** Mark a row as newly inserted (no baseline). */\n markNew(rowId: string): void {\n this.newRowIds.add(rowId);\n this.committedDirtyRowIds.add(rowId);\n }\n\n /** Mark a row as dirty (external mutation). */\n markDirty(rowId: string): void {\n this.changedRowIds.add(rowId);\n this.committedDirtyRowIds.add(rowId);\n }\n\n /** Re-snapshot all baselines and clear all tracking sets. */\n markAllPristine(resolveRow: RowResolver<T>): void {\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row) markPristine(this.baselines, rowId, row);\n }\n this.newRowIds.clear();\n this.changedRowIds.clear();\n this.committedDirtyRowIds.clear();\n }\n\n // --- Original row access ---\n\n /** Get deep-cloned baseline (undefined for new/untracked rows). */\n getOriginalRow(rowId: string): T | undefined {\n return getOriginalRow<T>(this.baselines, rowId);\n }\n\n /** Lightweight check whether a baseline exists (no cloning). */\n hasBaseline(rowId: string): boolean {\n return this.baselines.has(rowId);\n }\n\n // --- Aggregate queries ---\n\n /** Get all dirty rows with original + current data. Requires a row resolver. */\n getDirtyRows(resolveRow: RowResolver<T>): DirtyRowEntry<T>[] {\n const result: DirtyRowEntry<T>[] = [];\n for (const [rowId, baseline] of this.baselines) {\n const row = resolveRow(rowId);\n if (row && isRowDirty(this.baselines, rowId, row)) {\n result.push({ id: rowId, original: structuredClone(baseline), current: row });\n }\n }\n for (const newId of this.newRowIds) {\n const row = resolveRow(newId);\n if (row) {\n result.push({ id: newId, original: undefined, current: row });\n }\n }\n return result;\n }\n\n /** Get IDs of all dirty rows. Requires a row resolver. */\n getDirtyRowIds(resolveRow: RowResolver<T>): string[] {\n const ids: string[] = [];\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row && isRowDirty(this.baselines, rowId, row)) ids.push(rowId);\n }\n for (const newId of this.newRowIds) ids.push(newId);\n return ids;\n }\n\n // --- Revert operations ---\n\n /**\n * Revert a row to its baseline values (mutates row in-place).\n * Returns `true` when baseline existed and row was reverted.\n */\n revertRow(rowId: string, row: T): boolean {\n const reverted = revertToBaseline(this.baselines, rowId, row);\n if (reverted) {\n this.changedRowIds.delete(rowId);\n this.committedDirtyRowIds.delete(rowId);\n }\n return reverted;\n }\n\n /** Revert all rows to baseline values. Requires a row resolver. */\n revertAll(resolveRow: RowResolver<T>): void {\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row) revertToBaseline(this.baselines, rowId, row);\n }\n this.changedRowIds.clear();\n this.committedDirtyRowIds.clear();\n }\n\n // --- Changed row queries ---\n\n /** Resolve changed row IDs to row objects. */\n getChangedRows(resolveRow: RowResolver<T>): T[] {\n const rows: T[] = [];\n for (const id of this.changedRowIds) {\n const row = resolveRow(id);\n if (row) rows.push(row);\n }\n return rows;\n }\n\n /** Get a copy of changed row IDs. */\n getChangedRowIds(): string[] {\n return Array.from(this.changedRowIds);\n }\n\n /** Check if a row has been marked as changed. */\n isRowChanged(rowId: string): boolean {\n return this.changedRowIds.has(rowId);\n }\n}\n\n// #endregion\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","/**\n * Pure helper functions for the Editing Plugin.\n *\n * Extracted from EditingPlugin to reduce the main plugin file size.\n * All functions are stateless — they have no `this` access.\n *\n * @internal\n */\n\nimport type {\n ColumnConfig,\n ColumnEditorSpec,\n ColumnInternal,\n InternalGrid,\n RowElementInternal,\n} from '../../../core/types';\nimport { getInputValue } from '../editors';\nimport type { EditingConfig } from '../types';\n\n// #region Constants\n\n/**\n * CSS selector for focusable editor elements inside a cell.\n * Duplicated from core/internal/rows to avoid cross-boundary imports\n * that Vite externalises to `@toolbox-web/grid` during plugin bundling.\n *\n * @internal\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Returns the closest ancestor (or `target` itself) that matches\n * {@link FOCUSABLE_EDITOR_SELECTOR}, or `null` if none exists.\n *\n * Always prefer this over `target.matches(FOCUSABLE_EDITOR_SELECTOR)` —\n * non-focusable editor descendants (e.g. `<option>` inside an open\n * native `<select>` popup; spans inside `contenteditable`) need to be\n * recognised as \"inside the editor\" but do not themselves match the\n * selector.\n *\n * @internal\n */\nexport function getEditorAncestor(target: EventTarget | null | undefined): Element | null {\n const el = target as Element | null | undefined;\n return el?.closest?.(FOCUSABLE_EDITOR_SELECTOR) ?? null;\n}\n\n// #endregion\n\n// #region Editor Resolution\n\n/**\n * Resolves the editor for a column using the priority chain:\n * 1. Column-level (`column.editor`)\n * 2. Light DOM template (`__editorTemplate` → returns 'template')\n * 3. Grid-level (`gridConfig.typeDefaults[column.type]`)\n * 4. App-level (framework adapter's `getTypeDefault`)\n * 5. Returns undefined (caller uses built-in defaultEditorFor)\n */\nexport function resolveEditor<TRow>(\n grid: InternalGrid<TRow>,\n col: ColumnInternal<TRow>,\n): ColumnEditorSpec<TRow, unknown> | 'template' | undefined {\n // 1. Column-level editor (highest priority)\n if (col.editor) return col.editor;\n\n // 2. Light DOM template\n const tplHolder = col.__editorTemplate;\n if (tplHolder) return 'template';\n\n // No type specified - no type defaults to check\n if (!col.type) return undefined;\n\n // 3. Grid-level typeDefaults (access via effectiveConfig)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const gridTypeDefaults = (grid as any).effectiveConfig?.typeDefaults;\n if (gridTypeDefaults?.[col.type]?.editor) {\n return gridTypeDefaults[col.type].editor as ColumnEditorSpec<TRow, unknown>;\n }\n\n // 4. App-level registry (via framework adapter)\n const adapter = grid.__frameworkAdapter;\n if (adapter?.getTypeDefault) {\n const appDefault = adapter.getTypeDefault<TRow>(col.type, grid._hostElement);\n if (appDefault?.editor) {\n return appDefault.editor as ColumnEditorSpec<TRow, unknown>;\n }\n }\n\n // 5. No custom editor - caller uses built-in defaultEditorFor\n return undefined;\n}\n\n// #endregion\n\n// #region Property Key Safety\n\n/**\n * Returns true if the given property key is safe to use on a plain object.\n */\nexport function isSafePropertyKey(key: unknown): key is string {\n if (typeof key !== 'string') return false;\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') return false;\n return true;\n}\n\n// #endregion\n\n// #region Row Element State\n\n/**\n * Increment the editing cell count on a row element.\n */\nexport function incrementEditingCount(rowEl: RowElementInternal): void {\n const count = (rowEl.__editingCellCount ?? 0) + 1;\n rowEl.__editingCellCount = count;\n rowEl.setAttribute('data-has-editing', '');\n}\n\n/**\n * Clear all editing state from a row element.\n */\nexport function clearEditingState(rowEl: RowElementInternal): void {\n rowEl.__editingCellCount = 0;\n rowEl.removeAttribute('data-has-editing');\n}\n\n// #endregion\n\n// #region No-op Helpers\n\n/**\n * No-op updateRow function for rows without IDs.\n * Extracted to a named function to satisfy eslint no-empty-function.\n */\nexport function noopUpdateRow(_changes: unknown): void {\n // Row has no ID - cannot update\n}\n\n// #endregion\n\n// #region Editor Input Wiring\n\n/**\n * Auto-wire commit/cancel lifecycle for input elements in string-returned editors.\n */\nexport function wireEditorInputs(\n editorHost: HTMLElement,\n column: ColumnConfig<unknown>,\n commit: (value: unknown) => void,\n originalValue?: unknown,\n): void {\n const input = editorHost.querySelector('input,textarea,select') as\n | HTMLInputElement\n | HTMLTextAreaElement\n | HTMLSelectElement\n | null;\n if (!input) return;\n\n input.addEventListener('blur', () => {\n commit(getInputValue(input, column, originalValue));\n });\n\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.addEventListener('change', () => commit(input.checked));\n } else if (input instanceof HTMLSelectElement) {\n input.addEventListener('change', () => commit(getInputValue(input, column, originalValue)));\n }\n}\n\n// #endregion\n\n// #region Edit Guard\n\n/**\n * Returns `true` when the configured `onBeforeEditClose` callback vetoes\n * the close (i.e. returns `false`). Use as a one-liner guard:\n *\n * ```ts\n * if (shouldPreventEditClose(config, e)) return;\n * ```\n */\nexport function shouldPreventEditClose(config: EditingConfig, event: MouseEvent | KeyboardEvent): boolean {\n return config.onBeforeEditClose?.(event) === false;\n}\n\n// #endregion\n\n// #region ARIA Overlay Detection\n\n/**\n * Returns `true` when `target` is inside an overlay panel that is currently\n * \"owned\" by an open combobox/listbox/menu inside `scopeEl`.\n *\n * Detection uses the WAI-ARIA pattern: a control with\n * `aria-expanded=\"true\"` and `aria-controls=\"<id>\"` declares ownership of\n * the panel with that id. Used by the editing plugin as a generic fallback\n * so portal-rendered overlays from libraries like Downshift, Material UI,\n * or Headless UI are recognised as part of the active editor without the\n * consumer having to call `registerExternalFocusContainer` (#251).\n *\n * Cheap: only walks elements with `aria-expanded=\"true\"` inside the\n * editor scope (typically zero or one).\n *\n * @internal\n */\nexport function isInsideOpenAriaOverlay(target: Node | null | undefined, scopeEl: HTMLElement): boolean {\n if (!target) return false;\n const triggers = scopeEl.querySelectorAll<HTMLElement>('[aria-expanded=\"true\"][aria-controls]');\n for (let i = 0; i < triggers.length; i++) {\n const id = triggers[i].getAttribute('aria-controls');\n if (!id) continue;\n const panel = scopeEl.ownerDocument?.getElementById(id);\n if (panel && panel.contains(target as Node)) return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Row Comparison\n\n/**\n * Shallow-compare a snapshot against the current row to detect changes.\n * Returns `true` if any own-property value differs between the two objects.\n */\nexport function hasRowChanged<T>(snapshot: T | undefined, current: T): boolean {\n if (!snapshot) return false;\n const snapshotObj = snapshot as Record<string, unknown>;\n const currentObj = current as Record<string, unknown>;\n const allKeys = new Set([...Object.keys(snapshotObj), ...Object.keys(currentObj)]);\n for (const key of allKeys) {\n if (snapshotObj[key] !== currentObj[key]) return true;\n }\n return false;\n}\n\n// #endregion\n","/**\n * Editor injection logic for the Editing Plugin.\n *\n * Extracted from EditingPlugin to reduce the main file size.\n * Contains the DOM-heavy editor creation and template rendering,\n * while the plugin retains state management and event emission.\n *\n * @internal\n */\n\nimport { EDITOR_MOUNT_ERROR, warnDiagnostic } from '../../../core/internal/diagnostics';\nimport { parkCellLoadingOverlay } from '../../../core/internal/loading';\nimport { readCellField, writeCellField } from '../../../core/internal/value-accessor';\nimport type {\n ColumnConfig,\n ColumnEditorContext,\n ColumnInternal,\n GridHost,\n RowElementInternal,\n} from '../../../core/types';\nimport { defaultEditorFor, getInputValue } from '../editors';\nimport type { EditingConfig } from '../types';\nimport {\n FOCUSABLE_EDITOR_SELECTOR,\n getEditorAncestor,\n incrementEditingCount,\n isSafePropertyKey,\n noopUpdateRow,\n resolveEditor,\n shouldPreventEditClose,\n wireEditorInputs,\n} from './helpers';\n\n// #region Types\n\n/**\n * Dependencies injected by the EditingPlugin so the extraction\n * can call back into plugin-owned state and methods.\n */\nexport interface EditorInjectionDeps<T> {\n /** Internal grid reference (also serves as HTMLElement). */\n grid: GridHost<T>;\n /** Whether the grid is in always-editing \"grid\" mode. */\n isGridMode: boolean;\n /** Plugin configuration. */\n config: EditingConfig;\n /** Set of cells currently in edit mode (\"rowIndex:colIndex\"). */\n editingCells: Set<string>;\n /** Value-change callbacks keyed by \"rowIndex:field\". */\n editorValueCallbacks: Map<string, (newValue: unknown) => void>;\n /** Returns `true` when an edit session is active (#activeEditRow !== -1). */\n isEditSessionActive: () => boolean;\n /** Commit a single cell value change. */\n commitCellValue: (rowIndex: number, column: ColumnConfig<T>, newValue: unknown, rowData: T) => void;\n /** Exit editing for a row (commit or revert). */\n exitRowEdit: (rowIndex: number, revert: boolean) => void;\n}\n\n// #endregion\n\n// #region Editor Injection\n\n/**\n * Inject an editor into a cell element.\n *\n * Handles the full editor lifecycle: creates the editor host, resolves\n * the editor spec (template / custom-element / factory / component),\n * wires commit/cancel callbacks, and registers value-change listeners.\n */\nexport function injectEditor<T>(\n deps: EditorInjectionDeps<T>,\n rowData: T,\n rowIndex: number,\n column: ColumnConfig<T>,\n colIndex: number,\n cell: HTMLElement,\n skipFocus: boolean,\n parentRowEl?: HTMLElement,\n): void {\n if (!column.editable) return;\n if (cell.classList.contains('editing')) return;\n\n const { grid, isGridMode, config, editingCells, editorValueCallbacks } = deps;\n\n // Get row ID for updateRow helper (may not exist)\n let rowId: string | undefined;\n try {\n rowId = grid.getRowId?.(rowData);\n } catch {\n // Row has no ID\n }\n\n // Create updateRow helper for cascade updates (noop if row has no ID)\n const updateRow: (changes: Partial<T>) => void = rowId\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (changes) => (grid as any).updateRow(rowId!, changes as Record<string, unknown>, 'cascade')\n : noopUpdateRow;\n\n const originalValue = isSafePropertyKey(column.field) ? readCellField(rowData, column.field) : undefined;\n\n cell.classList.add('editing');\n editingCells.add(`${rowIndex}:${colIndex}`);\n\n // Use explicit parentRowEl when cell is in a DocumentFragment (not yet in DOM),\n // otherwise fall back to cell.parentElement for cells already attached to a row.\n const rowEl = (parentRowEl ?? cell.parentElement) as RowElementInternal | null;\n if (rowEl) incrementEditingCount(rowEl);\n\n let editFinalized = false;\n const commit = (newValue: unknown) => {\n // In grid mode, always allow commits (we're always editing)\n // In row mode, only allow commits if we're in an active edit session\n if (editFinalized || (!isGridMode && !deps.isEditSessionActive())) return;\n // Resolve row and index fresh at commit time.\n // With a row ID we use _getRowEntry for O(1) lookup — this is resilient\n // against _rows being replaced (e.g. Angular directive effect).\n // Without a row ID we fall back to the captured rowData reference.\n // Using _rows[rowIndex] without an ID is unsafe: the index may be stale\n // after _rows replacement, which would commit to the WRONG row.\n const entry = rowId ? grid._getRowEntry(rowId) : undefined;\n const currentRowData = (entry?.row ?? rowData) as T;\n const currentIndex = entry?.index ?? rowIndex;\n deps.commitCellValue(currentIndex, column, newValue, currentRowData);\n };\n const cancel = () => {\n editFinalized = true;\n if (isSafePropertyKey(column.field)) {\n // Same ID-first / captured-rowData fallback as commit — see comment above.\n const entry = rowId ? grid._getRowEntry(rowId) : undefined;\n const currentRowData = (entry?.row ?? rowData) as T;\n writeCellField(currentRowData, column.field, originalValue);\n }\n };\n\n const editorHost = document.createElement('div');\n editorHost.className = 'tbw-editor-host';\n const loadingOverlay = parkCellLoadingOverlay(cell);\n // Let the framework adapter (React/Vue/Angular) tear down any renderer\n // it mounted into this cell BEFORE we wipe the DOM. The `cell.innerHTML`\n // assignment below removes adapter-managed nodes synchronously without\n // notifying the framework — which leaves React's fiber tree pointing at\n // orphan DOM and throws `removeChild` on the next user-triggered commit\n // (issue #250). Calling `releaseCell` here unmounts cleanly while the\n // adapter's container is still attached to its remembered children.\n // Guard with `firstElementChild` so default-rendered cells (no\n // adapter-managed nodes) skip the no-op call — matches the wipe-guard\n // pattern in `core/internal/rows.ts`.\n if (cell.firstElementChild) grid.__frameworkAdapter?.releaseCell?.(cell);\n cell.innerHTML = '';\n cell.appendChild(editorHost);\n if (loadingOverlay) cell.appendChild(loadingOverlay);\n\n // Keydown handler for Enter/Escape\n editorHost.addEventListener('keydown', (e: KeyboardEvent) => {\n // Respect editor-level preventDefault: portal-based pickers (combobox,\n // autocomplete, calendar) call preventDefault on Enter to confirm an\n // option without exiting the row. Bubbling reaches editorHost with\n // defaultPrevented=true; honor that and skip the row-exit logic (#250).\n if (e.defaultPrevented) return;\n // ARIA-expanded fallback (#251): when the focused control declares\n // an open overlay via aria-expanded=\"true\" + aria-controls, defer\n // Enter to the overlay so combobox confirmation does not close the\n // row edit. Covers libraries like Downshift / Headless UI / MUI\n // Autocomplete that do not call preventDefault on confirm.\n // Only short-circuit Enter — Escape and other keys must still flow\n // through the normal handlers (Escape closes the overlay first via\n // its own listener, then dispatches up to cancel-edit if unhandled).\n if (e.key === 'Enter') {\n const ariaTarget = e.target as HTMLElement | null;\n if (\n ariaTarget &&\n ariaTarget.getAttribute?.('aria-expanded') === 'true' &&\n ariaTarget.hasAttribute?.('aria-controls')\n ) {\n return;\n }\n }\n if (e.key === 'Enter') {\n // Native <select> open-popup guard — applies in BOTH grid and row mode.\n // When Enter's target is a *descendant* of the editor (most notably an\n // <option> inside an open native <select> popup), do nothing: let the\n // browser commit the highlighted option and close the popup, which fires\n // `change` on the editor (committing via the change listener wired below).\n // Calling preventDefault / stopPropagation / exitRowEdit here would\n // destroy the open popup and discard the selection. A subsequent Enter\n // (popup closed → target IS the editor) then exits the row as usual.\n // Row mode previously lacked this guard, so keyboard selection in custom\n // <select> editors closed the row instead of picking the value (#427).\n const enterTarget = e.target as Element | null;\n const enterEditorAncestor = getEditorAncestor(enterTarget);\n if (enterEditorAncestor && enterEditorAncestor !== enterTarget) {\n return;\n }\n // In grid mode, Enter just commits without exiting\n if (isGridMode) {\n // Target IS the editor itself (SELECT with no popup open, or\n // INPUT/TEXTAREA): commit the current value. preventDefault\n // suppresses the browser opening the SELECT popup on Enter.\n // We deliberately do NOT stopPropagation — the event must\n // bubble so EditingPlugin can blur the editor and return\n // focus to the grid for cell navigation.\n e.preventDefault();\n const input = editorHost.querySelector('input,textarea,select') as\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null;\n if (input) {\n commit(getInputValue(input, column as ColumnConfig<unknown>, originalValue));\n }\n return;\n }\n if (shouldPreventEditClose(config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n editFinalized = true;\n deps.exitRowEdit(rowIndex, false);\n }\n if (e.key === 'Escape') {\n // In grid mode, Escape doesn't exit edit mode\n if (isGridMode) {\n e.stopPropagation();\n e.preventDefault();\n return;\n }\n if (shouldPreventEditClose(config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n cancel();\n deps.exitRowEdit(rowIndex, true);\n }\n });\n\n const colInternal = column as ColumnInternal<T>;\n const tplHolder = colInternal.__editorTemplate;\n // Resolve editor using priority chain: column → template → typeDefaults → adapter → built-in\n const editorSpec = resolveEditor(grid, colInternal) ?? defaultEditorFor(column);\n const value = originalValue;\n\n // Value-change callback registration.\n // Editors call onValueChange(cb) to receive pushes when the underlying row\n // is mutated externally (e.g., via updateRow from another cell's commit).\n // Multiple callbacks can be registered (user + auto-wire).\n const callbackKey = `${rowIndex}:${column.field}`;\n const callbacks: Array<(newValue: unknown) => void> = [];\n editorValueCallbacks.set(callbackKey, (newVal) => {\n for (const cb of callbacks) cb(newVal);\n });\n const onValueChange = (cb: (newValue: unknown) => void) => {\n callbacks.push(cb);\n };\n\n if (editorSpec === 'template' && tplHolder) {\n renderTemplateEditor(deps, editorHost, colInternal, rowData, originalValue, commit, cancel, skipFocus, rowIndex);\n // Auto-update built-in template editors when value changes externally.\n // Skip non-primitive values (arrays, objects) — framework adapters manage\n // those via the cell-cancel event. String() on arrays produces comma-separated junk.\n onValueChange((newVal) => {\n if (newVal != null && typeof newVal === 'object') return;\n const input = editorHost.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(\n 'input,textarea,select',\n );\n if (input) {\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.checked = !!newVal;\n } else {\n input.value = String(newVal ?? '');\n }\n }\n });\n } else if (typeof editorSpec === 'string') {\n const el = document.createElement(editorSpec) as HTMLElement & { value?: unknown };\n el.value = value;\n el.addEventListener('change', () => commit(el.value));\n // Auto-update custom element editors when value changes externally\n onValueChange((newVal) => {\n el.value = newVal;\n });\n editorHost.appendChild(el);\n if (!skipFocus) {\n queueMicrotask(() => {\n const focusable = editorHost.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n focusable?.focus({ preventScroll: true });\n });\n }\n } else if (typeof editorSpec === 'function') {\n const ctx: ColumnEditorContext<T> = {\n row: rowData,\n rowId: rowId ?? '',\n value,\n field: column.field,\n column,\n commit,\n cancel,\n updateRow,\n onValueChange,\n grid: deps.grid as ColumnEditorContext<T>['grid'],\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const produced = (editorSpec as any)(ctx);\n if (typeof produced === 'string') {\n // NOT sanitized on purpose: sanitizeHTML strips `input`/`select`/\n // `textarea`/`button`, which is exactly what an editor is made of. The\n // string comes from an author-supplied editor factory (code, not row\n // data) — row values reach the DOM through the compiled-template path\n // below, which escapes interpolated values.\n // eslint-disable-next-line no-restricted-syntax\n editorHost.innerHTML = produced;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n wireEditorInputs(editorHost, column as any, commit, originalValue);\n // Auto-update wired inputs when value changes externally\n onValueChange((newVal) => {\n const input = editorHost.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(\n 'input,textarea,select',\n );\n if (input) {\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.checked = !!newVal;\n } else {\n input.value = String(newVal ?? '');\n }\n }\n });\n } else if (produced instanceof Node) {\n editorHost.appendChild(produced);\n const isSimpleInput =\n produced instanceof HTMLInputElement ||\n produced instanceof HTMLSelectElement ||\n produced instanceof HTMLTextAreaElement;\n if (!isSimpleInput) {\n cell.setAttribute('data-editor-managed', '');\n } else {\n // Auto-update simple inputs returned by factory functions\n onValueChange((newVal) => {\n if (produced instanceof HTMLInputElement && produced.type === 'checkbox') {\n produced.checked = !!newVal;\n } else {\n (produced as HTMLInputElement).value = String(newVal ?? '');\n }\n });\n }\n } else if (!produced && editorHost.hasChildNodes()) {\n // Factory returned void but mounted content into the editor host\n // (e.g. Angular/React/Vue adapter component editor). Mark the cell\n // as externally managed so the native commit loop in #exitRowEdit\n // does not read raw input values from framework editor DOM.\n cell.setAttribute('data-editor-managed', '');\n }\n if (!skipFocus) {\n queueMicrotask(() => {\n const focusable = editorHost.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n focusable?.focus({ preventScroll: true });\n });\n }\n } else if (editorSpec && typeof editorSpec === 'object') {\n const placeholder = document.createElement('div');\n placeholder.setAttribute('data-external-editor', '');\n placeholder.setAttribute('data-field', column.field);\n editorHost.appendChild(placeholder);\n cell.setAttribute('data-editor-managed', '');\n const context: ColumnEditorContext<T> = {\n row: rowData,\n rowId: rowId ?? '',\n value,\n field: column.field,\n column,\n commit,\n cancel,\n updateRow,\n onValueChange,\n grid: deps.grid as ColumnEditorContext<T>['grid'],\n };\n if (editorSpec.mount) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n editorSpec.mount({ placeholder, context: context as any, spec: editorSpec });\n } catch (e) {\n warnDiagnostic(\n EDITOR_MOUNT_ERROR,\n `External editor mount error for column '${column.field}': ${e}`,\n deps.grid.id,\n );\n }\n } else {\n grid.dispatchEvent(\n new CustomEvent('mount-external-editor', { detail: { placeholder, spec: editorSpec, context } }),\n );\n }\n }\n\n nameEditorFromHeader(editorHost, column);\n}\n\n/**\n * Give the editor an accessible name taken from its column header.\n *\n * A screen reader announces the header when focus lands on a *gridcell*, but an\n * `<input>` inside that cell is a control in its own right and needs its own\n * name (SC 4.1.2 / SC 3.3.2) — otherwise it is announced as a bare \"edit text\".\n * Deferred a microtask so editors that mount asynchronously are covered, and\n * skipped entirely whenever the control already has a name of any kind.\n */\nfunction nameEditorFromHeader<T>(editorHost: HTMLElement, column: ColumnConfig<T>): void {\n const label = typeof column.header === 'string' ? column.header : column.field;\n if (!label) return;\n\n queueMicrotask(() => {\n const control = editorHost.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (!control) return;\n const named =\n control.hasAttribute('aria-label') ||\n control.hasAttribute('aria-labelledby') ||\n control.hasAttribute('title') ||\n (control.id && editorHost.querySelector(`label[for=\"${CSS.escape(control.id)}\"]`)) ||\n control.closest('label');\n if (!named) control.setAttribute('aria-label', label);\n });\n}\n\n// #endregion\n\n// #region Template Editor\n\n/**\n * Render a template-based editor inside an editor host element.\n */\nfunction renderTemplateEditor<T>(\n deps: Pick<EditorInjectionDeps<T>, 'config' | 'exitRowEdit'>,\n editorHost: HTMLElement,\n column: ColumnInternal<T>,\n rowData: T,\n originalValue: unknown,\n commit: (value: unknown) => void,\n cancel: () => void,\n skipFocus: boolean,\n rowIndex: number,\n): void {\n const tplHolder = column.__editorTemplate;\n if (!tplHolder) return;\n\n const clone = tplHolder.cloneNode(true) as HTMLElement;\n const compiledEditor = column.__compiledEditor;\n\n if (compiledEditor) {\n // NOT sanitized: editor markup is author light-DOM (`<template editor>`) and\n // contains form controls the sanitizer would strip. Row data interpolated\n // via `{{ }}` is HTML-escaped by the compiled template itself.\n // eslint-disable-next-line no-restricted-syntax\n clone.innerHTML = compiledEditor({\n row: rowData,\n value: originalValue,\n field: column.field,\n column,\n commit,\n cancel,\n });\n } else {\n clone.querySelectorAll<HTMLElement>('*').forEach((node) => {\n if (node.childNodes.length === 1 && node.firstChild?.nodeType === Node.TEXT_NODE) {\n node.textContent =\n node.textContent\n ?.replace(/{{\\s*value\\s*}}/g, originalValue == null ? '' : String(originalValue))\n .replace(/{{\\s*row\\.([a-zA-Z0-9_]+)\\s*}}/g, (_m, g: string) => {\n if (!isSafePropertyKey(g)) return '';\n const v = (rowData as Record<string, unknown>)[g];\n return v == null ? '' : String(v);\n }) || '';\n }\n });\n }\n\n const input = clone.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(\n 'input,textarea,select',\n );\n if (input) {\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.checked = !!originalValue;\n } else {\n input.value = String(originalValue ?? '');\n }\n\n let editFinalized = false;\n input.addEventListener('blur', () => {\n if (editFinalized) return;\n commit(getInputValue(input, column, originalValue));\n });\n input.addEventListener('keydown', (evt) => {\n const e = evt as KeyboardEvent;\n if (e.key === 'Enter') {\n if (shouldPreventEditClose(deps.config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n editFinalized = true;\n commit(getInputValue(input, column, originalValue));\n deps.exitRowEdit(rowIndex, false);\n }\n if (e.key === 'Escape') {\n if (shouldPreventEditClose(deps.config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n cancel();\n deps.exitRowEdit(rowIndex, true);\n }\n });\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.addEventListener('change', () => commit(input.checked));\n }\n if (!skipFocus) {\n setTimeout(() => input.focus({ preventScroll: true }), 0);\n }\n }\n editorHost.appendChild(clone);\n}\n\n// #endregion\n","/**\n * Editing Plugin\n *\n * Provides complete editing functionality for tbw-grid.\n * This plugin is FULLY SELF-CONTAINED - the grid has ZERO editing knowledge.\n *\n * The plugin:\n * - Owns all editing state (active cell, snapshots, changed rows)\n * - Uses event distribution (onCellClick, onKeyDown) to handle edit lifecycle\n * - Uses afterRender() hook to inject editors into cells\n * - Uses processColumns() to augment columns with editing metadata\n * - Emits its own events (cell-commit, row-commit, changed-rows-reset)\n *\n * Without this plugin, the grid cannot edit. With this plugin, editing\n * is fully functional without any core changes.\n */\n\nimport { GridDataAttrs } from '../../core/constants';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { ensureCellVisible } from '../../core/internal/keyboard';\nimport { parkCellLoadingOverlay } from '../../core/internal/loading';\nimport { compileTemplate } from '../../core/internal/sanitize';\nimport { invalidateAccessorCache, readCellField, writeCellField } from '../../core/internal/value-accessor';\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n PluginManifest,\n PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin, type CellClickEvent, type GridElement } from '../../core/plugin/base-plugin';\nimport type { CellEditablePredicate, CommitCellValueContext } from '../../core/plugin/types';\nimport type {\n ColumnConfig,\n ColumnInternal,\n GridHost,\n InternalGrid,\n RowElementInternal,\n TypeDefault,\n UpdateSource,\n} from '../../core/types';\nimport styles from './editing.css?inline';\nimport { getInputValue } from './editors';\nimport { CellValidationManager } from './internal/cell-validation';\nimport { type BaselinesCapturedDetail, type DirtyChangeDetail, type DirtyRowEntry } from './internal/dirty-tracking';\nimport { DirtyTrackingManager } from './internal/dirty-tracking-manager';\nimport { type EditorInjectionDeps, injectEditor as injectEditorImpl } from './internal/editor-injection';\nimport {\n clearEditingState,\n FOCUSABLE_EDITOR_SELECTOR,\n getEditorAncestor,\n hasRowChanged,\n isInsideOpenAriaOverlay,\n isSafePropertyKey,\n noopUpdateRow,\n shouldPreventEditClose,\n} from './internal/helpers';\nimport type {\n BeforeEditCloseDetail,\n CellCancelDetail,\n CellCommitDetail,\n ChangedRowsResetDetail,\n EditCloseDetail,\n EditingConfig,\n EditOpenDetail,\n EditorParams,\n RowCommitDetail,\n} from './types';\n\n// ============================================================================\n// EditingPlugin\n// ============================================================================\n\n/**\n * Editing Plugin for tbw-grid\n *\n * Enables inline cell editing in the grid. Provides built-in editors for common data types\n * and supports custom editor functions for specialized input scenarios.\n *\n * ## Why Opt-In?\n *\n * Editing is delivered as a plugin rather than built into the core grid:\n *\n * - **Smaller bundle** — Apps that only display data don't pay for editing code\n * - **Clear intent** — Explicit plugin registration makes editing capability obvious\n * - **Runtime validation** — Using `editable: true` without the plugin throws a helpful error\n *\n * ## Installation\n *\n * ```ts\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * ```\n *\n * ## Edit Triggers\n *\n * Configure how editing is triggered with the `editOn` option:\n *\n * | Value | Behavior |\n * |-------|----------|\n * | `'click'` | Single click enters edit mode (default) |\n * | `'dblclick'` | Double-click enters edit mode |\n *\n * ## Keyboard Shortcuts\n *\n * | Key | Action |\n * |-----|--------|\n * | `Enter` | Commit edit and move down |\n * | `Tab` | Commit edit and move right |\n * | `Escape` | Cancel edit, restore original value |\n * | `Arrow Keys` | Navigate between cells (when not editing) |\n *\n * @example Basic editing with double-click trigger\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', editable: true },\n * { field: 'price', type: 'number', editable: true },\n * { field: 'active', type: 'boolean', editable: true },\n * ],\n * plugins: [new EditingPlugin({ editOn: 'dblclick' })],\n * };\n *\n * grid.on('cell-commit', ({ field, oldValue, newValue }) => {\n * console.log(`${field}: ${oldValue} → ${newValue}`);\n * });\n * ```\n *\n * @example Custom editor function\n * ```ts\n * columns: [\n * {\n * field: 'status',\n * editable: true,\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * ['pending', 'active', 'completed'].forEach(opt => {\n * const option = document.createElement('option');\n * option.value = opt;\n * option.textContent = opt;\n * option.selected = ctx.value === opt;\n * select.appendChild(option);\n * });\n * select.addEventListener('change', () => ctx.commit(select.value));\n * return select;\n * },\n * },\n * ]\n * ```\n *\n * @see {@link EditingConfig} for configuration options\n * @see `ColumnEditorContext` for custom editor context\n * @see {@link EditingConfig} for interactive examples in the docs site\n * @since 0.4.0\n */\nexport class EditingPlugin<T = unknown> extends BaseGridPlugin<EditingConfig> {\n /**\n * Plugin manifest - declares owned properties for configuration validation.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n ownedProperties: [\n {\n property: 'editable',\n level: 'column',\n description: 'the \"editable\" column property',\n isUsed: (v) => v === true || typeof v === 'function',\n },\n {\n property: 'editor',\n level: 'column',\n description: 'the \"editor\" column property',\n },\n {\n property: 'editorParams',\n level: 'column',\n description: 'the \"editorParams\" column property',\n },\n {\n property: 'nullable',\n level: 'column',\n description: 'the \"nullable\" column property (allows null values)',\n },\n ],\n events: [\n {\n type: 'cell-edit-committed',\n description: 'Emitted when a cell edit is committed (for plugin-to-plugin coordination)',\n },\n {\n type: 'cell-edit-reverted',\n description:\n 'Emitted when row values are rolled back in place (revertRow/revertAll, row-edit cancel, grid-mode Escape) — not recorded as history',\n },\n ],\n queries: [\n {\n type: 'isEditing',\n description: 'Returns whether any cell is currently being edited',\n },\n {\n type: 'getCellEditableResolver',\n description:\n 'Returns a predicate (field, row) => boolean resolving per-cell editability (rowEditable gate + column.editable true/false/function), for consumers like clipboard paste',\n },\n {\n type: 'commitCellValue',\n description:\n 'Commits a single cell value coming from a core row mutation (updateRow/updateRows), running the full edit pipeline: cancelable cell-commit (validation/abortion), dirty tracking, history, and cascade. Returns true (applied), false (vetoed), or undefined (not handled).',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'editing';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<EditingConfig> {\n return {\n mode: 'row',\n editOn: 'click',\n };\n }\n\n /**\n * Whether the grid is in 'grid' mode (all cells always editable).\n */\n get #isGridMode(): boolean {\n return this.config.mode === 'grid';\n }\n\n /**\n * Resolve whether a given cell is editable.\n *\n * Resolution order:\n * 1. `gridConfig.rowEditable(row)` — row-level gate (if provided). Returns `false` → not editable.\n * 2. `column.editable` — `true`, `false`, or `(row) => boolean`.\n *\n * @returns `true` when the cell should be editable, `false` otherwise.\n */\n #isCellEditable(column: ColumnConfig<T>, row: T): boolean {\n // Row-level gate\n const rowEditable = this.#internalGrid.effectiveConfig?.rowEditable;\n if (rowEditable && !rowEditable(row as any)) return false;\n\n // Column-level check\n const { editable } = column;\n if (typeof editable === 'function') return editable(row);\n return editable === true;\n }\n\n /**\n * Check whether a column has ANY editability configured (static `true` or a\n * function). This is used for quick checks where no specific row is available\n * (e.g. \"does this row have any potentially-editable columns?\").\n */\n #hasEditableConfig(column: ColumnConfig<T>): boolean {\n return column.editable === true || typeof column.editable === 'function';\n }\n\n // #region Editing State (fully owned by plugin)\n\n /** Currently active edit row index, or -1 if not editing */\n #activeEditRow = -1;\n\n /** Row ID of the currently active edit row (stable across _rows replacement) */\n #activeEditRowId: string | undefined;\n\n /** Reference to the row object at edit-open time. Used as fallback in\n * #exitRowEdit when no row ID is available (prevents stale-index access). */\n #activeEditRowRef: T | undefined;\n\n /** Currently active edit column index, or -1 if not editing */\n #activeEditCol = -1;\n\n /** Snapshots of row data before editing started */\n #rowEditSnapshots = new Map<number, T>();\n\n /** Set of cells currently in edit mode: \"rowIndex:colIndex\" */\n #editingCells = new Set<string>();\n\n /**\n * Light-DOM elements whose declarative attributes have already been read.\n * The `editable` attribute (and `<tbw-grid-column-editor>` template) supply\n * the INITIAL state only — seeding once guarantees a later runtime/config\n * change to a falsy value is not re-applied on the next `processColumns`\n * pass (issue #272). Reset in `detach()` so a re-attached instance re-seeds.\n */\n #seededFromAttr = new WeakSet<HTMLElement>();\n\n /**\n * Value-change callbacks for active editors.\n * Keyed by \"rowIndex:field\" → callback that pushes updated values to the editor.\n * Populated during #injectEditor, cleaned up when editors are removed.\n */\n #editorValueCallbacks = new Map<string, (newValue: unknown) => void>();\n\n /** Flag to restore focus after next render (used when exiting edit mode) */\n #pendingFocusRestore = false;\n\n /** Row index pending animation after render, or -1 if none */\n #pendingRowAnimation = -1;\n\n /**\n * Cell validation manager — handles invalid-cell state tracking and DOM sync.\n * Initialized lazily in `attach()` since the sync callback needs grid access.\n */\n #validation!: CellValidationManager;\n\n /**\n * In grid mode, tracks whether an input field is currently focused.\n * When true: arrow keys work within input (edit mode).\n * When false: arrow keys navigate between cells (navigation mode).\n * Escape switches to navigation mode, Enter switches to edit mode.\n */\n #gridModeInputFocused = false;\n\n /**\n * In grid mode, when true, prevents inputs from auto-focusing.\n * This is set when Escape is pressed (navigation mode) and cleared\n * when Enter is pressed or user explicitly clicks an input.\n */\n #gridModeEditLocked = false;\n\n /**\n * When true, only a single cell is being edited (triggered by F2 or `beginCellEdit`).\n * Tab and Arrow keys commit and close the editor instead of navigating to adjacent cells.\n */\n #singleCellEdit = false;\n\n /**\n * In grid mode, snapshot of the focused cell's value when the editor first\n * receives focus. Used to revert on Escape (cell-level cancel).\n */\n #gridModeCellSnapshot: { rowIndex: number; colIndex: number; field: string; value: unknown } | null = null;\n\n // --- Dirty Tracking State (delegated to DirtyTrackingManager) ---\n\n /** Manages all dirty tracking state: baselines, changed/new/committed sets. */\n readonly #dirty = new DirtyTrackingManager<T>();\n\n /**\n * Cells currently mid-`cell-commit`, keyed `` `${rowId ?? rowIndex}\\0${field}` ``.\n * A `cell-commit` listener commonly cascades by calling `updateRow`, which\n * routes back through this commit pipeline synchronously. When such a nested\n * update targets a cell already in this set, `#commitCellValue` returns early\n * (the outer commit owns the single apply + `cell-edit-committed`), preventing\n * the infinite `updateRow → cell-commit → updateRow` recursion WITHOUT any\n * consumer-side re-entrancy guard and WITHOUT duplicating history or bypassing\n * a pending `preventDefault()`.\n */\n readonly #committingCells = new Set<string>();\n\n // --- Editor Injection Deps (cached for #injectEditor delegation) ---\n\n /** Dependency bag created once in `attach()` and reused by every `#injectEditor` call. */\n #editorDeps!: EditorInjectionDeps<T>;\n\n /**\n * Narrows the grid to its row type `T`. `GridElement` is intentionally\n * non-generic, so this accessor re-applies the plugin's own `T`.\n */\n get #internalGrid(): GridHost<T> {\n return this.grid;\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n const signal = this.disconnectSignal;\n const internalGrid = this.#internalGrid;\n\n // Initialize cell validation manager with DOM sync callback\n this.#validation = new CellValidationManager((rowId, field, invalid) => {\n this.#syncInvalidCellAttribute(rowId, field, invalid);\n });\n\n // Initialize editor injection deps (cached for all #injectEditor calls)\n this.#editorDeps = {\n grid: internalGrid,\n isGridMode: this.#isGridMode,\n config: this.config,\n editingCells: this.#editingCells,\n editorValueCallbacks: this.#editorValueCallbacks,\n isEditSessionActive: () => this.#activeEditRow !== -1,\n commitCellValue: (ri, col, val, row) => this.#commitCellValue(ri, col, val, row),\n exitRowEdit: (ri, revert) => this.#exitRowEdit(ri, revert),\n };\n\n // Inject editing state and methods onto grid for backward compatibility\n this.#syncGridEditState();\n\n // Inject changedRows getter\n Object.defineProperty(grid, 'changedRows', {\n get: () => this.changedRows,\n configurable: true,\n });\n\n // Inject changedRowIds getter (new ID-based API)\n Object.defineProperty(grid, 'changedRowIds', {\n get: () => this.changedRowIds,\n configurable: true,\n });\n\n // Inject raw Set for O(1) lookup in the render hot path\n Object.defineProperty(grid, '_changedRowIdSet', {\n get: () => this.#dirty.changedRowIds,\n configurable: true,\n });\n\n // Inject resetChangedRows method\n (grid as any).resetChangedRows = (silent?: boolean) => this.resetChangedRows(silent);\n\n this.#attachRowModeListeners(signal, internalGrid);\n\n // In grid mode, request a full render to trigger afterCellRender hooks\n if (this.#isGridMode) {\n internalGrid._isGridEditMode = true;\n this.gridElement.classList.add('tbw-grid-mode');\n this.requestRender();\n this.#attachGridModeListeners(signal, internalGrid);\n }\n }\n\n /**\n * Row-mode editing wiring: document-level Escape/click-outside exit, the optional\n * focus trap, external `cell-change` push-down into open editors, and undo/redo\n * dirty re-evaluation. Registered unconditionally — each handler self-guards on\n * `#isGridMode` / its own config flag.\n */\n #attachRowModeListeners(signal: AbortSignal, internalGrid: GridHost<T>): void {\n // Document-level Escape to cancel editing (only in 'row' mode)\n document.addEventListener(\n 'keydown',\n (e: KeyboardEvent) => {\n // In grid mode, Escape doesn't exit edit mode\n if (this.#isGridMode) return;\n if (e.key === 'Escape' && this.#activeEditRow !== -1) {\n if (shouldPreventEditClose(this.config, e)) return;\n this.#exitRowEdit(this.#activeEditRow, true);\n }\n },\n { capture: true, signal },\n );\n\n // Click outside to commit editing (only in 'row' mode)\n // Use queueMicrotask to allow pending change events to fire first.\n // This is important for Angular/React editors where the (change) event\n // fires after mousedown but before mouseup/click.\n document.addEventListener(\n 'mousedown',\n (e: MouseEvent) => {\n // In grid mode, clicking outside doesn't exit edit mode\n if (this.#isGridMode) return;\n if (this.#activeEditRow === -1) return;\n const rowEl = internalGrid.findRenderedRowElement?.(this.#activeEditRow);\n if (!rowEl) return;\n const path = (e.composedPath && e.composedPath()) || [];\n if (path.includes(rowEl)) return;\n\n // Check if click is inside a registered external focus container\n // (e.g., overlays, datepickers, dropdowns at <body> level).\n // Only check targets OUTSIDE the grid — clicks on other rows inside\n // the grid should still commit the active edit row.\n const target = e.target as Node | null;\n if (target && !this.gridElement.contains(target) && this.grid.containsFocus?.(target)) {\n return;\n }\n\n // ARIA-expanded fallback (#251): when the click target lives inside\n // an overlay declared by an open combobox/listbox in the active edit\n // row (aria-expanded=\"true\" + aria-controls=<id>), treat it as part\n // of the editor. Covers Downshift / Headless UI / MUI Autocomplete\n // panels that opt out of registerExternalFocusContainer.\n if (target && !this.gridElement.contains(target)) {\n const rowEl = internalGrid.findRenderedRowElement?.(this.#activeEditRow);\n if (rowEl && isInsideOpenAriaOverlay(target, rowEl)) return;\n }\n\n if (shouldPreventEditClose(this.config, e)) return;\n\n // Delay exit to allow pending change/commit events to fire\n queueMicrotask(() => {\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, false);\n }\n });\n },\n { signal },\n );\n\n // Focus trap: when enabled, prevent focus from leaving the grid\n // while a row is being edited. If focus moves outside the grid\n // (and its registered external containers), reclaim it.\n if (this.config.focusTrap) {\n this.gridElement.addEventListener(\n 'focusout',\n (e: FocusEvent) => {\n // Only trap in row mode when actively editing\n if (this.#isGridMode) return;\n if (this.#activeEditRow === -1) return;\n\n const related = e.relatedTarget as Node | null;\n // If focus is going to an external container, that's fine\n if (related && this.grid.containsFocus?.(related)) return;\n // If focus is going to another element inside the grid, allow it\n if (related && this.gridElement.contains(related)) return;\n\n // Focus left the grid entirely — reclaim it\n queueMicrotask(() => {\n // Re-check in case editing was committed in the meantime\n if (this.#activeEditRow === -1) return;\n this.#focusCurrentCellEditor();\n });\n },\n { signal },\n );\n }\n\n // Listen for external row mutations to push updated values to active editors.\n // When field A commits and sets field B via updateRow(), field B's editor\n // (if open) must reflect the new value.\n this.gridElement.addEventListener(\n 'cell-change',\n (e: Event) => {\n const detail = (e as CustomEvent).detail as {\n rowIndex: number;\n field: string;\n newValue: unknown;\n source: string;\n };\n // Only push updates from cascade/api sources — not from the editor's own commit\n if (detail.source === 'user') return;\n const key = `${detail.rowIndex}:${detail.field}`;\n const cb = this.#editorValueCallbacks.get(key);\n if (cb) cb(detail.newValue);\n },\n { signal },\n );\n\n // --- Dirty tracking: listen for undo/redo events to re-evaluate dirty state ---\n if (this.config.dirtyTracking) {\n type HistoryEdit = { rowIndex: number; rowId?: string };\n const handleUndoRedo = (e: Event) => {\n const action = (e as CustomEvent).detail?.action as\n (HistoryEdit & { type: 'cell-edit' }) | { type: 'compound'; actions: HistoryEdit[] } | undefined;\n if (!action) return;\n const edits = action.type === 'compound' ? action.actions : [action];\n const seen = new Set<string>();\n for (const edit of edits) {\n // Resolve by id (#526) — the display index is stale once the view is sorted/filtered.\n // O(1) visible-map lookup only: rows outside the view were already re-evaluated by\n // the 'history' branch of #handleCommitCellValue, so a source scan would buy nothing.\n const row = (\n edit.rowId !== undefined ? this.#internalGrid._getRowEntry(edit.rowId)?.row : this.rows[edit.rowIndex]\n ) as T | undefined;\n if (!row) continue;\n const rowId = edit.rowId ?? this.#safeGetRowId(row);\n if (!rowId || seen.has(rowId)) continue;\n seen.add(rowId);\n const dirty = this.#dirty.isRowDirty(rowId, row);\n this.#emitDirtyChange(rowId, row, dirty ? 'modified' : 'pristine');\n }\n };\n this.gridElement.addEventListener('undo', handleUndoRedo, { signal });\n this.gridElement.addEventListener('redo', handleUndoRedo, { signal });\n\n // Listen for row-inserted events to auto-mark new rows for dirty tracking\n this.on('row-inserted', (detail: { row: T; index: number }) => {\n const rowId = this.#safeGetRowId(detail.row);\n if (rowId != null) {\n this.markAsNew(String(rowId));\n }\n });\n\n // ServerSide blocks can land via an in-place swap + lightweight refresh that\n // never runs processRows, so capture their baselines on arrival — otherwise\n // the loaded rows have no original and an edit could never be reverted.\n // That refresh skips afterRender() on the virtualized path, so request a\n // STYLE pass to emit `baselines-captured` post-render (drained once, so a\n // coalesced full render still emits a single event).\n this.on('datasource:data', (detail: { rows: readonly T[] }) => {\n if (this.#captureBaselines(detail.rows)) this.requestAfterRender();\n });\n }\n }\n\n /**\n * Capture dirty-tracking baselines for `rows` (first-write-wins; rows without an id are skipped).\n * @returns whether any new baseline was captured\n */\n #captureBaselines(rows: readonly T[]): boolean {\n const internalGrid = this.#internalGrid;\n if (!internalGrid.getRowId) return false;\n return this.#dirty.capture(rows, (r) => {\n try {\n return internalGrid.getRowId?.(r);\n } catch {\n return undefined;\n }\n });\n }\n\n /**\n * Grid-mode (\"spreadsheet\") editing wiring: focus/blur tracking that distinguishes\n * navigation mode from edit mode, Escape-to-navigation, and click-to-unlock.\n * Only registered when `editing.mode === 'grid'`.\n */\n #attachGridModeListeners(signal: AbortSignal, internalGrid: GridHost<T>): void {\n // Track focus/blur on inputs to maintain navigation vs edit mode state\n this.gridElement.addEventListener(\n 'focusin',\n (e: FocusEvent) => {\n const target = e.target as HTMLElement;\n // Ignore focus on the grid element itself — it has tabindex=0 so it\n // matches FOCUSABLE_EDITOR_SELECTOR, but blurring + re-focusing it\n // would cause infinite recursion.\n if (target === this.gridElement) return;\n if (target.matches(FOCUSABLE_EDITOR_SELECTOR)) {\n // If edit is locked (navigation mode), blur the input immediately\n if (this.#gridModeEditLocked) {\n target.blur();\n this.gridElement.focus();\n return;\n }\n\n // Snapshot cell value on initial focus or when moving to a different cell.\n // This allows Escape to revert the cell to its pre-edit value.\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n const snap = this.#gridModeCellSnapshot;\n if (!snap || snap.rowIndex !== focusRow || snap.colIndex !== focusCol) {\n const column = internalGrid._visibleColumns?.[focusCol];\n const rowData = internalGrid._rows?.[focusRow];\n if (column?.field && rowData) {\n const field = column.field as string;\n this.#gridModeCellSnapshot = {\n rowIndex: focusRow,\n colIndex: focusCol,\n field,\n value: readCellField(rowData, field),\n };\n }\n }\n\n this.#gridModeInputFocused = true;\n }\n },\n { signal },\n );\n\n this.gridElement.addEventListener(\n 'focusout',\n (e: FocusEvent) => {\n const target = e.target as HTMLElement | null;\n const related = e.relatedTarget as HTMLElement | null;\n // Intra-editor focus moves MUST NOT flip the flag — the editor\n // is still logically focused. Cases observed in the wild:\n // - Native <select> opening: SELECT → child OPTION\n // - Native <select> popup ArrowDown: OPTION → sibling OPTION\n // - Framework editors moving focus to inner popup descendants\n // Detect via two ancestry checks: (a) `target` contains `related`\n // (parent-to-descendant), or (b) `related` lives inside an\n // editor element (input/select/textarea/contenteditable) — this\n // covers OPTION-to-OPTION and any other intra-editor traversal.\n // Exclude `gridElement` itself: it has tabindex=0 and so matches\n // FOCUSABLE_EDITOR_SELECTOR, but focus moving to the grid host\n // means we ARE leaving the editor — the explicit Escape/Enter\n // paths handle that by setting the flag to false themselves.\n if (target && related) {\n if (target.contains(related)) return;\n const relatedEditor = getEditorAncestor(related);\n if (relatedEditor && relatedEditor !== this.gridElement && this.gridElement.contains(relatedEditor)) {\n return;\n }\n }\n // Only clear if focus went outside grid (and external containers) or to a non-input element\n if (\n !related ||\n (!this.gridElement.contains(related) && !this.grid.containsFocus?.(related)) ||\n !related.matches(FOCUSABLE_EDITOR_SELECTOR)\n ) {\n this.#gridModeInputFocused = false;\n // Clear cell snapshot when leaving edit mode normally (not via Escape)\n this.#gridModeCellSnapshot = null;\n }\n },\n { signal },\n );\n\n // Handle Escape key directly on the grid element (capture phase)\n // This ensures we intercept Escape even when focus is inside an input\n this.gridElement.addEventListener(\n 'keydown',\n (e: KeyboardEvent) => {\n if (e.key === 'Escape' && this.#gridModeInputFocused) {\n // Check onBeforeEditClose to let overlays close first.\n if (shouldPreventEditClose(this.config, e)) {\n // Overlay is open — schedule deferred nav-mode transition\n // after the overlay tears down.\n queueMicrotask(() => {\n if (this.#gridModeInputFocused) {\n this.#enterGridModeNavigation();\n }\n });\n return;\n }\n this.#enterGridModeNavigation();\n e.preventDefault();\n e.stopPropagation();\n }\n },\n { capture: true, signal },\n );\n\n // Handle click on inputs - unlock edit mode when user explicitly clicks\n this.gridElement.addEventListener(\n 'mousedown',\n (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n if (target.matches(FOCUSABLE_EDITOR_SELECTOR)) {\n this.#gridModeEditLocked = false; // User clicked input - allow edit\n }\n },\n { signal },\n );\n }\n\n /** @internal */\n override detach(): void {\n const internalGrid = this.#internalGrid;\n internalGrid._isGridEditMode = false;\n this.gridElement.classList.remove('tbw-grid-mode');\n this.#activeEditRow = -1;\n this.#activeEditRowId = undefined;\n this.#activeEditRowRef = undefined;\n this.#activeEditCol = -1;\n this.#rowEditSnapshots.clear();\n this.#dirty.clear();\n this.#editingCells.clear();\n this.#editorValueCallbacks.clear();\n this.#gridModeInputFocused = false;\n this.#gridModeEditLocked = false;\n this.#gridModeCellSnapshot = null;\n this.#singleCellEdit = false;\n this.#seededFromAttr = new WeakSet<HTMLElement>();\n super.detach();\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'isEditing') {\n // In grid mode, we're always editing\n return this.#isGridMode || this.#activeEditRow !== -1;\n }\n if (query.type === 'getCellEditableResolver') {\n // Return a predicate that applies the plugin's full per-cell editability\n // resolution (rowEditable gate + column.editable true/false/function).\n // Fields are mapped to columns once; consumers (e.g. clipboard paste)\n // then ask per cell without importing editing-owned config.\n const columns = (this.#internalGrid.effectiveConfig?.columns as ColumnConfig<T>[] | undefined) ?? [];\n const byField = new Map<string, ColumnConfig<T>>();\n for (const col of columns) byField.set(col.field, col);\n const resolver: CellEditablePredicate = (field, row) => {\n const col = byField.get(field);\n return col ? this.#isCellEditable(col, row as T) : false;\n };\n return resolver;\n }\n if (query.type === 'commitCellValue') {\n return this.#handleCommitCellValue(query.context as CommitCellValueContext);\n }\n return undefined;\n }\n\n /**\n * Answer the core `commitCellValue` query dispatched from `updateRow`/\n * `updateRows`. Routes the change through the full edit pipeline so\n * programmatic mutations participate in validation, dirty tracking, history,\n * and cascade — exactly like an interactive edit.\n *\n * @returns `true` when applied, `false` when a `cell-commit` handler vetoed\n * it (abortion), or `undefined` when the field/row can't be resolved (core\n * then applies the value directly).\n * @internal\n */\n #handleCommitCellValue(ctx: CommitCellValueContext): boolean | undefined {\n const internalGrid = this.#internalGrid;\n const columns = (internalGrid._visibleColumns as ColumnConfig<T>[] | undefined) ?? [];\n const column =\n columns.find((c) => c.field === ctx.field) ??\n (internalGrid._columns as ColumnConfig<T>[] | undefined)?.find((c) => c.field === ctx.field);\n const rowData =\n (ctx.row as T | undefined) ??\n (ctx.rowId != null ? internalGrid._getRowEntry(ctx.rowId)?.row : undefined) ??\n (internalGrid._rows?.[ctx.rowIndex] as T | undefined);\n\n if (ctx.source === 'sync') {\n // Declarative host sync (a framework adapter replacing its `rows` prop in\n // place). Authoritative external data, NOT a user edit: apply the value and\n // re-baseline the cell so the new value is the pristine truth — the row is\n // never marked dirty and no undo/redo history is recorded (and no cascade).\n // Handled before the column guard so non-column fields (dirty tracking\n // snapshots the whole row) are re-baselined too.\n if (!rowData || !isSafePropertyKey(ctx.field)) return undefined;\n writeCellField(rowData, ctx.field, ctx.newValue);\n invalidateAccessorCache(rowData as object);\n const rowId = this.#safeGetRowId(rowData);\n if (rowId && this.config.dirtyTracking) {\n this.#dirty.rebaselineCell(rowId, ctx.field, ctx.newValue);\n const stillDirty = this.#dirty.isRowDirty(rowId, rowData);\n if (!stillDirty) this.#dirty.changedRowIds.delete(rowId);\n this.#emitDirtyChange(rowId, rowData, stillDirty ? 'modified' : 'pristine');\n }\n return true;\n }\n\n if (!column || !rowData || !isSafePropertyKey(ctx.field)) return undefined; // core applies directly\n\n if (ctx.source === 'history') {\n // Undo/redo re-application: apply the value but DO NOT record a fresh\n // history entry or mark the row changed — the undo stack owns this change.\n // Recompute dirty so the row's changed flag reflects its baseline.\n writeCellField(rowData, ctx.field, ctx.newValue);\n invalidateAccessorCache(rowData as object);\n const rowId = this.#safeGetRowId(rowData);\n if (rowId && this.config.dirtyTracking) {\n const dirty = this.#dirty.isRowDirty(rowId, rowData);\n if (!dirty) this.#dirty.changedRowIds.delete(rowId);\n this.#emitDirtyChange(rowId, rowData, dirty ? 'modified' : 'pristine');\n }\n return true;\n }\n\n // Normal path: full commit pipeline (cancelable cell-commit for\n // validation/abortion, dirty tracking, history via cell-edit-committed,\n // cascade). #commitCellValue leaves the value unchanged when a handler\n // vetoes, so compare to detect abortion.\n this.#commitCellValue(ctx.rowIndex, column, ctx.newValue, rowData, ctx.source);\n return readCellField(rowData, ctx.field) === ctx.newValue;\n }\n\n // #endregion\n\n // #region Event Handlers (event distribution)\n\n /**\n * Handle cell clicks - start editing if configured for click mode.\n * Both click and dblclick events come through this handler.\n * Starts row-based editing (all editable cells in the row get editors).\n * @internal\n */\n override onCellClick(event: CellClickEvent): boolean | void {\n // In grid mode, all cells are already editable - no need to trigger row edit\n if (this.#isGridMode) return false;\n\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n\n // Check if editing is disabled\n if (editOn === false || editOn === 'manual') return false;\n\n // Check if this is click or dblclick mode\n if (editOn !== 'click' && editOn !== 'dblclick') return false;\n\n // Check if the event type matches the edit mode\n const isDoubleClick = event.originalEvent.type === 'dblclick';\n if (editOn === 'click' && isDoubleClick) return false; // In click mode, only handle single clicks\n if (editOn === 'dblclick' && !isDoubleClick) return false; // In dblclick mode, only handle double clicks\n\n const { rowIndex } = event;\n\n // Check if any column in the row is potentially editable\n const hasEditableColumn = internalGrid._columns?.some((col) => this.#hasEditableConfig(col as ColumnConfig<T>));\n if (!hasEditableColumn) return false;\n\n // Row-level gate\n if (!this.#isRowEditable(internalGrid._rows[rowIndex] as T | undefined)) return false;\n\n // Start row-based editing (all editable cells get editors)\n event.originalEvent.stopPropagation();\n this.beginBulkEdit(rowIndex);\n return true; // Handled\n }\n\n /**\n * Handle keyboard events for edit lifecycle.\n * @internal\n */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n switch (event.key) {\n case 'Escape':\n return this.#onEscapeKey(event);\n case 'ArrowUp':\n case 'ArrowDown':\n case 'ArrowLeft':\n case 'ArrowRight':\n return this.#onArrowKey(event);\n case 'Tab':\n return this.#onTabKey(event);\n case ' ':\n case 'Spacebar':\n return this.#onSpaceKey(event);\n case 'Enter':\n // Modified Enter (Shift/Ctrl/Alt/Meta) is not ours — don't block it.\n if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return false;\n return this.#onEnterKey(event);\n case 'F2':\n return this.#onF2Key(event);\n default:\n // Don't block other keyboard events\n return false;\n }\n }\n\n /**\n * Escape: cancel the current edit (row mode) or leave edit mode (grid mode).\n *\n * NOTE: No `onBeforeEditClose` check here — the capture-phase handler is the\n * authoritative overlay guard. If the event reaches this bubble-phase handler,\n * either (a) the capture handler already bailed (overlay was open) and the\n * template handler (e.g. `select.close()`) addressed it, or (b) no overlay was\n * open. In both cases we should transition to navigation mode.\n */\n #onEscapeKey(event: KeyboardEvent): boolean {\n // In grid mode: revert cell, blur input, enter navigation mode.\n if (this.#isGridMode && this.#gridModeInputFocused) {\n this.#enterGridModeNavigation();\n // Update focus styling\n this.requestAfterRender();\n return true;\n }\n\n // In row mode: cancel edit\n if (this.#activeEditRow !== -1 && !this.#isGridMode) {\n if (shouldPreventEditClose(this.config, event)) return true;\n this.#exitRowEdit(this.#activeEditRow, true);\n return true;\n }\n\n return false;\n }\n\n /**\n * Arrow keys — decides whether the grid navigates or the editor keeps the key.\n *\n * `#gridModeInputFocused` is a cached flag, so `event.target` is used as a\n * fallback: a stale flag (e.g. between a native `<select>` popup opening and\n * our focusout/focusin guard updating it) must not hijack ArrowUp/Down away\n * from a focused editor. Uses `closest()` (not `matches()`) so descendants of\n * an editor — most notably `<option>` children of an open native `<select>`\n * popup — also count as \"keydown originating inside an editor\".\n */\n #onArrowKey(event: KeyboardEvent): boolean {\n const arrowEditorAncestor = getEditorAncestor(event.target);\n const targetIsEditor = !!arrowEditorAncestor && arrowEditorAncestor !== this.gridElement;\n const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';\n\n // Grid mode, no editor focused: let the grid's default navigation handle it.\n if (this.#isGridMode && !this.#gridModeInputFocused && !targetIsEditor) return false;\n\n // Grid mode with a focused editor: ArrowUp/Down belong to the editor\n // (e.g. ArrowDown opens autocomplete/datepicker overlays, ArrowUp/Down\n // navigates options). Handled: block grid navigation, let event reach editor.\n if (this.#isGridMode && (this.#gridModeInputFocused || targetIsEditor) && isVertical) return true;\n\n // Arrow Up/Down while a row is in edit mode (row mode): the editor owns\n // the key. The grid does NOT commit + jump to an adjacent row — the user\n // must explicitly leave edit mode (Enter / Escape / Tab / click) before\n // arrow keys resume cell navigation. Rationale:\n // - <input type=\"number\"> spinners must work natively\n // - <select> / native popups need ArrowUp/Down to traverse options\n // - <textarea> needs ArrowUp/Down to move the caret between lines\n // - Framework-adapter editors (Angular Material, MUI, etc.) commonly\n // bind ArrowUp/Down for autocomplete, datepicker, number-stepper,\n // and combobox interactions\n // Returning `true` blocks the core keyboard handler from running its\n // commit-and-navigate fallback (see `core/internal/keyboard.ts`'s\n // ArrowUp/Down switch case). The native event still reaches the focused\n // editor so it can consume it however it needs to.\n if (isVertical && this.#activeEditRow !== -1 && !this.#isGridMode) return true;\n\n return false;\n }\n\n /** Tab/Shift+Tab while editing: move to the next/previous editable cell. */\n #onTabKey(event: KeyboardEvent): boolean {\n if (this.#activeEditRow === -1 && !this.#isGridMode) return false;\n\n event.preventDefault();\n\n // In single-cell edit mode (F2), commit and close instead of navigating\n if (this.#singleCellEdit) {\n this.#exitRowEdit(this.#activeEditRow, false);\n return true;\n }\n\n this.#handleTabNavigation(!event.shiftKey);\n return true;\n }\n\n /** Space: toggle boolean cells (only when not editing — editors own their own space). */\n #onSpaceKey(event: KeyboardEvent): boolean {\n // Modified Space is a selection chord (Shift/Ctrl/⌘+Space) — never mutate data on it.\n if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return false;\n // If we're in row edit mode, let the event pass through to the editor (e.g., checkbox)\n if (this.#activeEditRow !== -1) return false;\n\n const internalGrid = this.#internalGrid;\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n if (focusRow < 0 || focusCol < 0) return false;\n\n const column = internalGrid._visibleColumns[focusCol];\n const rowData = internalGrid._rows[focusRow];\n if (!column || !rowData || column.type !== 'boolean') return false;\n if (!this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return false;\n\n const field = column.field;\n // Space on a cell we cannot safely write - don't block keyboard navigation\n if (!isSafePropertyKey(field)) return false;\n\n this.#commitCellValue(focusRow, column, !readCellField(rowData, field), rowData);\n event.preventDefault();\n // Re-render to update the UI\n this.requestRender();\n return true;\n }\n\n /** Enter (unmodified): start row edit, commit, or enter edit mode in grid mode. */\n #onEnterKey(event: KeyboardEvent): boolean {\n // In grid mode when not editing: focus the current cell's input\n if (this.#isGridMode && !this.#gridModeInputFocused) {\n this.#focusCurrentCellEditor();\n return true;\n }\n\n if (this.#isGridMode && this.#gridModeInputFocused) return this.#onEnterWhileEditorFocused(event);\n\n if (this.#activeEditRow !== -1) {\n // ARIA-expanded fallback (#251): when the focused control declares\n // an open overlay via aria-expanded=\"true\" + aria-controls, defer\n // Enter to the overlay so combobox confirmation does not exit the\n // row. Mirrors the editor-injection.ts handler so the same guard\n // applies whether the editor host or the grid receives the event.\n const ariaTarget = event.target as HTMLElement | null;\n if (\n ariaTarget &&\n ariaTarget.getAttribute?.('aria-expanded') === 'true' &&\n ariaTarget.hasAttribute?.('aria-controls')\n ) {\n return false;\n }\n if (shouldPreventEditClose(this.config, event)) return true;\n // Already editing - let cell handlers deal with it\n return false;\n }\n\n return this.#beginRowEditFromEnter(event);\n }\n\n /**\n * Enter in grid mode while an editor (or its descendant — most notably an\n * `<option>` inside an open native `<select>` popup) has focus. Does NOT start\n * row-based editing or re-render. Two sub-cases:\n * - target is a **descendant** of an editor (popup option): let the browser's\n * native commit-and-close run untouched — calling `beginBulkEdit` /\n * re-rendering would destroy the open popup and abort the commit.\n * - target **is** the editor itself (focused SELECT after popup closed,\n * focused INPUT with no popup): commit the current value, blur the editor,\n * return focus to the grid so arrow keys resume cell navigation. Mirrors\n * Escape but accepts the value instead of reverting.\n */\n #onEnterWhileEditorFocused(event: KeyboardEvent): boolean {\n const enterEditorAncestor = getEditorAncestor(event.target);\n if (enterEditorAncestor && enterEditorAncestor !== event.target) {\n // Descendant — let native commit fire; do not interfere. Return true so\n // core's Enter handler (which would dispatch cell-activate) does not also\n // run. We don't call preventDefault, so the browser still commits the popup.\n return true;\n }\n // Editor itself — commit + blur + return to navigation mode.\n this.#gridModeCellSnapshot = null;\n const activeEl = document.activeElement as HTMLElement | null;\n if (activeEl && this.gridElement.contains(activeEl)) {\n activeEl.blur();\n this.gridElement.focus();\n }\n this.#gridModeInputFocused = false;\n this.#gridModeEditLocked = true;\n this.requestAfterRender();\n return true;\n }\n\n /**\n * Enter on a focused cell with no edit in progress: start row-based editing\n * (not just the focused cell), after giving consumers a cancelable\n * `cell-activate` event.\n */\n #beginRowEditFromEnter(event: KeyboardEvent): boolean {\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n if (editOn === false || editOn === 'manual') return false;\n\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n if (focusRow < 0) return false;\n\n // Check if ANY column in the row is potentially editable\n const hasEditableColumn = internalGrid._columns?.some((col) => this.#hasEditableConfig(col as ColumnConfig<T>));\n // Row-level gate. No editable columns - don't block keyboard navigation.\n const row = internalGrid._rows[focusRow];\n if (!hasEditableColumn || !this.#isRowEditable(row as T | undefined)) return false;\n\n // Emit cell-activate event BEFORE starting edit\n // This ensures consumers always get the activation event\n const column = internalGrid._visibleColumns[focusCol];\n const field = column?.field ?? '';\n const value = field && row ? readCellField(row, field) : undefined;\n const cellEl = this.gridElement.querySelector(`[data-row=\"${focusRow}\"][data-col=\"${focusCol}\"]`) as\n HTMLElement | undefined;\n\n const activateEvent = new CustomEvent('cell-activate', {\n cancelable: true,\n bubbles: true,\n detail: {\n rowIndex: focusRow,\n colIndex: focusCol,\n field,\n value,\n row,\n cellEl,\n trigger: 'keyboard' as const,\n originalEvent: event,\n },\n });\n this.gridElement.dispatchEvent(activateEvent);\n\n // If consumer canceled the activation, don't start editing\n if (activateEvent.defaultPrevented) {\n event.preventDefault();\n return true;\n }\n\n this.beginBulkEdit(focusRow);\n return true;\n }\n\n /** F2: begin single-cell edit on the focused cell. */\n #onF2Key(event: KeyboardEvent): boolean {\n if (this.#activeEditRow !== -1 || this.#isGridMode) return false;\n\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n if (editOn === false) return false;\n\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n if (focusRow < 0 || focusCol < 0) return false;\n\n const column = internalGrid._visibleColumns[focusCol];\n const rowData = internalGrid._rows[focusRow];\n if (!column || !rowData || !column.field) return false;\n if (!this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return false;\n\n event.preventDefault();\n this.beginCellEdit(focusRow, column.field);\n return true;\n }\n\n // #endregion\n\n // #region Render Hooks\n\n /**\n * Parse `data-*` attributes from a `<tbw-grid-type>` element into a camelCased map.\n */\n #readTypeParams(typeEl: HTMLElement): Record<string, unknown> {\n const params: Record<string, unknown> = {};\n for (const attr of Array.from(typeEl.attributes)) {\n if (!attr.name.startsWith('data-')) continue;\n const key = attr.name\n .slice(5)\n .replace(/-([a-z])/g, (_m, letter: string) => letter.toUpperCase())\n .trim();\n if (!key) continue;\n params[key] = attr.value;\n }\n return params;\n }\n\n /**\n * Seed type-level editor defaults from declarative `<tbw-grid-type>` light-DOM.\n *\n * This mirrors the column-level `<tbw-grid-column-editor>` behavior: editor\n * templates are editing-plugin-owned and only become active when the editing\n * plugin is imported.\n */\n #seedTypeEditorsFromLightDom(): void {\n const internalGrid = this.#internalGrid;\n const host = internalGrid._hostElement;\n if (!host) return;\n\n const effectiveConfig = internalGrid.effectiveConfig;\n if (!effectiveConfig) return;\n\n const typeElements = Array.from(host.children).filter((el) => el.tagName.toLowerCase() === 'tbw-grid-type');\n if (!typeElements.length) return;\n\n const adapter = internalGrid.__frameworkAdapter;\n const typeDefaults = (effectiveConfig.typeDefaults ??= {});\n\n for (let i = 0; i < typeElements.length; i++) {\n const typeEl = typeElements[i] as HTMLElement;\n const name = typeEl.getAttribute('name')?.trim();\n if (!name) continue;\n\n const existing = typeDefaults[name];\n // Programmatic (or already-seeded) editor always wins.\n if (existing?.editor) continue;\n\n const editorTpl = typeEl.querySelector('tbw-grid-column-editor') as HTMLElement | null;\n const editorTarget = (editorTpl ?? typeEl) as HTMLElement;\n\n let editor: TypeDefault<T>['editor'] | undefined = adapter?.canHandle?.(editorTarget)\n ? (adapter.createEditor?.(editorTarget) as TypeDefault<T>['editor'] | undefined)\n : undefined;\n\n if (!editor && editorTpl) {\n const params = this.#readTypeParams(typeEl);\n const compiled = compileTemplate(editorTpl.innerHTML);\n editor = ((ctx) =>\n compiled({\n row: ctx.row as T,\n value: ctx.value,\n field: ctx.field,\n column: ctx.column as ColumnInternal<T>,\n typeDefault: params,\n })) as TypeDefault<T>['editor'];\n }\n\n if (!editor) continue;\n\n typeDefaults[name] = { ...(existing ?? {}), editor };\n }\n }\n\n /**\n * Process columns to merge type-level editorParams with column-level and to\n * read editing-owned attributes from declarative `<tbw-grid-column>` markup.\n *\n * Column-level params take precedence over type-level. Editing-owned\n * attributes (`editable`, `<tbw-grid-column-editor>` template) are parsed\n * here from `col.__element` rather than in core, so the grid carries no\n * editing knowledge (issue #272).\n * @internal\n */\n override processColumns(columns: ColumnConfig<T>[]): ColumnConfig<T>[] {\n const internalGrid = this.#internalGrid;\n this.#seedTypeEditorsFromLightDom();\n const typeDefaults = (internalGrid as any).effectiveConfig?.typeDefaults;\n const adapter = internalGrid.__frameworkAdapter;\n const hasTypeDefaults = !!typeDefaults || !!adapter?.getTypeDefault;\n\n return columns.map((col) => {\n // Read editing-owned attributes from the originating light-DOM element.\n // Each element is seeded at most once: the attribute supplies the INITIAL\n // value only, so config and later runtime changes always win and are not\n // overridden by re-reading the attribute on subsequent passes (#272).\n const el = (col as ColumnInternal<T>).__element;\n if (el && !this.#seededFromAttr.has(el)) {\n this.#seededFromAttr.add(el);\n // `col.editable == null` lets a programmatic config value (including an\n // explicit `editable: false`) win over the attribute on this first pass.\n if (col.editable == null && el.hasAttribute('editable')) {\n col.editable = el.getAttribute('editable') !== 'false';\n }\n const internalCol = col as ColumnInternal<T>;\n if (!internalCol.__editorTemplate) {\n const editorTpl = el.querySelector('tbw-grid-column-editor');\n if (editorTpl) internalCol.__editorTemplate = editorTpl as HTMLElement;\n }\n }\n\n if (!col.type || !hasTypeDefaults) return col;\n\n // Get type-level editorParams\n let typeEditorParams: EditorParams | undefined;\n\n // Check grid-level typeDefaults first\n if (typeDefaults?.[col.type]?.editorParams) {\n typeEditorParams = typeDefaults[col.type].editorParams;\n }\n\n // Then check app-level (adapter) typeDefaults\n if (!typeEditorParams && adapter?.getTypeDefault) {\n const appDefault = adapter.getTypeDefault<T>(col.type, internalGrid._hostElement);\n if (appDefault?.editorParams) {\n typeEditorParams = appDefault.editorParams;\n }\n }\n\n // No type-level params to merge\n if (!typeEditorParams) return col;\n\n // Merge: type-level as base, column-level wins on conflicts\n return {\n ...col,\n editorParams: { ...typeEditorParams, ...col.editorParams },\n };\n });\n }\n\n /**\n * Stabilize the actively edited row across `rows` array replacements and\n * capture dirty tracking baselines.\n *\n * **Editing stability:** When the consumer reassigns `grid.rows` while\n * editing, the full pipeline (sort, filter, group) runs on the new data.\n * This hook finds the edited row in the new array by ID and swaps in the\n * in-progress row reference (`#activeEditRowRef`) so editors survive.\n *\n * **Dirty tracking baselines:** When `dirtyTracking` is enabled, captures\n * a `structuredClone` snapshot of each row on first appearance\n * (first-write-wins). This prevents Angular's feedback loop from\n * overwriting baselines.\n *\n * @internal Plugin API — part of the render pipeline\n */\n override processRows(rows: readonly T[]): T[] {\n // --- Dirty tracking: capture baselines (first-write-wins) ---\n if (this.config.dirtyTracking) this.#captureBaselines(rows);\n\n // --- Editing stability: swap in the in-progress row ---\n if (this.#activeEditRow === -1 || this.#isGridMode) return rows as T[];\n\n const editRowId = this.#activeEditRowId;\n const editRowRef = this.#activeEditRowRef;\n\n // Without a stable row ID we cannot match across array replacements\n if (!editRowId || !editRowRef) return rows as T[];\n\n const result = [...rows] as T[];\n\n // Find the edited row's new position by ID\n let newIndex = -1;\n for (let i = 0; i < result.length; i++) {\n if (this.#safeGetRowId(result[i]) === editRowId) {\n newIndex = i;\n break;\n }\n }\n\n if (newIndex === -1) {\n // Row was deleted server-side — close the editor.\n // Cannot close synchronously during the processRows pipeline;\n // schedule for after the current render cycle completes.\n setTimeout(() => this.cancelActiveRowEdit(), 0);\n return result;\n }\n\n // Swap in the in-progress row data to preserve editor state\n result[newIndex] = editRowRef;\n\n // Update index-keyed state if the position changed (due to sort/filter)\n if (this.#activeEditRow !== newIndex) {\n this.#migrateEditRowIndex(this.#activeEditRow, newIndex);\n }\n\n return result;\n }\n\n /**\n * After render, reapply editors to cells in edit mode.\n * This handles virtualization - when a row scrolls back into view,\n * we need to re-inject the editor.\n * @internal\n */\n override afterRender(): void {\n const internalGrid = this.#internalGrid;\n\n // --- Editing stability: verify active edit row index ---\n // After processRows, subsequent plugins (filtering, grouping) may have\n // shifted row indices. Verify the index is still correct and fix if needed\n // before re-injecting editors.\n if (this.#activeEditRow !== -1 && this.#activeEditRowRef && !this.#isGridMode) {\n if (internalGrid._rows[this.#activeEditRow] !== this.#activeEditRowRef) {\n const newIndex = (internalGrid._rows as T[]).indexOf(this.#activeEditRowRef);\n if (newIndex !== -1) {\n this.#migrateEditRowIndex(this.#activeEditRow, newIndex);\n } else {\n // Row no longer in rendered set (filtered out or deleted)\n setTimeout(() => this.cancelActiveRowEdit(), 0);\n return;\n }\n }\n }\n\n // Restore focus after exiting edit mode\n if (this.#pendingFocusRestore) {\n this.#pendingFocusRestore = false;\n this.#restoreCellFocus(internalGrid);\n }\n\n // Animate the row after render completes (so the row element exists)\n if (this.#pendingRowAnimation !== -1) {\n const rowIndex = this.#pendingRowAnimation;\n this.#pendingRowAnimation = -1;\n internalGrid.animateRow?.(rowIndex, 'change');\n }\n\n // Emit baselines-captured event when new baselines were captured this cycle.\n // Emitted post-render so consumers can safely read grid.rows, query the DOM,\n // or call getOriginalRow() in their handler.\n const capturedCount = this.#dirty.drainCapturedFlag();\n if (capturedCount != null) {\n this.emit<BaselinesCapturedDetail>('baselines-captured', {\n count: capturedCount,\n });\n }\n\n // In 'grid' mode, editors are injected via afterCellRender hook during render\n if (this.#isGridMode) return;\n\n if (this.#editingCells.size === 0) return;\n\n // Re-inject editors for any editing cells that are visible\n for (const cellKey of this.#editingCells) {\n const [rowStr, colStr] = cellKey.split(':');\n const rowIndex = parseInt(rowStr, 10);\n const colIndex = parseInt(colStr, 10);\n\n const cellEl = this.#getCell(rowIndex, colIndex);\n if (!cellEl || cellEl.classList.contains('editing')) continue;\n\n // Cell is visible but not in editing mode - reinject editor\n const rowData = internalGrid._rows[rowIndex];\n const column = internalGrid._visibleColumns[colIndex];\n if (rowData && column) {\n this.#injectEditor(rowData, rowIndex, column, colIndex, cellEl, true);\n }\n }\n }\n\n /**\n * Hook called after each cell is rendered.\n * In grid mode, injects editors into editable cells during render (no DOM queries needed).\n * @internal\n */\n override afterCellRender(context: AfterCellRenderContext): void {\n const { row, rowIndex, column, colIndex, cellElement } = context;\n\n const editable = this.#isCellEditable(column as ColumnConfig<T>, row as T);\n\n // Set aria-readonly on non-editable cells so screen readers can distinguish\n if (!editable) {\n cellElement.setAttribute('aria-readonly', 'true');\n // In grid mode, tear down stale editors on cells that are no longer editable\n // (e.g., after virtualization recycles a row element for different row data)\n if (this.#isGridMode && cellElement.classList.contains('editing')) {\n cellElement.classList.remove('editing');\n const value = readCellField(row, (column as ColumnConfig<T>).field);\n cellElement.textContent = value == null ? '' : String(value);\n }\n } else {\n cellElement.removeAttribute('aria-readonly');\n }\n\n // Only inject editors in grid mode\n if (!this.#isGridMode) return;\n\n // Skip non-editable cells\n if (!editable) return;\n\n // Skip if already has editor\n if (cellElement.classList.contains('editing')) return;\n\n // Inject editor (don't track in editingCells - we're always editing in grid mode)\n // Pass rowElement so incrementEditingCount works even when cell is in a DocumentFragment\n this.#injectEditor(row as T, rowIndex, column as ColumnConfig<T>, colIndex, cellElement, true, context.rowElement);\n }\n\n /**\n * Apply dirty-tracking CSS classes to each rendered row.\n *\n * - `tbw-cell-dirty` on individual cells whose value differs from baseline\n * (applied on cell-commit, visible during editing)\n * - `tbw-row-dirty` on the row element only after the row edit session is\n * committed (edit-close without cancel)\n * - `tbw-row-new` when a row was inserted via `insertRow()` with no baseline\n *\n * Only active when `dirtyTracking: true`.\n *\n * @internal Plugin API\n */\n override afterRowRender(context: AfterRowRenderContext): void {\n if (!this.config.dirtyTracking) return;\n\n const internalGrid = this.#internalGrid;\n const rowId = internalGrid.getRowId?.(context.row as T);\n if (!rowId) return;\n\n const { isNew, isCommittedDirty, hasBaseline } = this.#dirty.getRowDirtyState(rowId, context.row as T);\n\n const el = context.rowElement;\n\n // Row-level classes (tbw-row-dirty only after row-commit AND data differs)\n el.classList.toggle('tbw-row-dirty', isCommittedDirty);\n el.classList.toggle('tbw-row-new', isNew);\n\n // Cell-level classes (tbw-cell-dirty on individual cells with changed values)\n // Only run the per-cell loop when the row has a baseline — avoids\n // querySelectorAll on the hot path for rows without dirty tracking state.\n if (hasBaseline) {\n const cells = el.querySelectorAll('.cell[data-field]');\n for (let i = 0; i < cells.length; i++) {\n const cell = cells[i] as HTMLElement;\n const field = cell.getAttribute('data-field');\n if (field) {\n cell.classList.toggle('tbw-cell-dirty', this.#dirty.isCellDirty(rowId, context.row as T, field));\n }\n }\n } else {\n // Clean up stale tbw-cell-dirty classes on recycled row elements\n // that previously displayed a dirty row but now show a pristine one.\n const dirtyCells = el.querySelectorAll('.tbw-cell-dirty');\n for (let i = 0; i < dirtyCells.length; i++) {\n dirtyCells[i].classList.remove('tbw-cell-dirty');\n }\n }\n }\n\n /**\n * On scroll render, reapply editors to recycled cells.\n * @internal\n */\n override onScrollRender(): void {\n this.afterRender();\n }\n\n // #endregion\n\n // #region Public API\n\n /**\n * Get all rows that have been modified.\n * Uses ID-based lookup for stability when rows are reordered.\n */\n get changedRows(): T[] {\n return this.#dirty.getChangedRows((id) => this.grid.getRow(id) as T | undefined);\n }\n\n /**\n * Get IDs of all modified rows.\n */\n get changedRowIds(): string[] {\n return this.#dirty.getChangedRowIds();\n }\n\n /**\n * Get the currently active edit row index, or -1 if not editing.\n */\n get activeEditRow(): number {\n return this.#activeEditRow;\n }\n\n /**\n * Get the currently active edit column index, or -1 if not editing.\n */\n get activeEditCol(): number {\n return this.#activeEditCol;\n }\n\n /**\n * Check if a specific row is currently being edited.\n */\n isRowEditing(rowIndex: number): boolean {\n return this.#activeEditRow === rowIndex;\n }\n\n /**\n * Check if a specific cell is currently being edited.\n */\n isCellEditing(rowIndex: number, colIndex: number): boolean {\n return this.#editingCells.has(`${rowIndex}:${colIndex}`);\n }\n\n /**\n * Check if a specific row has been modified.\n * @param rowIndex - Row index to check (will be converted to ID internally)\n */\n isRowChanged(rowIndex: number): boolean {\n const internalGrid = this.#internalGrid;\n const row = internalGrid._rows[rowIndex];\n if (!row) return false;\n try {\n const rowId = internalGrid.getRowId?.(row);\n return rowId ? this.#dirty.isRowChanged(rowId) : false;\n } catch {\n return false;\n }\n }\n\n /**\n * Check if a row with the given ID has been modified.\n * @param rowId - Row ID to check\n */\n isRowChangedById(rowId: string): boolean {\n return this.#dirty.isRowChanged(rowId);\n }\n\n // #region Dirty Tracking API (delegated to DirtyTrackingManager)\n\n /** Check if a row differs from its baseline. Requires `dirtyTracking: true`. */\n isDirty(rowId: string): boolean {\n if (!this.config.dirtyTracking) return false;\n if (this.#dirty.newRowIds.has(rowId)) return true;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return false;\n return this.#dirty.isRowDirty(rowId, row);\n }\n\n /** Inverse of `isDirty`. */\n isPristine(rowId: string): boolean {\n return !this.isDirty(rowId);\n }\n\n /** Whether any row in the grid is dirty. */\n get dirty(): boolean {\n if (!this.config.dirtyTracking) return false;\n const internalGrid = this.#internalGrid;\n return this.#dirty.hasAnyDirty((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /** Whether all rows are pristine. */\n get pristine(): boolean {\n return !this.dirty;\n }\n\n /** Re-snapshot baseline from current data (call after a successful save). */\n markAsPristine(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return;\n this.#dirty.markPristine(rowId, row);\n // After mark-pristine, original === current — emit row as the canonical value.\n this.emit<DirtyChangeDetail<T>>('dirty-change', {\n rowId,\n row,\n original: row,\n type: 'pristine',\n });\n }\n\n /** Mark a row as new (auto-called by `insertRow()` when dirty tracking is on). */\n markAsNew(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n this.#dirty.markNew(rowId);\n const row = this.grid.getRow(rowId) as T | undefined;\n // `original` is intentionally undefined for new rows — emit directly\n // rather than via #emitDirtyChange (which always derives original from\n // the baseline map).\n this.emit<DirtyChangeDetail<T>>('dirty-change', {\n rowId,\n row: row as T,\n original: undefined,\n type: 'new',\n });\n }\n\n /** Mark a row as dirty after an external mutation that bypassed the editing pipeline. */\n markAsDirty(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return;\n this.#dirty.markDirty(rowId);\n this.#emitDirtyChange(rowId, row, 'modified');\n }\n\n /** Mark all tracked rows as pristine (call after a batch save). */\n markAllPristine(): void {\n if (!this.config.dirtyTracking) return;\n const internalGrid = this.#internalGrid;\n this.#dirty.markAllPristine((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /** Get a deep clone of the original (baseline) row data. Returns `undefined` for new rows. */\n getOriginalRow(rowId: string): T | undefined {\n if (!this.config.dirtyTracking) return undefined;\n return this.#dirty.getOriginalRow(rowId);\n }\n\n /** Lightweight check for whether a baseline exists (no cloning). */\n hasBaseline(rowId: string): boolean {\n if (!this.config.dirtyTracking) return false;\n return this.#dirty.hasBaseline(rowId);\n }\n\n /** Get all dirty rows with their original and current data. */\n getDirtyRows(): DirtyRowEntry<T>[] {\n if (!this.config.dirtyTracking) return [];\n const internalGrid = this.#internalGrid;\n return this.#dirty.getDirtyRows((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /**\n * Get IDs of all dirty rows.\n */\n get dirtyRowIds(): string[] {\n if (!this.config.dirtyTracking) return [];\n const internalGrid = this.#internalGrid;\n return this.#dirty.getDirtyRowIds((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /**\n * Revert a row to its baseline values (mutates the current row in-place).\n * Triggers a re-render.\n *\n * @param rowId - Row ID (from `getRowId`)\n */\n revertRow(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return;\n if (this.#dirty.revertRow(rowId, row)) {\n this.#emitDirtyChange(rowId, row, 'reverted');\n this.emitPluginEvent('cell-edit-reverted', { rowId });\n this.requestRender();\n }\n }\n\n /**\n * Revert all dirty rows to their baseline values and re-render.\n */\n revertAll(): void {\n if (!this.config.dirtyTracking) return;\n const internalGrid = this.#internalGrid;\n this.#dirty.revertAll((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n this.emitPluginEvent('cell-edit-reverted', {});\n this.requestRender();\n }\n\n // #endregion\n\n // #region Cell Validation (delegated to CellValidationManager)\n\n /**\n * Mark a cell as invalid with an optional validation message.\n * Invalid cells are marked with a `data-invalid` attribute for styling.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @param message - Optional validation message (for tooltips or display)\n *\n * @example\n * ```typescript\n * // In cell-commit handler:\n * grid.on('cell-commit', (detail, e) => {\n * if (detail.field === 'email' && !isValidEmail(detail.value)) {\n * detail.setInvalid('Invalid email format');\n * }\n * });\n *\n * // Or programmatically:\n * editingPlugin.setInvalid('row-123', 'email', 'Invalid email format');\n * ```\n */\n setInvalid(rowId: string, field: string, message = ''): void {\n this.#validation.setInvalid(rowId, field, message);\n }\n\n /** Clear the invalid state for a specific cell. */\n clearInvalid(rowId: string, field: string): void {\n this.#validation.clearInvalid(rowId, field);\n }\n\n /** Clear all invalid cells for a specific row. */\n clearRowInvalid(rowId: string): void {\n this.#validation.clearRowInvalid(rowId);\n }\n\n /** Clear all invalid cell states across all rows. */\n clearAllInvalid(): void {\n this.#validation.clearAllInvalid();\n }\n\n /** Check if a specific cell is marked as invalid. */\n isCellInvalid(rowId: string, field: string): boolean {\n return this.#validation.isCellInvalid(rowId, field);\n }\n\n /** Get the validation message for an invalid cell. */\n getInvalidMessage(rowId: string, field: string): string | undefined {\n return this.#validation.getInvalidMessage(rowId, field);\n }\n\n /** Check if a row has any invalid cells. */\n hasInvalidCells(rowId: string): boolean {\n return this.#validation.hasInvalidCells(rowId);\n }\n\n /** Get all invalid fields for a row. */\n getInvalidFields(rowId: string): Map<string, string> {\n return this.#validation.getInvalidFields(rowId);\n }\n\n // #endregion\n\n /**\n * Reset all change tracking.\n * @param silent - If true, suppresses the `changed-rows-reset` event\n * @fires changed-rows-reset - Emitted when tracking is reset (unless silent)\n */\n resetChangedRows(silent?: boolean): void {\n const rows = this.changedRows;\n const ids = this.changedRowIds;\n this.#dirty.changedRowIds.clear();\n this.#dirty.committedDirtyRowIds.clear();\n this.#syncGridEditState();\n\n if (!silent) {\n this.emit<ChangedRowsResetDetail<T>>('changed-rows-reset', { rows, ids });\n }\n\n // Clear visual indicators\n const internalGrid = this.#internalGrid;\n internalGrid._rowPool?.forEach((r) => r.classList.remove('changed'));\n }\n\n /**\n * Programmatically begin editing a cell.\n * @param rowIndex - Index of the row to edit\n * @param field - Field name of the column to edit\n * @fires cell-commit - Emitted when the cell value is committed (on blur or Enter)\n */\n beginCellEdit(rowIndex: number, field: string): void {\n const internalGrid = this.#internalGrid;\n const colIndex = internalGrid._visibleColumns.findIndex((c) => c.field === field);\n if (colIndex === -1) return;\n\n const column = internalGrid._visibleColumns[colIndex];\n const rowData = internalGrid._rows[rowIndex];\n if (!column || !rowData || !this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return;\n\n const cellEl = this.#getCell(rowIndex, colIndex);\n if (!cellEl) return;\n\n this.#singleCellEdit = true;\n this.#beginCellEdit(rowIndex, colIndex, cellEl);\n }\n\n /**\n * Programmatically begin editing all editable cells in a row.\n * @param rowIndex - Index of the row to edit\n * @fires cell-commit - Emitted for each cell value that is committed\n * @fires row-commit - Emitted when focus leaves the row\n */\n beginBulkEdit(rowIndex: number): void {\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n if (editOn === false) return;\n\n const hasEditableColumn = internalGrid._columns?.some((col) => this.#hasEditableConfig(col as ColumnConfig<T>));\n if (!hasEditableColumn) return;\n\n const rowEl = internalGrid.findRenderedRowElement?.(rowIndex);\n if (!rowEl) return;\n\n // Row-level gate\n const rowData = internalGrid._rows[rowIndex];\n if (!this.#isRowEditable(rowData as T | undefined)) return;\n\n // Bulk edit clears single-cell mode\n this.#singleCellEdit = false;\n\n // Start row edit\n this.#startRowEdit(rowIndex, rowData);\n\n // Enter edit mode on all editable cells\n Array.from(rowEl.children).forEach((cell, i) => {\n const col = internalGrid._visibleColumns[i];\n if (col && this.#isCellEditable(col as ColumnConfig<T>, rowData as T)) {\n const cellEl = cell as HTMLElement;\n if (!cellEl.classList.contains('editing')) {\n this.#injectEditor(rowData, rowIndex, col, i, cellEl, true);\n }\n }\n });\n\n // Focus the first editable cell\n setTimeout(() => {\n let targetCell = rowEl.querySelector(`.cell[data-col=\"${internalGrid._focusCol}\"]`);\n if (!targetCell?.classList.contains('editing')) {\n targetCell = rowEl.querySelector('.cell.editing');\n }\n if (targetCell?.classList.contains('editing')) {\n const editor = (targetCell as HTMLElement).querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n try {\n editor?.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n }\n }, 0);\n }\n\n /**\n * Commit the currently active row edit.\n * @fires row-commit - Emitted after the row edit is committed\n */\n commitActiveRowEdit(): void {\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, false);\n }\n }\n\n /**\n * Cancel the currently active row edit.\n */\n cancelActiveRowEdit(): void {\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, true);\n }\n }\n\n // #endregion\n\n // #region Internal Methods\n\n // --- Small private helpers (centralised to keep call sites uniform) ---\n\n /**\n * Safely resolve a row's ID via the configured `getRowId`. Returns\n * `undefined` when no ID can be derived (no `getRowId` callback, or it\n * threw — e.g. row has no key).\n */\n #safeGetRowId(row: T | undefined | null): string | undefined {\n if (!row) return undefined;\n try {\n return this.#internalGrid.getRowId?.(row);\n } catch {\n return undefined;\n }\n }\n\n /**\n * Resolve a rendered cell element by row + column index. Returns `null`\n * when the row is not currently rendered or the cell is missing.\n */\n #getCell(rowIndex: number, colIndex: number): HTMLElement | null {\n const rowEl = this.#internalGrid.findRenderedRowElement?.(rowIndex);\n return (rowEl?.querySelector(`.cell[data-col=\"${colIndex}\"]`) as HTMLElement | null) ?? null;\n }\n\n /**\n * Row-level editability gate. Returns `false` only when\n * `gridConfig.rowEditable` is configured AND vetoes the row. Rows without\n * data return `true` (defer to the column-level check).\n */\n #isRowEditable(rowData: T | undefined): boolean {\n if (!rowData) return true;\n const rowEditable = this.#internalGrid.effectiveConfig?.rowEditable;\n if (!rowEditable) return true;\n return rowEditable(rowData as any) !== false;\n }\n\n /**\n * Emit a `dirty-change` event with the canonical `{ rowId, row, original, type }`\n * shape. Centralises baseline lookup so every emit site uses the same\n * source of truth.\n */\n #emitDirtyChange(rowId: string, row: T | undefined, type: DirtyChangeDetail<T>['type']): void {\n this.emit<DirtyChangeDetail<T>>('dirty-change', {\n rowId,\n row: row as T,\n original: this.#dirty.getOriginalRow(rowId),\n type,\n });\n }\n\n /**\n * Grid-mode \"Escape → navigation mode\" transition: revert the cell from\n * the focus snapshot, blur the active editor, return focus to the grid,\n * and flip the navigation/lock flags. Used by the capture-phase Escape\n * handler (both deferred and sync branches) and the bubble-phase Escape\n * handler in `onKeyDown`.\n */\n #enterGridModeNavigation(): void {\n this.#revertGridModeCellEdit();\n const activeEl = document.activeElement as HTMLElement | null;\n if (activeEl && this.gridElement.contains(activeEl)) {\n activeEl.blur();\n this.gridElement.focus();\n }\n this.#gridModeInputFocused = false;\n this.#gridModeEditLocked = true;\n }\n\n /**\n * Restore a row to its snapshot values and clear all per-row dirty\n * bookkeeping. Called both from the explicit-revert path and from the\n * `row-commit` cancellation path in `#exitRowEdit`.\n */\n #revertRowFromSnapshot(rowId: string | undefined, current: T, snapshot: T | undefined): void {\n if (!snapshot) return;\n Object.keys(snapshot as object).forEach((k) => {\n (current as Record<string, unknown>)[k] = (snapshot as Record<string, unknown>)[k];\n });\n if (rowId) {\n this.#dirty.changedRowIds.delete(rowId);\n this.#dirty.committedDirtyRowIds.delete(rowId);\n this.clearRowInvalid(rowId);\n }\n this.emitPluginEvent('cell-edit-reverted', { rowId });\n }\n\n /**\n * Sync the data-invalid attribute on a cell element.\n * Used as the DOM callback for CellValidationManager.\n */\n #syncInvalidCellAttribute(rowId: string, field: string, invalid: boolean): void {\n const internalGrid = this.#internalGrid;\n const colIndex = internalGrid._visibleColumns?.findIndex((c) => c.field === field);\n if (colIndex === -1 || colIndex === undefined) return;\n\n // Find the row element by rowId\n const rows = internalGrid._rows;\n const rowIndex = rows?.findIndex((r) => this.#safeGetRowId(r as T) === rowId);\n if (rowIndex === -1 || rowIndex === undefined) return;\n\n const cellEl = this.#getCell(rowIndex, colIndex);\n if (!cellEl) return;\n\n if (invalid) {\n cellEl.setAttribute('data-invalid', 'true');\n // `data-invalid` is styling only; `aria-invalid` is what actually reaches a\n // screen reader (SC 3.3.1 Error Identification).\n cellEl.setAttribute('aria-invalid', 'true');\n const message = this.#validation.getInvalidMessage(rowId, field);\n if (message) {\n cellEl.setAttribute('title', message);\n // Hand the title over: core would otherwise reclaim it as its own\n // truncation tooltip on the next hover and overwrite the message.\n cellEl.removeAttribute(GridDataAttrs.TRUNCATED);\n }\n } else {\n cellEl.removeAttribute('data-invalid');\n cellEl.removeAttribute('aria-invalid');\n cellEl.removeAttribute('title');\n }\n }\n\n /**\n * Migrate all index-keyed editing state when the active edit row moves to\n * a different position in `_rows` (e.g. after sort, filter, or new data push).\n *\n * Updates: `#activeEditRow`, `#editingCells`, `#rowEditSnapshots`,\n * `#editorValueCallbacks`, and syncs `_activeEditRows` on the grid.\n */\n #migrateEditRowIndex(oldIndex: number, newIndex: number): void {\n this.#activeEditRow = newIndex;\n\n // Migrate #editingCells keys (\"rowIndex:colIndex\")\n const migratedCells = new Set<string>();\n const prefix = `${oldIndex}:`;\n for (const cellKey of this.#editingCells) {\n if (cellKey.startsWith(prefix)) {\n migratedCells.add(`${newIndex}:${cellKey.substring(prefix.length)}`);\n } else {\n migratedCells.add(cellKey);\n }\n }\n this.#editingCells.clear();\n for (const key of migratedCells) {\n this.#editingCells.add(key);\n }\n\n // Migrate #rowEditSnapshots key\n const snapshot = this.#rowEditSnapshots.get(oldIndex);\n if (snapshot !== undefined) {\n this.#rowEditSnapshots.delete(oldIndex);\n this.#rowEditSnapshots.set(newIndex, snapshot);\n }\n\n // Migrate #editorValueCallbacks keys (\"rowIndex:field\")\n const updates: [string, (newValue: unknown) => void][] = [];\n for (const [key, cb] of this.#editorValueCallbacks) {\n if (key.startsWith(prefix)) {\n updates.push([`${newIndex}:${key.substring(prefix.length)}`, cb]);\n this.#editorValueCallbacks.delete(key);\n }\n }\n for (const [key, cb] of updates) {\n this.#editorValueCallbacks.set(key, cb);\n }\n\n // Sync the grid's rendering state so rows.ts checks the correct index\n this.#syncGridEditState();\n }\n\n /**\n * Begin editing a single cell.\n */\n #beginCellEdit(rowIndex: number, colIndex: number, cellEl: HTMLElement): void {\n const internalGrid = this.#internalGrid;\n const rowData = internalGrid._rows[rowIndex];\n const column = internalGrid._visibleColumns[colIndex];\n\n if (!rowData || !column || !this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return;\n if (cellEl.classList.contains('editing')) return;\n\n // Start row edit if not already\n if (this.#activeEditRow !== rowIndex) {\n this.#startRowEdit(rowIndex, rowData);\n }\n\n this.#activeEditCol = colIndex;\n this.#injectEditor(rowData, rowIndex, column, colIndex, cellEl, false);\n }\n\n /**\n * Revert the focused cell's value from the snapshot taken when the editor\n * first received focus, then emit a `cell-cancel` event so framework\n * adapters (e.g., GridFormArray) can revert their FormControls.\n *\n * Called by the grid-mode Escape handlers (both capture and bubble phase).\n */\n #revertGridModeCellEdit(): void {\n const snapshot = this.#gridModeCellSnapshot;\n if (!snapshot) return;\n\n const internalGrid = this.#internalGrid;\n const rowData = internalGrid._rows?.[snapshot.rowIndex];\n if (rowData) {\n writeCellField(rowData, snapshot.field, snapshot.value);\n this.emitPluginEvent('cell-edit-reverted', { rowId: this.#safeGetRowId(rowData as T) });\n }\n\n // Push the reverted value to the editor's input element so that the\n // subsequent blur-commit (triggered by activeEl.blur()) reads the\n // reverted value instead of the user's typed text. This makes the\n // blur-commit a no-op (oldValue === newValue) and prevents it from\n // overwriting the revert.\n const callbackKey = `${snapshot.rowIndex}:${snapshot.field}`;\n const cb = this.#editorValueCallbacks.get(callbackKey);\n if (cb) cb(snapshot.value);\n\n // Notify framework adapters to revert FormControls\n this.emit<CellCancelDetail>('cell-cancel', {\n rowIndex: snapshot.rowIndex,\n colIndex: snapshot.colIndex,\n field: snapshot.field,\n previousValue: snapshot.value,\n });\n\n // After reverting, clean up changedRowIds so the built-in `.changed` class\n // toggle in rows.ts doesn't fight with consumer's rowClass callback.\n // changedRowIds.add() is unconditional during commit, so cleanup must also\n // be unconditional — otherwise the Set retains stale IDs when dirtyTracking\n // is disabled and the built-in toggle re-adds `.changed` on every render.\n if (rowData) {\n const rowId = this.#safeGetRowId(rowData as T);\n if (rowId) {\n if (this.config.dirtyTracking) {\n // With dirty tracking, only remove if the row has no other dirty cells\n if (!this.#dirty.isRowDirty(rowId, rowData as T)) {\n this.#dirty.changedRowIds.delete(rowId);\n this.#emitDirtyChange(rowId, rowData as T, 'reverted');\n }\n } else {\n // Without dirty tracking there's no baseline to compare, so always\n // remove — the consumer manages their own change state via rowClass.\n this.#dirty.changedRowIds.delete(rowId);\n }\n }\n }\n\n // Always re-render after revert so rowClass callbacks re-evaluate\n // (e.g., consumer may toggle a 'changed' class based on local state\n // updated in response to the cell-cancel event above).\n this.requestRender();\n\n this.#gridModeCellSnapshot = null;\n }\n\n /**\n * Focus the editor input in the currently focused cell (grid mode only).\n * Used when pressing Enter to enter edit mode from navigation mode.\n */\n #focusCurrentCellEditor(): void {\n const internalGrid = this.#internalGrid;\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n\n if (focusRow < 0 || focusCol < 0) return;\n\n const cellEl = this.#getCell(focusRow, focusCol);\n\n if (cellEl?.classList.contains('editing')) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (editor) {\n this.#gridModeEditLocked = false; // Unlock edit mode - user pressed Enter\n editor.focus();\n this.#gridModeInputFocused = true;\n // Select all text in text inputs for quick replacement\n if (editor instanceof HTMLInputElement && (editor.type === 'text' || editor.type === 'number')) {\n editor.select();\n }\n }\n }\n }\n\n /**\n * Handle Tab/Shift+Tab navigation while editing.\n * Moves to next/previous editable cell, staying in edit mode.\n * Wraps to next/previous row when reaching row boundaries.\n */\n #handleTabNavigation(forward: boolean): void {\n const internalGrid = this.#internalGrid;\n const rows = internalGrid._rows;\n // In grid mode, use focusRow since there's no active edit row\n const currentRow = this.#isGridMode ? internalGrid._focusRow : this.#activeEditRow;\n const currentRowData = rows[currentRow] as T;\n\n // Get editable column indices for the CURRENT row\n const editableCols = internalGrid._visibleColumns\n .map((c, i) => (currentRowData && this.#isCellEditable(c as ColumnConfig<T>, currentRowData) ? i : -1))\n .filter((i) => i >= 0);\n if (editableCols.length === 0) return;\n\n const currentIdx = editableCols.indexOf(internalGrid._focusCol);\n const nextIdx = currentIdx + (forward ? 1 : -1);\n\n // Can move within same row?\n if (nextIdx >= 0 && nextIdx < editableCols.length) {\n internalGrid._focusCol = editableCols[nextIdx];\n const cellEl = this.#getCell(currentRow, editableCols[nextIdx]);\n if (cellEl?.classList.contains('editing')) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n ensureCellVisible(internalGrid, { forceHorizontalScroll: true });\n return;\n }\n\n // Can move to adjacent row?\n const nextRow = currentRow + (forward ? 1 : -1);\n if (nextRow >= 0 && nextRow < rows.length) {\n const nextRowData = rows[nextRow] as T;\n // Compute editable columns for the next row\n const nextEditableCols = internalGrid._visibleColumns\n .map((c, i) => (nextRowData && this.#isCellEditable(c as ColumnConfig<T>, nextRowData) ? i : -1))\n .filter((i) => i >= 0);\n if (nextEditableCols.length === 0) return; // Next row has no editable cells\n\n // In grid mode, just move focus (all rows are always editable)\n if (this.#isGridMode) {\n internalGrid._focusRow = nextRow;\n internalGrid._focusCol = forward ? nextEditableCols[0] : nextEditableCols[nextEditableCols.length - 1];\n ensureCellVisible(internalGrid, { forceHorizontalScroll: true });\n // Focus the editor in the new cell after render\n this.requestAfterRender();\n setTimeout(() => {\n const cellEl = this.#getCell(nextRow, internalGrid._focusCol);\n if (cellEl?.classList.contains('editing')) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n }, 0);\n } else {\n // In row mode, commit current row and enter next row\n this.#exitRowEdit(currentRow, false);\n internalGrid._focusRow = nextRow;\n internalGrid._focusCol = forward ? nextEditableCols[0] : nextEditableCols[nextEditableCols.length - 1];\n this.beginBulkEdit(nextRow);\n ensureCellVisible(internalGrid, { forceHorizontalScroll: true });\n }\n }\n // else: at boundary - stay put\n }\n\n /**\n * Sync the internal grid state with the plugin's editing state.\n */\n #syncGridEditState(): void {\n const internalGrid = this.#internalGrid;\n internalGrid._activeEditRows = this.#activeEditRow;\n internalGrid._rowEditSnapshots = this.#rowEditSnapshots;\n }\n\n /**\n * Snapshot original row data and mark as editing.\n */\n #startRowEdit(rowIndex: number, rowData: T): void {\n if (this.#activeEditRow !== rowIndex) {\n // Commit the previous row before starting a new one\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, false);\n }\n this.#rowEditSnapshots.set(rowIndex, { ...rowData });\n this.#activeEditRow = rowIndex;\n this.#activeEditRowRef = rowData;\n\n // Store stable row ID for resilience against _rows replacement during editing\n this.#activeEditRowId = this.#safeGetRowId(rowData);\n\n this.#syncGridEditState();\n\n // Broadcast edit-open so consumers (DOM listeners) AND other plugins\n // (notably SelectionPlugin via the plugin event bus) stay in sync with\n // the row currently being edited. Issue #284.\n // Row mode only — in grid mode every row is perpetually editable.\n if (!this.#isGridMode) {\n this.broadcast<EditOpenDetail<T>>('edit-open', {\n rowIndex,\n rowId: this.#activeEditRowId ?? '',\n row: rowData,\n });\n announce(this.gridElement, getA11yMessage(this.gridElement, 'editingStarted', rowIndex));\n }\n }\n }\n\n /**\n * Exit editing for a row.\n */\n #exitRowEdit(rowIndex: number, revert: boolean): void {\n if (this.#activeEditRow !== rowIndex) return;\n\n const internalGrid = this.#internalGrid;\n const snapshot = this.#rowEditSnapshots.get(rowIndex);\n const rowEl = internalGrid.findRenderedRowElement?.(rowIndex);\n const { rowId, current } = this.#resolveEditedRow(rowIndex);\n\n // Collect and commit values from active editors before re-rendering\n if (!revert && rowEl && current) {\n this.#commitActiveEditors(rowEl, rowIndex, current);\n }\n\n // Flush managed editors (framework adapters) before clearing state.\n // At this point the commit() callback is still active, so editors can\n // synchronously commit their pending values in response to this event.\n if (!revert && !this.#isGridMode && current) {\n this.emit<BeforeEditCloseDetail<T>>('before-edit-close', {\n rowIndex,\n rowId: rowId ?? '',\n row: current,\n });\n }\n\n // Revert if requested\n if (revert && current) {\n this.#revertRowFromSnapshot(rowId, current, snapshot);\n } else if (!revert && current) {\n this.#finalizeRowCommit(rowIndex, rowId, current, snapshot);\n }\n\n this.#clearRowEditState(rowIndex);\n\n // Mark that focus should be restored after the upcoming render completes.\n // This must be set BEFORE refreshVirtualWindow because it calls afterRender()\n // synchronously, which reads this flag.\n this.#pendingFocusRestore = true;\n\n // Re-render the row to remove editors\n if (rowEl) {\n this.#teardownRowEditors(rowEl, internalGrid);\n } else {\n // Row not visible - restore focus immediately (no render will happen)\n this.#restoreCellFocus(internalGrid);\n this.#pendingFocusRestore = false;\n }\n\n // Broadcast edit-close (row mode only, fires for both commit and revert)\n // so consumers AND other plugins receive it. Issue #284.\n if (!this.#isGridMode && current) {\n this.broadcast<EditCloseDetail<T>>('edit-close', {\n rowIndex,\n rowId: rowId ?? '',\n row: current,\n reverted: revert,\n });\n if (!revert) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'editingCommitted', rowIndex));\n }\n }\n }\n\n /**\n * Resolve the row being edited using the stored row ID.\n *\n * The `_rows` array may have been replaced (e.g. Angular pushing new rows via\n * directive effect) since editing started, so `_rows[rowIndex]` could point to\n * a completely different row — the ID map is always up to date. Without an ID\n * we fall back to the row reference stored at edit-open (`#activeEditRowRef`),\n * which is safer than the possibly-stale `_rows[rowIndex]`.\n */\n #resolveEditedRow(rowIndex: number): { rowId: string | undefined; current: T | undefined } {\n const internalGrid = this.#internalGrid;\n let rowId = this.#activeEditRowId;\n const entry = rowId ? internalGrid._getRowEntry(rowId) : undefined;\n const current = entry?.row ?? this.#activeEditRowRef ?? internalGrid._rows[rowIndex];\n\n if (!rowId && current) {\n rowId = this.#safeGetRowId(current);\n }\n return { rowId, current };\n }\n\n /**\n * Read pending values out of every native editor in the row and commit the\n * ones that actually changed.\n */\n #commitActiveEditors(rowEl: HTMLElement, rowIndex: number, current: T): void {\n const internalGrid = this.#internalGrid;\n rowEl.querySelectorAll('.cell.editing').forEach((cell) => {\n const colIndex = Number((cell as HTMLElement).getAttribute('data-col'));\n if (isNaN(colIndex)) return;\n const col = internalGrid._visibleColumns[colIndex];\n if (!col) return;\n\n // Skip cells with externally-managed editors (framework adapters like Angular/React/Vue).\n // These editors handle their own commits via the commit() callback - we should NOT\n // try to read values from their DOM inputs (which may contain formatted display values).\n if ((cell as HTMLElement).hasAttribute('data-editor-managed')) return;\n\n const input = cell.querySelector('input,textarea,select') as\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null;\n if (!input) return;\n\n const field = col.field;\n const originalValue = readCellField(current, field) as T[keyof T];\n const val = getInputValue(input, col, originalValue);\n if (originalValue !== val) {\n this.#commitCellValue(rowIndex, col, val, current);\n }\n });\n }\n\n /**\n * Emit the cancelable `row-commit` event and apply its outcome — revert on\n * `preventDefault()`, otherwise refresh committed-dirty tracking and queue the\n * change animation.\n */\n #finalizeRowCommit(rowIndex: number, rowId: string | undefined, current: T, snapshot: T | undefined): void {\n // Compare snapshot vs current to detect if changes were made during THIS edit session\n const changedThisSession = hasRowChanged(snapshot, current);\n\n // Check if this row has any cumulative changes (via ID tracking)\n // Fall back to session-based detection when no row ID is available\n const changed = rowId ? this.#dirty.changedRowIds.has(rowId) : changedThisSession;\n\n const cancelled = this.emitCancelable<RowCommitDetail<T>>('row-commit', {\n rowIndex,\n rowId: rowId ?? '',\n row: current,\n oldValue: snapshot,\n newValue: current,\n changed,\n changedRows: this.changedRows,\n changedRowIds: this.changedRowIds,\n });\n\n // If consumer called preventDefault(), revert the row\n if (cancelled) {\n if (snapshot) this.#revertRowFromSnapshot(rowId, current, snapshot);\n return;\n }\n\n // Mark row as committed-dirty if it has actual changes vs baseline\n if (rowId && this.config.dirtyTracking) {\n if (this.#dirty.isRowDirty(rowId, current)) {\n this.#dirty.committedDirtyRowIds.add(rowId);\n } else {\n this.#dirty.committedDirtyRowIds.delete(rowId);\n }\n }\n\n if (changedThisSession && this.isAnimationEnabled) {\n // Animate the row only if changes were made during this edit session\n // (deferred to afterRender so the row element exists after re-render)\n this.#pendingRowAnimation = rowIndex;\n }\n }\n\n /**\n * Clear all per-row editing bookkeeping.\n *\n * The `#editingCells` / `#editorValueCallbacks` keys use the rowIndex captured\n * at edit-open time. Even if `_rows` was replaced and the row moved to a\n * different index, the keys still match what was inserted during this session.\n */\n #clearRowEditState(rowIndex: number): void {\n this.#rowEditSnapshots.delete(rowIndex);\n this.#activeEditRow = -1;\n this.#activeEditRowId = undefined;\n this.#activeEditRowRef = undefined;\n this.#activeEditCol = -1;\n this.#singleCellEdit = false;\n this.#syncGridEditState();\n\n for (const cellKey of this.#editingCells) {\n if (cellKey.startsWith(`${rowIndex}:`)) {\n this.#editingCells.delete(cellKey);\n }\n }\n for (const callbackKey of this.#editorValueCallbacks.keys()) {\n if (callbackKey.startsWith(`${rowIndex}:`)) {\n this.#editorValueCallbacks.delete(callbackKey);\n }\n }\n }\n\n /**\n * Release framework editor components, drop the editing state from the cells\n * and re-render the row so the display values come back.\n */\n #teardownRowEditors(rowEl: HTMLElement, internalGrid: InternalGrid<T>): void {\n // Release framework editor components (Angular/React/Vue) BEFORE clearing\n // editing state. This ensures releaseCell runs while the editor DOM is\n // still inside each cell, so the adapter can find and destroy ComponentRefs.\n // Without this, overlay editors (BaseOverlayEditor) leak panels on <body>\n // with active MutationObservers that react to cell-focus class changes.\n // An active cell loader is parked outside the cell meanwhile, as in\n // editor injection: React's releaseCell would otherwise unmount it, and the\n // loading replay never refills an overlay that is still present.\n const adapter = internalGrid.__frameworkAdapter;\n const editingCells = rowEl.querySelectorAll('.cell.editing');\n if (adapter?.releaseCell) {\n editingCells.forEach((cell) => {\n const loader = parkCellLoadingOverlay(cell as HTMLElement);\n adapter.releaseCell!(cell as HTMLElement);\n if (loader) cell.appendChild(loader);\n });\n }\n\n // Remove editing class and re-render cells\n editingCells.forEach((cell) => {\n cell.classList.remove('editing');\n clearEditingState(cell.parentElement as RowElementInternal);\n });\n\n // Refresh the virtual window to restore cell content WITHOUT rebuilding\n // the row model. requestRender() would trigger processRows (ROWS phase)\n // which re-sorts — causing the edited row to jump to a new position and\n // disappear from view. refreshVirtualWindow re-renders visible cells from\n // the current _rows order, keeping the row in place until the user\n // explicitly sorts again or new data arrives.\n internalGrid.refreshVirtualWindow(true);\n }\n\n /**\n * Commit a single cell value change.\n * Uses ID-based change tracking for stability when rows are reordered.\n */\n #commitCellValue(\n rowIndex: number,\n column: ColumnConfig<T>,\n newValue: unknown,\n rowData: T,\n source: UpdateSource = 'user',\n ): void {\n const field = column.field;\n if (!isSafePropertyKey(field)) return;\n const oldValue = readCellField(rowData, field);\n if (oldValue === newValue) return;\n\n const internalGrid = this.#internalGrid;\n\n // Get row ID for change tracking (may not exist if getRowId not configured)\n const rowId = this.#safeGetRowId(rowData);\n\n const firstTime = rowId ? !this.#dirty.changedRowIds.has(rowId) : true;\n\n // Create updateRow helper for cascade updates (noop if row has no ID)\n const updateRow: (changes: Partial<T>) => void = rowId\n ? (changes) => this.grid.updateRow(rowId!, changes as Record<string, unknown>, 'cascade')\n : noopUpdateRow;\n\n // Track whether setInvalid was called during event handling\n let invalidWasSet = false;\n\n // Create setInvalid callback for validation (noop if row has no ID)\n const setInvalid = rowId\n ? (message?: string) => {\n invalidWasSet = true;\n this.setInvalid(rowId!, field, message ?? '');\n }\n : () => {}; // eslint-disable-line @typescript-eslint/no-empty-function\n\n // Emit cancelable event BEFORE applying the value.\n //\n // Re-entrancy guard: a `cell-commit` listener commonly cascades by calling\n // `detail.updateRow()` / `grid.updateRow()` (which route back through this\n // commit pipeline) or forces a row re-render the same way. If that nested\n // update targets the SAME cell that is still mid-commit, re-emitting\n // `cell-commit` would recurse until the call stack overflows\n // (`RangeError: Maximum call stack size exceeded`).\n //\n // When we detect a re-entrant commit of a cell already in flight we RETURN\n // EARLY rather than re-applying: the OUTER commit still owns this cell and\n // will apply the value + emit `cell-edit-committed` exactly once when its\n // own emit returns. Re-applying here would instead (a) fire a duplicate\n // `cell-edit-committed` → a phantom UndoRedo history entry, and (b) sneak\n // the value in even when the outer listener called `preventDefault()`,\n // bypassing the cancelable-commit contract. Cascades to OTHER cells still\n // emit normally, and the set is keyed per (row, field) so distinct fields\n // can never form an unbounded chain.\n const commitKey = `${rowId ?? rowIndex}\\u0000${field}`;\n if (this.#committingCells.has(commitKey)) return;\n\n let cancelled = false;\n this.#committingCells.add(commitKey);\n try {\n cancelled = this.emitCancelable<CellCommitDetail<T>>('cell-commit', {\n row: rowData,\n rowId: rowId ?? '',\n field,\n oldValue,\n value: newValue,\n source,\n rowIndex,\n changedRows: this.changedRows,\n changedRowIds: this.changedRowIds,\n firstTimeForRow: firstTime,\n updateRow,\n setInvalid,\n });\n } finally {\n this.#committingCells.delete(commitKey);\n }\n\n // If consumer called preventDefault(), abort the commit\n if (cancelled) return;\n\n // Clear any previous invalid state for this cell ONLY if setInvalid wasn't called\n // (if setInvalid was called, the handler wants it to remain invalid)\n if (rowId && !invalidWasSet && this.isCellInvalid(rowId, field)) {\n this.clearInvalid(rowId, field);\n }\n\n // Apply the value and mark row as changed.\n // Whole-row invalidation (not per-`field`): `field` here is this column's\n // own key, but OTHER columns' `valueAccessor`s may derive from it while\n // being cached under their own `column.field`. Per-field invalidation would\n // refresh only this column and leave those dependent columns stale.\n writeCellField(rowData, field, newValue);\n invalidateAccessorCache(rowData as object);\n if (rowId) {\n this.#dirty.changedRowIds.add(rowId);\n }\n this.#syncGridEditState();\n\n // Emit dirty-change event if dirty tracking is enabled\n if (this.config.dirtyTracking && rowId) {\n const dirty = this.#dirty.isRowDirty(rowId, rowData);\n this.#emitDirtyChange(rowId, rowData, dirty ? 'modified' : 'pristine');\n }\n\n // Notify other plugins (e.g., UndoRedoPlugin) about the committed edit\n this.emitPluginEvent('cell-edit-committed', {\n rowIndex,\n rowId,\n field,\n oldValue,\n newValue,\n });\n\n // Mark the row visually as changed (animation happens when row edit closes)\n const rowEl = internalGrid.findRenderedRowElement?.(rowIndex);\n if (rowEl) {\n rowEl.classList.add('changed');\n }\n }\n\n /**\n * Inject an editor into a cell.\n * Delegates to the extracted `editor-injection` module.\n */\n #injectEditor(\n rowData: T,\n rowIndex: number,\n column: ColumnConfig<T>,\n colIndex: number,\n cell: HTMLElement,\n skipFocus: boolean,\n parentRowEl?: HTMLElement,\n ): void {\n injectEditorImpl(this.#editorDeps, rowData, rowIndex, column, colIndex, cell, skipFocus, parentRowEl);\n }\n\n /**\n * Restore focus to cell after exiting edit mode.\n */\n #restoreCellFocus(internalGrid: InternalGrid<T>): void {\n queueMicrotask(() => {\n try {\n const rowIdx = internalGrid._focusRow;\n const colIdx = internalGrid._focusCol;\n const rowEl = internalGrid.findRenderedRowElement?.(rowIdx);\n if (rowEl) {\n Array.from(internalGrid._bodyEl.querySelectorAll('.cell-focus')).forEach((el) =>\n el.classList.remove('cell-focus'),\n );\n const cell = rowEl.querySelector(`.cell[data-row=\"${rowIdx}\"][data-col=\"${colIdx}\"]`) as HTMLElement | null;\n if (cell) {\n cell.classList.add('cell-focus');\n cell.setAttribute('aria-selected', 'true');\n if (!cell.hasAttribute('tabindex')) cell.setAttribute('tabindex', '-1');\n cell.focus({ preventScroll: true });\n }\n }\n } catch {\n /* empty */\n }\n });\n }\n\n // #endregion\n}\n"],"mappings":"oMAoBA,IAgBa,EAsBF,UAmDE,EAWA,qBCwFb,SAAgB,EAAS,EAAqB,GAG5C,IAAK,EAAQ,OAKb,IAAoC,KAHlC,oBAAqB,EAChB,EAA0D,qBAC3D,IACM,MAAM,cAAyB,OAE3C,MAAM,EAAK,EAAO,gBAAgB,gBAC7B,IAEL,EAAG,YAAc,GAIoB,mBAA1B,sBACT,sBAAA,KACE,EAAG,YAAc,IAGnB,EAAG,YAAc,EAErB,CAUA,SAAgB,EACd,EACA,KACG,GAOH,MAAM,GAHJ,GAAU,oBAAqB,EAC1B,EAA0D,qBAC3D,IACmB,MAAM,WAAW,GAC1C,OAAI,EAAiB,KAAY,GACzB,EAAsB,MAAsB,EACtD,CEmJA,SAAgB,EAAkB,EAAgB,IAlJlD,SAAkC,GAChC,IAAK,EAAK,iBAAiB,QAAS,OACpC,MAAM,UAAE,EAAA,UAAW,EAAA,WAAW,EAAA,cAAY,GAAkB,EAAK,gBAG3D,EAAW,EACX,EAAgB,GAAY,cAAgB,GAAU,cAAgB,EAC5E,IAAK,GAAY,GAAiB,EAAG,OAErC,MAAM,EAAU,EAAK,4BAA4B,EAAK,YAAc,CAAE,IAAK,EAAG,OAAQ,GACtF,GAAI,EAAQ,WAAY,OAExB,MAAM,EAAM,KAAK,IAAI,EAAG,EAAQ,KAE1B,EAAe,EAAgB,EADtB,KAAK,IAAI,EAAG,EAAQ,QAEnC,GAAI,GAAgB,EAAG,OAEvB,MAAM,EAAI,EAAK,UAAY,EACrB,EAAmB,EAAgB,EAAmB,EAAS,UAAW,GAAiB,EAAS,UAC1G,GAAI,EAAI,EAAmB,EAAK,CAC9B,MAAM,EAAS,KAAK,IAAI,EAAG,EAAI,GAC/B,EAAS,UAAY,EAAgB,EAAqB,EAAQ,GAAiB,CACrF,MAAO,GAAI,EAAI,EAAY,EAAmB,EAAM,EAAc,CAChE,MAAM,EAAS,KAAK,IAAI,EAAG,EAAI,EAAe,EAAY,GAC1D,EAAS,UAAY,EAAgB,EAAqB,EAAQ,GAAiB,CACrF,CACF,CAyHE,CAAyB,GAEzB,MAAM,EA7XR,SAAsB,GACpB,YAAgC,IAAzB,EAAK,kBAA0D,IAAzB,EAAK,eACpD,CA2XoB,CAAa,MAAW,EAAK,gBD3TjD,IAA+B,EC6TxB,GAAW,EAAK,sBAAqB,ID7Tb,EC+Td,EAAK,UD7TpB,EAAK,iBAAiB,eAAe,QAAS,GAAO,EAAG,UAAU,OAAO,eC+TzE,MAAM,KAAK,EAAK,QAAQ,iBAAiB,2BAA2B,QAAS,IAC3E,EAAG,aAAa,gBAAiB,WAGnC,MAAM,EA/HR,SAAyB,GACvB,MAAM,EAAW,EAAK,UAChB,EAAS,EAAK,gBAAgB,OAAS,EACvC,EAAO,EAAK,gBAAgB,KAAO,EAAK,MAAM,OACpD,GAAI,EAAW,GAAU,GAAY,EAAM,OAE3C,MAAM,EAAQ,EAAK,QAAQ,iBAAiB,kBAAkB,EAAW,GAGzE,IAAI,EAAO,GAAO,SAAS,EAAK,WAKhC,OAJK,GAAS,EAAK,WAAW,SAAS,UACrC,EAAQ,GAAO,cAAc,mBAAmB,EAAK,gBAAkB,GAAO,cAAc,oBAGvF,EAAO,CAAE,MAAO,GAAS,KAAM,aAAS,CACjD,CAgHkB,CAAgB,GAChC,IAAK,EAAS,OACd,MAAM,MAAE,EAAA,KAAO,GAAS,EAExB,EAAK,UAAU,IAAI,cACnB,EAAK,aAAa,gBAAiB,QAI9B,IAAa,GAAS,uBAtH7B,SACE,EACA,EACA,EACA,GAEA,MAAM,EAAa,EAAK,cAAc,oBACtC,IAAK,EAAY,OAGjB,GAAI,GAAS,gBAEX,YADA,EAAW,WAAa,GAG1B,GAAI,GAAS,iBAEX,YADA,EAAW,WAAa,EAAW,YAAc,EAAW,aAO9D,MAAM,EAAU,EAAK,8BAA8B,QAAS,EAAW,IAAS,CAAE,KAAM,EAAG,MAAO,GAClG,GAAI,EAAQ,WAAY,OAExB,MAAM,EAAW,EAAK,wBAChB,EAAiB,EAAW,wBAC5B,EAAW,EAAS,KAAO,EAAe,KAAO,EAAW,WAC5D,EAAY,EAAW,EAAS,MAChC,EAAc,EAAW,WAAa,EAAQ,KAC9C,EAAe,EAAW,WAAa,EAAW,YAAc,EAAQ,MAC1E,EAAW,EACb,EAAW,WAAa,EAAW,EAAQ,KAClC,EAAY,IACrB,EAAW,WAAa,EAAY,EAAW,YAAc,EAAQ,MAEzE,CAmFI,CAA+B,EAAM,EAAO,EAAM,GAhFtD,SAA0B,EAAgB,EAAmB,GAC3D,GAAI,GAAa,EAAK,UAAU,SAAS,GAAsB,CAE7D,MAAM,EAAc,EAAK,cHvU3B,uGGwUE,GAAI,GAAe,SAAS,gBAAkB,EAC5C,IACE,EAAY,MAAM,CAAE,eAAe,GACrC,CAAA,MAEA,CAEF,MACF,CAEA,GAAI,EAAW,CAGb,GAAI,EAAK,SAAS,SAAS,eAAgB,OACtC,EAAK,aAAa,aAAa,EAAK,aAAa,WAAY,MAClE,IACE,EAAK,MAAM,CAAE,eAAe,GAC9B,CAAA,MAEA,CACA,MACF,CAeA,MAAM,EAAS,SAAS,cAEtB,aAAkB,aAClB,IAAW,GACc,mBAAlB,EAAK,UACZ,EAAK,SAAS,KACb,EAAO,UAAU,SAAS,UAC1B,EAAO,QAAQ,UACC,IAAW,GAC5B,EAAK,MAAM,CAAE,eAAe,GAEhC,CAgCE,CAAiB,EAAM,EAAM,EAC/B,CCvaA,IAAM,EAAU,qBACV,EAAiB,eACjB,EAAY,+BAKZ,EACJ,4SACG,MAAM,KACN,IAAK,GAAM,EAAE,MAAM,IAAI,UAAU,KAAK,KACrC,EAAY,IAAI,OAAO,uCAAuC,EAAG,MAAM,GAAG,KAAK,gBA0B9D,IAAI,IACzB,2KAA2K,MACzK,MAYc,IAAI,IAAI,kEAAkE,MAAM,MA+OlG,SAAS,EAAS,EAAc,GAC9B,MAAM,EApER,SAAkB,GAChB,MAAM,EAAkB,GACxB,IAAI,EAAI,EACR,KAAO,EAAI,EAAK,QAAQ,CACtB,MAAM,EAAK,EAAK,GAChB,GAAW,MAAP,EAAJ,CAMA,GAAK,GAAM,KAAO,GAAM,KAAgB,MAAP,GAAc,EAAI,EAAI,EAAK,QAAU,EAAK,EAAI,IAAM,KAAO,EAAK,EAAI,IAAM,IAAM,CAC/G,MAAM,EAAQ,EACd,KAAO,EAAI,EAAK,SAAY,EAAK,IAAM,KAAO,EAAK,IAAM,KAAoB,MAAZ,EAAK,KAAa,IACnF,EAAO,KAAK,CAAE,KAAM,MAAO,EAAG,OAAO,EAAK,MAAM,EAAO,MACvD,QACF,CAGA,GAAW,MAAP,EAAY,CACd,IAAI,EAAI,GAER,IADA,IACO,EAAI,EAAK,QAAsB,MAAZ,EAAK,IAAY,GAAK,EAAK,KACrD,GAAI,GAAK,EAAK,OAAQ,OAAO,KAC7B,IACA,EAAO,KAAK,CAAE,KAAM,MAAO,EAAG,IAC9B,QACF,CAGA,GAAK,GAAM,KAAO,GAAM,KAAS,GAAM,KAAO,GAAM,KAAe,MAAP,GAAqB,MAAP,EAAY,CACpF,MAAM,EAAQ,EACd,KAAO,EAAI,EAAK,QAAU,SAAS,KAAK,EAAK,KAAK,IAClD,EAAO,KAAK,CAAE,KAAM,KAAM,EAAG,EAAK,MAAM,EAAO,KAC/C,QACF,CAGA,GAAI,EAAI,EAAI,EAAK,OAAQ,CACvB,MAAM,EAAM,EAAK,MAAM,EAAG,EAAI,GAC9B,GAAY,QAAR,GAAyB,QAAR,EAAe,CAClC,EAAO,KAAK,CAAE,KAAM,KAAM,EAAG,IAC7B,GAAK,EACL,QACF,CACF,CACA,GAAI,EAAI,EAAI,EAAK,OAAQ,CACvB,MAAM,EAAK,EAAK,MAAM,EAAG,EAAI,GAC7B,GAAW,OAAP,GAAsB,OAAP,GAAsB,OAAP,GAAsB,OAAP,GAAsB,OAAP,GAAsB,OAAP,EAAa,CAC1F,EAAO,KAAK,CAAE,KAAM,KAAM,EAAG,IAC7B,GAAK,EACL,QACF,CACF,CAGA,IAAI,gBAAgB,SAAS,GAM7B,OAAO,KALL,EAAO,KAAK,CAAE,KAAM,KAAM,EAAG,IAC7B,GAlDF,MAFE,GAyDJ,CACA,OAAO,CACT,CAGe,CAAS,GACtB,IAAK,EAAM,OACX,MAAM,EAAkB,EACxB,IAAI,EAAM,EAEV,SAAS,IACP,OAAO,EAAO,EAChB,CACA,SAAS,IACP,OAAO,EAAO,IAChB,CACA,SAAS,EAAM,EAAc,GAC3B,MAAM,EAAI,IACV,QAAK,IACD,EAAE,OAAS,SACH,IAAR,GAAqB,EAAE,IAAM,GAEnC,CAkBA,SAAS,IACP,MAAM,EAYR,WACE,IAAI,EAAO,IACX,KAAO,EAAM,KAAM,OACjB,IACA,EAAO,GAAQ,IAEjB,OAAO,CACT,CAnBiB,GACf,GAAI,EAAM,KAAM,KAAM,CACpB,IACA,MAAM,EAAa,IACnB,IAAK,EAAM,KAAM,KAAM,OACvB,IACA,MAAM,EAAY,IAClB,OAAO,EAAS,EAAa,CAC/B,CACA,OAAO,CACT,CAWA,SAAS,IACP,IAAI,EAAO,IACX,KAAO,EAAM,KAAM,OACjB,IACA,EAAO,GAAQ,IAEjB,OAAO,CACT,CAEA,SAAS,IACP,IAAI,EAAO,IACX,OAAa,CACX,MAAM,EAAI,IACV,IAAK,GAAgB,OAAX,EAAE,KAAe,MAC3B,GAAY,QAAR,EAAE,EACJ,IACA,EAAO,IAAS,SACX,GAAY,QAAR,EAAE,EACX,IACA,EAAO,IAAS,SACX,GAAY,OAAR,EAAE,EACX,IACA,EAAO,GAAQ,QACV,IAAY,OAAR,EAAE,EAGN,MAFL,IACA,EAAO,GAAQ,GACV,CACT,CACA,OAAO,CACT,CAEA,SAAS,IACP,IAAI,EAAO,IACX,OAAa,CACX,MAAM,EAAI,IACV,IAAK,GAAgB,OAAX,EAAE,KAAe,MAC3B,GAAY,MAAR,EAAE,EACJ,IACA,EAAQ,EAAmB,SACtB,GAAY,MAAR,EAAE,EACX,IACA,EAAQ,EAAmB,SACtB,GAAY,OAAR,EAAE,EACX,IACA,EAAQ,GAAoB,QACvB,IAAY,OAAR,EAAE,EAGN,MAFL,IACA,EAAQ,GAAoB,GACvB,CACT,CACA,OAAO,CACT,CAEA,SAAS,IACP,IAAI,EAAO,IACX,OAAa,CACX,MAAM,EAAI,IACV,IAAK,GAAgB,OAAX,EAAE,KAAe,MAC3B,GAAY,MAAR,EAAE,EACJ,IACA,GAA2B,QACtB,IAAY,MAAR,EAAE,EAGN,MAFL,IACA,GAA2B,GACtB,CACT,CACA,OAAO,CACT,CAEA,SAAS,IACP,IAAI,EAAO,IACX,OAAa,CACX,MAAM,EAAI,IACV,IAAK,GAAgB,OAAX,EAAE,KAAe,MAC3B,GAAY,MAAR,EAAE,EACJ,IACA,GAA2B,SACtB,GAAY,MAAR,EAAE,EACX,IACA,GAA2B,QACtB,IAAY,MAAR,EAAE,EAGN,MAFL,IACA,GAA2B,GACtB,CACT,CACA,OAAO,CACT,CAEA,SAAS,IACP,OAAI,EAAM,KAAM,MACd,KACQ,KAEH,GACT,CAEA,SAAS,IACP,MAAM,EAAI,IACV,GAAK,EAAL,CACA,GAAe,QAAX,EAAE,KAEJ,OADA,IACO,EAAE,EAEX,GAAe,QAAX,EAAE,KAEJ,OADA,IACO,EAAE,EAEX,GAAe,OAAX,EAAE,KAEJ,OADA,IAlJJ,SAAmB,GACjB,GAAa,UAAT,EAAkB,OAAO,EAAI,MACjC,GAAI,EAAK,WAAW,QAAS,CAC3B,MAAM,EAAM,EAAK,MAAM,GACvB,OAAO,EAAI,IAAM,EAAI,IAAI,QAAO,CAClC,CACA,GAAI,EAAK,WAAW,gBAAiB,CACnC,MAAM,EAAM,EAAK,MAAM,IACvB,OAAO,EAAI,YAAc,EAAI,YAAY,QAAO,CAClD,CAEA,MAAa,QAAT,EAAuB,EAAI,IAClB,gBAAT,EAA+B,EAAI,iBAAvC,CAEF,CAqIW,CAAU,EAAE,GAErB,GAAe,OAAX,EAAE,MAAyB,MAAR,EAAE,EAAW,CAClC,IACA,MAAM,EAAS,IAEf,OADI,EAAM,KAAM,MAAM,IACf,CACT,CAEA,MAAe,OAAX,EAAE,MAAyB,MAAR,EAAE,GACvB,KACS,UAFX,CApBe,CAyBjB,CAEA,MAAM,EAAS,IAEf,OAAI,EAAM,EAAO,YAAjB,EACO,CACT,CAGA,SAAgB,EAAmB,EAAa,GAC9C,IAAK,IAA6B,IAAtB,EAAI,QAAQ,MAAc,OAAO,EAC7C,MAAM,EAA4C,GAS5C,GA4Ca,EApDD,EAAI,QAAQ,EAAA,CAAU,EAAI,KAC1C,MAAM,EAiBV,SAAoB,EAAc,GAEhC,KADA,GAAQ,GAAQ,IAAI,QACT,OAAO,EAClB,GAAI,EAAc,KAAK,GAAO,OAAO,EACrC,GAAa,UAAT,EAAkB,OAAoB,MAAb,EAAI,MAAgB,EAAiB,OAAO,EAAI,OAC7E,GAAI,EAAK,WAAW,UAAY,QAAQ,KAAK,KAAU,EAAK,SAAS,KAAM,CACzE,MAAM,EAAM,EAAK,MAAM,GACjB,EAAI,EAAI,IAAM,EAAI,IAAI,QAAO,EACnC,OAAY,MAAL,EAAY,EAAiB,OAAO,EAC7C,CACA,GAAI,EAAK,WAAW,kBAAoB,QAAQ,KAAK,KAAU,EAAK,SAAS,KAAM,CACjF,MAAM,EAAM,EAAK,MAAM,IACjB,EAAI,EAAI,YAAc,EAAI,YAAY,QAAO,EACnD,OAAY,MAAL,EAAY,EAAiB,OAAO,EAC7C,CACA,GAAI,EAAK,OAAS,GAAI,OAAO,EAC7B,IAAK,EAAU,KAAK,IAAS,EAAU,KAAK,GAAO,OAAO,EAC1D,MAAM,EAAW,EAAK,MAAM,OAC5B,GAAI,GAAY,EAAS,OAAS,EAAG,OAAO,EAC5C,IACE,MAAM,EAAM,EAAS,EAAM,GACrB,EAAa,MAAP,EAAc,GAAK,OAAO,GACtC,OAAI,EAAc,KAAK,GAAa,EAC7B,GAAO,CAChB,CAAA,MACE,OAAO,CACT,CACF,CA5CgB,CAAW,EAAM,GAK7B,OAJA,EAAM,KAAK,CAAE,KAAM,EAAK,OAAQ,OAAQ,IAIjC,IAAQ,EAAiB,GAldT,EAkd0B,IAjdtB,iBAAT,EACb,EACJ,QAAQ,KAAM,SACd,QAAQ,KAAM,QACd,QAAQ,KAAM,QACd,QAAQ,KAAM,UACd,QAAQ,KAAM,SAN6B,GADhD,IAA2B,IAigBpB,EACE,EAAE,QAAQ,IAAI,OAAO,EAAgB,KAAM,IAAI,QAAQ,kDAAmD,IADlG,GADjB,IAAqB,EAxCnB,MAAM,EAAW,EAAM,QAAU,EAAM,MAAO,GAAmB,KAAb,EAAE,QAAiB,EAAE,SAAW,GAEpF,OADqB,EAAc,KAAK,IACpB,EAAiB,GAC9B,CACT,CAkCA,IAAM,EAAgB,wBAqBtB,SAAgB,EAAgB,GAC9B,GAAI,EAAc,KAAK,GAAM,CAC3B,MAAM,EAAA,IAAY,GAElB,OADA,EAAG,WAAY,EACR,CACT,CACA,MAAM,EAAO,GAAqB,EAAmB,EAAK,GAE1D,OADA,EAAG,WAAY,EACR,CACT,CC3dA,SAAgB,EAAuB,GACrC,IAAK,EAAO,kBAAmB,OAAO,KACtC,MAAM,EAAU,EAAO,cAA2B,sCAClD,IAAK,EAAS,OAAO,KACrB,MAAM,EAAS,EAAO,cAGtB,OAFI,EAAQ,EAAO,aAAa,EAAS,GACpC,EAAQ,SACN,CACT,CEPA,SAAS,EAAiB,GACxB,OAAQ,IACN,MAAM,EAAS,EAAO,aAChB,EAAQ,SAAS,cAAc,SACrC,EAAM,KAAO,OAGT,EAAI,iBAAiB,KACvB,EAAM,YAAc,EAAI,MACM,iBAAd,EAAI,OAAsB,EAAI,QAE9C,EAAM,MAAQ,EAAI,MAAM,MAAM,KAAK,IAEjC,GAAQ,MAAK,EAAM,IAAM,EAAO,KAChC,GAAQ,MAAK,EAAM,IAAM,EAAO,KAChC,GAAQ,cAAa,EAAM,YAAc,EAAO,aAiCpD,OALA,EAAM,iBAAiB,SAzBjB,KA5EV,IAAmB,EA6Eb,GAAK,EAAM,MAgBc,iBAAd,EAAI,MAEb,EAAI,OAAO,EAAM,OAEjB,EAAI,OAAO,EAAM,kBAnBjB,GAAI,EAAO,SACT,EAAI,OAAO,UACN,CAEL,MAAM,EAAW,GAAQ,QACA,iBAAd,EAAI,OAA0C,iBAAb,EAE1C,EAAI,OAA2B,iBAAb,EAAwB,EAjF7C,IAJU,EAqFwD,kBAAY,IAAI,MApF1E,iBACL,OAAO,EAAK,WAAa,GAAG,SAAS,EAAG,QACxC,OAAO,EAAK,WAAW,SAAS,EAAG,QAqFnC,EAAI,OAAO,aAAoB,KAAO,iBAAW,IAAI,KAEzD,IAYJ,EAAM,iBAAiB,UAAY,IACnB,WAAV,EAAE,KAAkB,EAAI,WAGvB,EAEX,CAGA,IAAM,EAAuB,eAmH7B,SAAgB,EAAiB,GAC/B,OAAQ,EAAO,MACb,IAAK,SACH,OAzNN,SAA4B,GAC1B,OAAQ,IACN,MAAM,EAAS,EAAO,aAChB,EAAQ,SAAS,cAAc,SACrC,EAAM,KAAO,SACb,EAAM,MAAqB,MAAb,EAAI,MAAgB,OAAO,EAAI,OAAS,QAElC,IAAhB,GAAQ,MAAmB,EAAM,IAAM,OAAO,EAAO,WACrC,IAAhB,GAAQ,MAAmB,EAAM,IAAM,OAAO,EAAO,WACpC,IAAjB,GAAQ,OAAoB,EAAM,KAAO,OAAO,EAAO,OACvD,GAAQ,cAAa,EAAM,YAAc,EAAO,aAEpD,MAAM,EAAA,KACgB,KAAhB,EAAM,MACJ,EAAO,SACT,EAAI,OAAO,MAGX,EAAI,OAAO,GAAQ,KAAO,GAG5B,EAAI,OAAO,OAAO,EAAM,SAS5B,OANA,EAAM,iBAAiB,OAAQ,GAC/B,EAAM,iBAAiB,UAAY,IACnB,UAAV,EAAE,KAAiB,IACT,WAAV,EAAE,KAAkB,EAAI,WAGvB,EAEX,CAyLa,CAAmB,GAC5B,IAAK,UACH,OAvLI,IACN,MAAM,EAAQ,SAAS,cAAc,SAIrC,OAHA,EAAM,KAAO,WACb,EAAM,UAAY,EAAI,MACtB,EAAM,iBAAiB,SAAA,IAAgB,EAAI,OAAO,EAAM,UACjD,GAmLP,IAAK,OACH,OAAO,EAAiB,GAC1B,IAAK,SACH,OAzHN,SAA4B,GAC1B,OAAQ,IACN,MAAM,EAAS,EAAO,aAChB,EAAS,SAAS,cAAc,UAKtC,GAJI,EAAO,QAAO,EAAO,UAAW,GAGlB,EAAO,UAAY,GAAQ,aAC9B,CACb,MAAM,EAAW,SAAS,cAAc,UACxC,EAAS,MAAQ,EAAO,SAAW,EAAuB,GAC1D,EAAS,YAAc,EAAO,SAAY,GAAQ,YAAc,UAAc,GAAQ,YAAc,GACnF,MAAb,EAAI,QAAe,EAAS,UAAW,GAC3C,EAAO,YAAY,EACrB,EAvIJ,SAAwB,GACtB,MAAM,EAAM,EAAO,QACnB,OAAK,EACiB,mBAAR,EAAqB,IAAQ,EAD1B,EAEnB,EAuII,CAD+B,GACvB,QAAS,IACf,MAAM,EAAI,SAAS,cAAc,UACjC,EAAE,MAAQ,OAAO,EAAI,OACrB,EAAE,YAAc,EAAI,MAChB,EAAO,OAAS,MAAM,QAAQ,EAAI,QAAU,EAAI,MAAM,SAAS,EAAI,OACrE,EAAE,UAAW,EACH,EAAO,OAAS,EAAI,QAAU,EAAI,QAC5C,EAAE,UAAW,GAEf,EAAO,YAAY,KAGrB,MAAM,EAAA,KACJ,GAAI,EAAO,MAAO,CAChB,MAAM,EAAS,MAAM,KAAK,EAAO,iBAAiB,IAAK,GAAM,EAAE,OAC/D,EAAI,OAAO,EACb,MAAW,EAAO,UAAY,EAAO,QAAU,EAC7C,EAAI,OAAO,MAEX,EAAI,OAAO,EAAO,QAUtB,OANA,EAAO,iBAAiB,SAAU,GAClC,EAAO,iBAAiB,OAAQ,GAChC,EAAO,iBAAiB,UAAY,IACpB,WAAV,EAAE,KAAkB,EAAI,WAGvB,EAEX,CAwEa,CAAmB,GAC5B,QACE,OAvEN,SAA0B,GACxB,OAAQ,IACN,MAAM,EAAS,EAAO,aAChB,EAAQ,SAAS,cAAc,SACrC,EAAM,KAAO,OACb,EAAM,MAAqB,MAAb,EAAI,MAAgB,OAAO,EAAI,OAAS,QAE5B,IAAtB,GAAQ,YAAyB,EAAM,UAAY,EAAO,WAC1D,GAAQ,UAAS,EAAM,QAAU,EAAO,SACxC,GAAQ,cAAa,EAAM,YAAc,EAAO,aAGpD,MAAM,EAAA,KACJ,MAAM,EAAW,EAAM,MAGN,KAAb,EAYqB,iBAAd,EAAI,OAAsB,IAAa,EAAI,MAAM,QAAQ,UAAW,MAItD,iBAAd,EAAI,MACb,EAAI,OAAO,OAAO,IAElB,EAAI,OAAO,IAlBP,EAAO,SACT,EAAI,OAAO,MAGX,EAAI,OAAO,KAwBjB,OANA,EAAM,iBAAiB,OAAQ,GAC/B,EAAM,iBAAiB,UAAY,IACnB,UAAV,EAAE,KAAiB,IACT,WAAV,EAAE,KAAkB,EAAI,WAGvB,EAEX,CAwBa,CAAiB,GAE9B,CAUA,SAAS,EAAiB,EAAa,GACrC,MAAY,KAAR,EACE,GAAQ,SAAiB,MACd,GAAQ,eACR,KAAO,EAEjB,OAAO,EAChB,CAgEA,SAAgB,EACd,EACA,EACA,GAEA,KAAM,aAAiB,kBAAmB,OA3B5C,SACE,EACA,EACA,GAIA,MAAoB,KAAhB,EAAM,OAAkC,WAAjB,GAAQ,MAA8C,iBAAlB,EAI/D,MAAK,GAA0E,KAAhB,EAAM,MAC5D,GAAQ,SAAW,KAAO,GAE5B,EAAM,MANJ,OAAO,EAAM,MAOxB,CAYmD,CAAqB,EAAO,EAAQ,GACrF,OAAQ,EAAM,MACZ,IAAK,WACH,OAAO,EAAM,QACf,IAAK,SACH,OAAO,EAAiB,EAAM,MAAO,GACvC,IAAK,OACH,OAtEN,SAAuB,EAAyB,EAAoB,GAClE,OAAK,EAAM,MAOkB,iBAAlB,EACF,EAAM,MAER,EAAM,YATP,GAAQ,SAAiB,KAEA,iBAAlB,EAAmC,mBAAA,IAAqB,MAAO,cAAc,MAAM,EAAG,IACzF,kBAA0B,IAAI,IAO1C,CA0Da,CAAc,EAAO,EAAQ,GACtC,QACE,OAtDN,SAA4B,EAAyB,EAAoB,GAEvE,MAA6B,iBAAlB,EAAmC,EAAiB,EAAM,MAAO,GAExD,KAAhB,EAAM,OAAU,MAAO,EAClB,GAAQ,SAAW,KAAO,GAGN,iBAAlB,GAA8B,EAAM,QAAU,EAAc,QAAQ,UAAW,IACjF,EAEF,EAAM,KACf,CA0Ca,CAAmB,EAAO,EAAQ,GAE/C,CC/VA,IAAa,EAAb,MAEE,kBAAkB,IAAI,IAGtB,GAEA,WAAA,CAAY,GACV,MAAK,EAAiB,CACxB,CAWA,UAAA,CAAW,EAAe,EAAe,EAAU,IACjD,IAAI,EAAc,MAAK,EAAO,IAAI,GAC7B,IACH,iBAAc,IAAI,IAClB,MAAK,EAAO,IAAI,EAAO,IAEzB,EAAY,IAAI,EAAO,GACvB,MAAK,EAAe,EAAO,GAAO,EACpC,CAQA,YAAA,CAAa,EAAe,GAC1B,MAAM,EAAc,MAAK,EAAO,IAAI,GAChC,IACF,EAAY,OAAO,GACM,IAArB,EAAY,MACd,MAAK,EAAO,OAAO,IAGvB,MAAK,EAAe,EAAO,GAAO,EACpC,CAOA,eAAA,CAAgB,GACd,MAAM,EAAc,MAAK,EAAO,IAAI,GACpC,GAAI,EAAa,CACf,MAAM,EAAS,MAAM,KAAK,EAAY,QACtC,MAAK,EAAO,OAAO,GACnB,EAAO,QAAS,GAAU,MAAK,EAAe,EAAO,GAAO,GAC9D,CACF,CAKA,eAAA,GACE,MAAM,EAAU,MAAM,KAAK,MAAK,EAAO,WACvC,MAAK,EAAO,QACZ,EAAQ,QAAA,EAAU,EAAO,MACvB,EAAO,QAAA,CAAS,EAAG,IAAU,MAAK,EAAe,EAAO,GAAO,KAEnE,CAaA,aAAA,CAAc,EAAe,GAC3B,OAAO,MAAK,EAAO,IAAI,IAAQ,IAAI,KAAU,CAC/C,CASA,iBAAA,CAAkB,EAAe,GAC/B,OAAO,MAAK,EAAO,IAAI,IAAQ,IAAI,EACrC,CAQA,eAAA,CAAgB,GACd,MAAM,EAAc,MAAK,EAAO,IAAI,GACpC,QAAO,GAAc,EAAY,KAAO,CAC1C,CAQA,gBAAA,CAAiB,GACf,OAAO,IAAI,IAAI,MAAK,EAAO,IAAI,IAAU,GAC3C,GCrCF,SAAgB,EAAc,EAA2B,EAAe,GACtE,MAAM,EAAW,EAAU,IAAI,GAC/B,QAAK,IACG,EAAU,EAAU,EAC9B,CAmBA,SAAS,EAAU,EAAY,GAC7B,GAAI,IAAM,EAAG,OAAO,EACpB,GAAS,MAAL,GAAkB,MAAL,EAAW,OAAO,EACnC,UAAW,UAAa,EAAG,OAAO,EAGlC,GAAI,aAAa,MAAQ,aAAa,KAAM,OAAO,EAAE,YAAc,EAAE,UAGrE,GAAI,MAAM,QAAQ,GAAI,CACpB,IAAK,MAAM,QAAQ,IAAM,EAAE,SAAW,EAAE,OAAQ,OAAO,EACvD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAE,OAAQ,IAC5B,IAAK,EAAU,EAAE,GAAI,EAAE,IAAK,OAAO,EAErC,OAAO,CACT,CAGA,GAAiB,iBAAN,EAAgB,CACzB,MAAM,EAAO,EACP,EAAO,EACP,EAAQ,OAAO,KAAK,GACpB,EAAQ,OAAO,KAAK,GAC1B,GAAI,EAAM,SAAW,EAAM,OAAQ,OAAO,EAC1C,IAAK,MAAM,KAAO,EAChB,IAAK,EAAU,EAAK,GAAM,EAAK,IAAO,OAAO,EAE/C,OAAO,CACT,CAEA,OAAO,CACT,CAYA,SAAgB,EAAgB,EAA2B,EAAe,GACxE,EAAU,IAAI,EAAO,gBAAgB,GACvC,CAiBA,SAAgB,EAAoB,EAA2B,EAAe,GAC5E,MAAM,EAAW,EAAU,IAAI,GAC/B,IAAK,EAAU,OAAO,EACtB,MAAM,EAAc,EACd,EAAa,EACnB,IAAK,MAAM,KAAO,OAAO,KAAK,GAC5B,EAAW,GAAO,EAAY,GAEhC,OAAO,CACT,CC/JA,IAAa,EAAb,MAIE,yBAAqB,IAAI,IAGzB,uBAAgC,EAGhC,yBAAqB,IAAI,IAGzB,6BAAyB,IAAI,IAG7B,oCAAgC,IAAI,IAKpC,KAAA,GACE,KAAK,cAAc,QACnB,KAAK,qBAAqB,QAC1B,KAAK,UAAU,QACf,KAAK,UAAU,QACf,KAAK,uBAAwB,CAC/B,CASA,OAAA,CAAQ,EAAoB,GAC1B,MAAM,EAAa,KAAK,UAAU,KAElC,ODJJ,SACE,EACA,EACA,GAEA,IAAK,MAAM,KAAO,EAGhB,IAA2D,IAAtD,GAAwC,UAC7C,IACE,MAAM,EAAK,EAAS,GACV,MAAN,GAAe,EAAU,IAAI,IAC/B,EAAU,IAAI,EAAI,gBAAgB,GAEtC,CAAA,MAEA,CAEJ,CCfI,CAAiB,KAAK,UAAW,EAAM,GACnC,KAAK,UAAU,KAAO,IACxB,KAAK,uBAAwB,GACtB,EAGX,CAMA,iBAAA,GACE,OAAK,KAAK,uBACV,KAAK,uBAAwB,EACtB,KAAK,UAAU,MAFkB,IAG1C,CAKA,UAAA,CAAW,EAAe,GACxB,OAAO,EAAW,KAAK,UAAW,EAAO,EAC3C,CAGA,WAAA,CAAY,GACV,GAAI,KAAK,UAAU,KAAO,EAAG,OAAO,EACpC,IAAK,MAAO,KAAU,KAAK,UAAW,CACpC,MAAM,EAAM,EAAW,GACvB,GAAI,GAAO,EAAW,KAAK,UAAW,EAAO,GAAM,OAAO,CAC5D,CACA,OAAO,CACT,CAKA,WAAA,CAAY,EAAe,EAAQ,GACjC,ODDJ,SAA+B,EAA2B,EAAe,EAAe,GACtF,MAAM,EAAW,EAAU,IAAI,GAC/B,QAAK,IAGG,EAFc,EAAc,EAAU,GACzB,EAAc,EAAY,GAEjD,CCLW,CAAY,KAAK,UAAW,EAAO,EAAK,EACjD,CAUA,gBAAA,CAAiB,EAAe,GAC9B,MAAM,EAAQ,KAAK,UAAU,IAAI,GAEjC,MAAO,CAAE,QAAO,kBADU,GAAS,KAAK,qBAAqB,IAAI,IAAU,EAAW,KAAK,UAAW,EAAO,GAC3E,YAAa,KAAK,UAAU,IAAI,GACpE,CAKA,YAAA,CAAa,EAAe,GAC1B,EAAa,KAAK,UAAW,EAAO,GACpC,KAAK,UAAU,OAAO,GACtB,KAAK,cAAc,OAAO,GAC1B,KAAK,qBAAqB,OAAO,EACnC,CAUA,cAAA,CAAe,EAAe,EAAe,GAC3C,MAAM,EAAW,KAAK,UAAU,IAAI,GAChC,GAAU,EAAe,EAAU,EAAO,EAChD,CAGA,OAAA,CAAQ,GACN,KAAK,UAAU,IAAI,GACnB,KAAK,qBAAqB,IAAI,EAChC,CAGA,SAAA,CAAU,GACR,KAAK,cAAc,IAAI,GACvB,KAAK,qBAAqB,IAAI,EAChC,CAGA,eAAA,CAAgB,GACd,IAAK,MAAO,KAAU,KAAK,UAAW,CACpC,MAAM,EAAM,EAAW,GACnB,GAAK,EAAa,KAAK,UAAW,EAAO,EAC/C,CACA,KAAK,UAAU,QACf,KAAK,cAAc,QACnB,KAAK,qBAAqB,OAC5B,CAKA,cAAA,CAAe,GACb,ODJJ,SAAkC,EAA2B,GAC3D,MAAM,EAAW,EAAU,IAAI,GAC/B,OAAO,EAAW,gBAAgB,QAAY,CAChD,CCCW,CAAkB,KAAK,UAAW,EAC3C,CAGA,WAAA,CAAY,GACV,OAAO,KAAK,UAAU,IAAI,EAC5B,CAKA,YAAA,CAAa,GACX,MAAM,EAA6B,GACnC,IAAK,MAAO,EAAO,KAAa,KAAK,UAAW,CAC9C,MAAM,EAAM,EAAW,GACnB,GAAO,EAAW,KAAK,UAAW,EAAO,IAC3C,EAAO,KAAK,CAAE,GAAI,EAAO,SAAU,gBAAgB,GAAW,QAAS,GAE3E,CACA,IAAK,MAAM,KAAS,KAAK,UAAW,CAClC,MAAM,EAAM,EAAW,GACnB,GACF,EAAO,KAAK,CAAE,GAAI,EAAO,cAAU,EAAW,QAAS,GAE3D,CACA,OAAO,CACT,CAGA,cAAA,CAAe,GACb,MAAM,EAAgB,GACtB,IAAK,MAAO,KAAU,KAAK,UAAW,CACpC,MAAM,EAAM,EAAW,GACnB,GAAO,EAAW,KAAK,UAAW,EAAO,IAAM,EAAI,KAAK,EAC9D,CACA,IAAK,MAAM,KAAS,KAAK,UAAW,EAAI,KAAK,GAC7C,OAAO,CACT,CAQA,SAAA,CAAU,EAAe,GACvB,MAAM,EAAW,EAAiB,KAAK,UAAW,EAAO,GAKzD,OAJI,IACF,KAAK,cAAc,OAAO,GAC1B,KAAK,qBAAqB,OAAO,IAE5B,CACT,CAGA,SAAA,CAAU,GACR,IAAK,MAAO,KAAU,KAAK,UAAW,CACpC,MAAM,EAAM,EAAW,GACnB,GAAK,EAAiB,KAAK,UAAW,EAAO,EACnD,CACA,KAAK,cAAc,QACnB,KAAK,qBAAqB,OAC5B,CAKA,cAAA,CAAe,GACb,MAAM,EAAY,GAClB,IAAK,MAAM,KAAM,KAAK,cAAe,CACnC,MAAM,EAAM,EAAW,GACnB,GAAK,EAAK,KAAK,EACrB,CACA,OAAO,CACT,CAGA,gBAAA,GACE,OAAO,MAAM,KAAK,KAAK,cACzB,CAGA,YAAA,CAAa,GACX,OAAO,KAAK,cAAc,IAAI,EAChC,GE7OF,IAAa,EACX,sGAcF,SAAgB,EAAkB,GAEhC,OAAO,GAAI,UAAA,wGAAwC,IACrD,CAuDA,SAAgB,EAAkB,GAChC,MAAmB,iBAAR,IACC,cAAR,GAA+B,gBAAR,GAAiC,cAAR,EAEtD,CA+BA,SAAgB,EAAc,GAE9B,CA6CA,SAAgB,EAAuB,EAAuB,GAC5D,OAA6C,IAAtC,EAAO,oBAAoB,EACpC,CCpHA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACA,EACA,EACA,GAEA,IAAK,EAAO,SAAU,OACtB,GAAI,EAAK,UAAU,SAAS,WAAY,OAExC,MAAM,KAAE,EAAA,WAAM,EAAA,OAAY,EAAA,aAAQ,EAAA,qBAAc,GAAyB,EAGzE,IAAI,EACJ,IACE,EAAQ,EAAK,WAAW,EAC1B,CAAA,MAEA,CAGA,MAAM,EAA2C,EAE5C,GAAa,EAAa,UAAU,EAAQ,EAAoC,WACjF,EAEE,EAAgB,EAAkB,EAAO,OAAS,EAAc,EAAS,EAAO,YAAS,EAE/F,EAAK,UAAU,IAAI,WACnB,EAAa,IAAI,GAAG,KAAY,KAIhC,MAAM,EAAS,GAAe,EAAK,cAC/B,GDQN,SAAsC,GAEpC,EAAM,oBADS,EAAM,oBAAsB,GAAK,EAEhD,EAAM,aAAa,mBAAoB,GACzC,CCZa,CAAsB,GAEjC,IAAI,GAAgB,EACpB,MAAM,EAAU,IAGd,GAAI,IAAmB,IAAe,EAAK,sBAAwB,OAOnE,MAAM,EAAQ,EAAQ,EAAK,aAAa,QAAS,EAC3C,EAAkB,GAAO,KAAO,EAChC,EAAe,GAAO,OAAS,EACrC,EAAK,gBAAgB,EAAc,EAAQ,EAAU,IAEjD,EAAA,KAEJ,GADA,GAAgB,EACZ,EAAkB,EAAO,OAAQ,CAGnC,MAAM,GADQ,EAAQ,EAAK,aAAa,QAAS,IAClB,KAAO,EACtC,EAAe,EAAgB,EAAO,MAAO,EAC/C,GAGI,EAAa,SAAS,cAAc,OAC1C,EAAW,UAAY,kBACvB,MAAM,EAAiB,EAAuB,GAW1C,EAAK,mBAAmB,EAAK,oBAAoB,cAAc,GACnE,EAAK,UAAY,GACjB,EAAK,YAAY,GACb,GAAgB,EAAK,YAAY,GAGrC,EAAW,iBAAiB,UAAY,IAKtC,IAAI,EAAE,iBAAN,CASA,GAAc,UAAV,EAAE,IAAiB,CACrB,MAAM,EAAa,EAAE,OACrB,GACE,GAC+C,SAA/C,EAAW,eAAe,kBAC1B,EAAW,eAAe,iBAE1B,MAEJ,CACA,GAAc,UAAV,EAAE,IAAiB,CAWrB,MAAM,EAAc,EAAE,OAChB,EAAsB,EAAkB,GAC9C,GAAI,GAAuB,IAAwB,EACjD,OAGF,GAAI,EAAY,CAOd,EAAE,iBACF,MAAM,EAAQ,EAAW,cAAc,yBAKvC,YAHI,GACF,EAAO,EAAc,EAAO,EAAiC,IAGjE,CACA,GAAI,EAAuB,EAAQ,GAAI,OACvC,EAAE,kBACF,EAAE,iBACF,GAAgB,EAChB,EAAK,YAAY,GAAU,EAC7B,CACA,GAAc,WAAV,EAAE,IAAkB,CAEtB,GAAI,EAGF,OAFA,EAAE,uBACF,EAAE,iBAGJ,GAAI,EAAuB,EAAQ,GAAI,OACvC,EAAE,kBACF,EAAE,iBACF,IACA,EAAK,YAAY,GAAU,EAC7B,CArEwB,IAwE1B,MAAM,EAAc,EACd,EAAY,EAAY,iBAExB,ED7KR,SACE,EACA,GAGA,GAAI,EAAI,OAAQ,OAAO,EAAI,OAI3B,GADkB,EAAI,iBACP,MAAO,WAGtB,IAAK,EAAI,KAAM,OAIf,MAAM,EAAoB,EAAa,iBAAiB,aACxD,GAAI,IAAmB,EAAI,OAAO,OAChC,OAAO,EAAiB,EAAI,MAAM,OAIpC,MAAM,EAAU,EAAK,mBACrB,GAAI,GAAS,eAAgB,CAC3B,MAAM,EAAa,EAAQ,eAAqB,EAAI,KAAM,EAAK,cAC/D,GAAI,GAAY,OACd,OAAO,EAAW,MAEtB,CAIF,CC6IqB,CAAc,EAAM,IAAgB,EAAiB,GAClE,EAAQ,EAMR,EAAc,GAAG,KAAY,EAAO,QACpC,EAAgD,GACtD,EAAqB,IAAI,EAAc,IACrC,IAAK,MAAM,KAAM,EAAW,EAAG,KAEjC,MAAM,EAAiB,IACrB,EAAU,KAAK,IAGjB,GAAmB,aAAf,GAA6B,GA8KnC,SACE,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,GAEA,MAAM,EAAY,EAAO,iBACzB,IAAK,EAAW,OAEhB,MAAM,EAAQ,EAAU,WAAU,GAC5B,EAAiB,EAAO,iBAE1B,EAKF,EAAM,UAAY,EAAe,CAC/B,IAAK,EACL,MAAO,EACP,MAAO,EAAO,MACd,SACA,SACA,WAGF,EAAM,iBAA8B,KAAK,QAAS,IACjB,IAA3B,EAAK,WAAW,QAAgB,EAAK,YAAY,WAAa,KAAK,YACrE,EAAK,YACH,EAAK,aACD,QAAQ,mBAAqC,MAAjB,EAAwB,GAAK,OAAO,IACjE,QAAQ,kCAAA,CAAoC,EAAI,KAC/C,IAAK,EAAkB,GAAI,MAAO,GAClC,MAAM,EAAK,EAAoC,GAC/C,OAAY,MAAL,EAAY,GAAK,OAAO,MAC3B,MAKhB,MAAM,EAAQ,EAAM,cAClB,yBAEF,GAAI,EAAO,CACL,aAAiB,kBAAmC,aAAf,EAAM,KAC7C,EAAM,UAAY,EAElB,EAAM,MAAQ,OAAO,GAAiB,IAGxC,IAAI,GAAgB,EACpB,EAAM,iBAAiB,OAAA,KACjB,GACJ,EAAO,EAAc,EAAO,EAAQ,MAEtC,EAAM,iBAAiB,UAAY,IACjC,MAAM,EAAI,EACV,GAAc,UAAV,EAAE,IAAiB,CACrB,GAAI,EAAuB,EAAK,OAAQ,GAAI,OAC5C,EAAE,kBACF,EAAE,iBACF,GAAgB,EAChB,EAAO,EAAc,EAAO,EAAQ,IACpC,EAAK,YAAY,GAAU,EAC7B,CACA,GAAc,WAAV,EAAE,IAAkB,CACtB,GAAI,EAAuB,EAAK,OAAQ,GAAI,OAC5C,EAAE,kBACF,EAAE,iBACF,IACA,EAAK,YAAY,GAAU,EAC7B,IAEE,aAAiB,kBAAmC,aAAf,EAAM,MAC7C,EAAM,iBAAiB,SAAA,IAAgB,EAAO,EAAM,UAEjD,GACH,WAAA,IAAiB,EAAM,MAAM,CAAE,eAAe,IAAS,EAE3D,CACA,EAAW,YAAY,EACzB,CAnQI,CAAqB,EAAM,EAAY,EAAa,EAAS,EAAe,EAAQ,EAAQ,EAAW,GAIvG,EAAe,IACb,GAAc,MAAV,GAAoC,iBAAX,EAAqB,OAClD,MAAM,EAAQ,EAAW,cACvB,yBAEE,IACE,aAAiB,kBAAmC,aAAf,EAAM,KAC7C,EAAM,UAAY,EAElB,EAAM,MAAQ,OAAO,GAAU,YAIhC,GAA0B,iBAAf,EAAyB,CACzC,MAAM,EAAK,SAAS,cAAc,GAClC,EAAG,MAAQ,EACX,EAAG,iBAAiB,SAAA,IAAgB,EAAO,EAAG,QAE9C,EAAe,IACb,EAAG,MAAQ,IAEb,EAAW,YAAY,GAClB,GACH,eAAA,KAEE,EAD6B,cAAc,IAChC,MAAM,CAAE,eAAe,KAGxC,MAAO,GAA0B,mBAAf,EAA2B,CAc3C,MAAM,EAAY,EAAmB,CAZnC,IAAK,EACL,MAAO,GAAS,GAChB,QACA,MAAO,EAAO,MACd,SACA,SACA,SACA,YACA,gBACA,KAAM,EAAK,OAIW,iBAAb,GAOT,EAAW,UAAY,ED7J7B,SACE,EACA,EACA,EACA,GAEA,MAAM,EAAQ,EAAW,cAAc,yBAKlC,IAEL,EAAM,iBAAiB,OAAA,KACrB,EAAO,EAAc,EAAO,EAAQ,MAGlC,aAAiB,kBAAmC,aAAf,EAAM,KAC7C,EAAM,iBAAiB,SAAA,IAAgB,EAAO,EAAM,UAC3C,aAAiB,mBAC1B,EAAM,iBAAiB,SAAA,IAAgB,EAAO,EAAc,EAAO,EAAQ,KAE/E,CCyIM,CAAiB,EAAY,EAAe,EAAQ,GAEpD,EAAe,IACb,MAAM,EAAQ,EAAW,cACvB,yBAEE,IACE,aAAiB,kBAAmC,aAAf,EAAM,KAC7C,EAAM,UAAY,EAElB,EAAM,MAAQ,OAAO,GAAU,QAI5B,aAAoB,MAC7B,EAAW,YAAY,GAErB,aAAoB,kBACpB,aAAoB,mBACpB,aAAoB,oBAKpB,EAAe,IACT,aAAoB,kBAAsC,aAAlB,EAAS,KACnD,EAAS,UAAY,EAErB,EAA+B,MAAQ,OAAO,GAAU,MAP5D,EAAK,aAAa,sBAAuB,MAWjC,GAAY,EAAW,iBAKjC,EAAK,aAAa,sBAAuB,IAEtC,GACH,eAAA,KAEE,EAD6B,cAAc,IAChC,MAAM,CAAE,eAAe,KAGxC,MAAO,GAAI,GAAoC,iBAAf,EAAyB,CACvD,MAAM,EAAc,SAAS,cAAc,OAC3C,EAAY,aAAa,uBAAwB,IACjD,EAAY,aAAa,aAAc,EAAO,OAC9C,EAAW,YAAY,GACvB,EAAK,aAAa,sBAAuB,IACzC,MAAM,EAAkC,CACtC,IAAK,EACL,MAAO,GAAS,GAChB,QACA,MAAO,EAAO,MACd,SACA,SACA,SACA,YACA,gBACA,KAAM,EAAK,MAEb,GAAI,EAAW,MACb,IAEE,EAAW,MAAM,CAAE,cAAsB,UAAgB,KAAM,GACjE,CAAA,MAAS,GFhGgB,EA7IG,SA6ImB,EEmG3C,2CAA2C,EAAO,WAAW,IFnGD,EEoG5D,EAAK,KAAK,GFnGlB,QAAQ,KAtBV,SAAiC,EAAsB,EAAiB,EAAiB,GAEvF,MAAO,GAnOT,SAA2B,EAAiB,GAG1C,MAAO,YAFI,EAAS,IAAI,IAAW,KACpB,EAAa,IAAI,IAAe,KAEjD,CA8NiB,CAAW,EAAQ,MACd,MAAS,uBApB/B,SAAiB,GACf,MAAO,qCAAgB,EAAK,eAC9B,CAkB4D,CAAQ,IACpE,CAmBe,CAAiB,EAAM,EAAS,EAAQ,GEqGjD,MAEA,EAAK,cACH,IAAI,YAAY,wBAAyB,CAAE,OAAQ,CAAE,cAAa,KAAM,EAAY,aAG1F,CF5GF,IAA+B,EAAsB,EAAiB,EAAiB,GE0HvF,SAAiC,EAAyB,GACxD,MAAM,EAAiC,iBAAlB,EAAO,OAAsB,EAAO,OAAS,EAAO,MACzE,IAAK,EAAO,OAEZ,eAAA,KACE,MAAM,EAAU,EAAW,cAAc,GACpC,IAEH,EAAQ,aAAa,eACrB,EAAQ,aAAa,oBACrB,EAAQ,aAAa,UACpB,EAAQ,IAAM,EAAW,cAAc,cAAc,IAAI,OAAO,EAAQ,UACzE,EAAQ,QAAQ,UACN,EAAQ,aAAa,aAAc,KAEnD,CA3BE,CAAqB,EAAY,EACnC,CC3OA,IAAa,EAAb,cAAgD,EAK9C,gBAAoD,CAClD,gBAAiB,CACf,CACE,SAAU,WACV,MAAO,SACP,YAAa,iCACb,OAAS,IAAY,IAAN,GAA2B,mBAAN,GAEtC,CACE,SAAU,SACV,MAAO,SACP,YAAa,gCAEf,CACE,SAAU,eACV,MAAO,SACP,YAAa,sCAEf,CACE,SAAU,WACV,MAAO,SACP,YAAa,wDAGjB,OAAQ,CACN,CACE,KAAM,sBACN,YAAa,6EAEf,CACE,KAAM,qBACN,YACE,wIAGN,QAAS,CACP,CACE,KAAM,YACN,YAAa,sDAEf,CACE,KAAM,0BACN,YACE,2KAEJ,CACE,KAAM,kBACN,YACE,iRAMR,KAAgB,UAEhB,g/CAGA,iBAAuB,GACrB,MAAO,CACL,KAAM,MACN,OAAQ,QAEZ,CAKA,KAAI,GACF,MAA4B,SAArB,KAAK,OAAO,IACrB,CAWA,EAAA,CAAgB,EAAyB,GAEvC,MAAM,EAAc,MAAK,EAAc,iBAAiB,YACxD,GAAI,IAAgB,EAAY,GAAa,OAAO,EAGpD,MAAM,SAAE,GAAa,EACrB,MAAwB,mBAAb,EAAgC,EAAS,IAChC,IAAb,CACT,CAOA,EAAA,CAAmB,GACjB,OAA2B,IAApB,EAAO,UAAgD,mBAApB,EAAO,QACnD,CAKA,IAAiB,EAGjB,GAIA,GAGA,IAAiB,EAGjB,kBAAoB,IAAI,IAGxB,kBAAgB,IAAI,IASpB,kBAAkB,IAAI,QAOtB,kBAAwB,IAAI,IAG5B,IAAuB,EAGvB,IAAuB,EAMvB,GAQA,IAAwB,EAOxB,IAAsB,EAMtB,IAAkB,EAMlB,GAAsG,KAKtG,GAAkB,IAAI,EAYtB,kBAA4B,IAAI,IAKhC,GAMA,KAAI,GACF,OAAO,KAAK,IACd,CAOA,MAAA,CAAgB,GACd,MAAM,OAAO,GAEb,MAAM,EAAS,KAAK,iBACd,EAAe,MAAK,EAG1B,MAAK,EAAc,IAAI,EAAA,CAAuB,EAAO,EAAO,KAC1D,MAAK,EAA0B,EAAO,EAAO,KAI/C,MAAK,EAAc,CACjB,KAAM,EACN,WAAY,MAAK,EACjB,OAAQ,KAAK,OACb,aAAc,MAAK,EACnB,qBAAsB,MAAK,EAC3B,oBAAA,KAAmD,IAAxB,MAAK,EAChC,gBAAA,CAAkB,EAAI,EAAK,EAAK,IAAQ,MAAK,EAAiB,EAAI,EAAK,EAAK,GAC5E,YAAA,CAAc,EAAI,IAAW,MAAK,EAAa,EAAI,IAIrD,MAAK,IAGL,OAAO,eAAe,EAAM,cAAe,CACzC,IAAA,IAAW,KAAK,YAChB,cAAc,IAIhB,OAAO,eAAe,EAAM,gBAAiB,CAC3C,IAAA,IAAW,KAAK,cAChB,cAAc,IAIhB,OAAO,eAAe,EAAM,mBAAoB,CAC9C,IAAA,IAAW,MAAK,EAAO,cACvB,cAAc,IAIhB,EAAc,iBAAoB,GAAqB,KAAK,iBAAiB,GAE7E,MAAK,EAAwB,EAAQ,GAGjC,MAAK,IACP,EAAa,iBAAkB,EAC/B,KAAK,YAAY,UAAU,IAAI,iBAC/B,KAAK,gBACL,MAAK,EAAyB,EAAQ,GAE1C,CAQA,EAAA,CAAwB,EAAqB,GA+G3C,GA7GA,SAAS,iBACP,UACC,IAEC,IAAI,MAAK,GACK,WAAV,EAAE,MAA4C,IAAxB,MAAK,EAAuB,CACpD,GAAI,EAAuB,KAAK,OAAQ,GAAI,OAC5C,MAAK,EAAa,MAAK,GAAgB,EACzC,GAEF,CAAE,SAAS,EAAM,WAOnB,SAAS,iBACP,YACC,IAEC,GAAI,MAAK,EAAa,OACtB,IAA4B,IAAxB,MAAK,EAAuB,OAChC,MAAM,EAAQ,EAAa,yBAAyB,MAAK,GACzD,IAAK,EAAO,OAEZ,IADc,EAAE,cAAgB,EAAE,gBAAmB,IAC5C,SAAS,GAAQ,OAM1B,MAAM,EAAS,EAAE,OACjB,IAAI,GAAW,KAAK,YAAY,SAAS,KAAW,KAAK,KAAK,gBAAgB,GAA9E,CASA,GAAI,IAAW,KAAK,YAAY,SAAS,GAAS,CAChD,MAAM,EAAQ,EAAa,yBAAyB,MAAK,GACzD,GAAI,GFlRd,SAAwC,EAAiC,GACvE,IAAK,EAAQ,OAAO,EACpB,MAAM,EAAW,EAAQ,iBAA8B,yCACvD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAS,OAAQ,IAAK,CACxC,MAAM,EAAK,EAAS,GAAG,aAAa,iBACpC,IAAK,EAAI,SACT,MAAM,EAAQ,EAAQ,eAAe,eAAe,GACpD,GAAI,GAAS,EAAM,SAAS,GAAiB,OAAO,CACtD,CACA,OAAO,CACT,CEwQuB,CAAwB,EAAQ,GAAQ,MACvD,CAEI,EAAuB,KAAK,OAAQ,IAGxC,eAAA,MAC8B,IAAxB,MAAK,GACP,MAAK,EAAa,MAAK,GAAgB,IAlBzC,GAsBJ,CAAE,WAMA,KAAK,OAAO,WACd,KAAK,YAAY,iBACf,WACC,IAEC,GAAI,MAAK,EAAa,OACtB,IAA4B,IAAxB,MAAK,EAAuB,OAEhC,MAAM,EAAU,EAAE,cAEd,GAAW,KAAK,KAAK,gBAAgB,IAErC,GAAW,KAAK,YAAY,SAAS,IAGzC,eAAA,MAE8B,IAAxB,MAAK,GACT,MAAK,OAGT,CAAE,WAON,KAAK,YAAY,iBACf,cACC,IACC,MAAM,EAAU,EAAkB,OAOlC,GAAsB,SAAlB,EAAO,OAAmB,OAC9B,MAAM,EAAM,GAAG,EAAO,YAAY,EAAO,QACnC,EAAK,MAAK,EAAsB,IAAI,GACtC,GAAI,EAAG,EAAO,WAEpB,CAAE,WAIA,KAAK,OAAO,cAAe,CAE7B,MAAM,EAAkB,IACtB,MAAM,EAAU,EAAkB,QAAQ,OAE1C,IAAK,EAAQ,OACb,MAAM,EAAwB,aAAhB,EAAO,KAAsB,EAAO,QAAU,CAAC,GACvD,iBAAO,IAAI,IACjB,IAAK,MAAM,KAAQ,EAAO,CAIxB,MAAM,OACW,IAAf,EAAK,MAAsB,MAAK,EAAc,aAAa,EAAK,QAAQ,IAAM,KAAK,KAAK,EAAK,UAE/F,IAAK,EAAK,SACV,MAAM,EAAQ,EAAK,OAAS,MAAK,EAAc,GAC/C,IAAK,GAAS,EAAK,IAAI,GAAQ,SAC/B,EAAK,IAAI,GACT,MAAM,EAAQ,MAAK,EAAO,WAAW,EAAO,GAC5C,MAAK,EAAiB,EAAO,EAAK,EAAQ,WAAa,WACzD,GAEF,KAAK,YAAY,iBAAiB,OAAQ,EAAgB,CAAE,WAC5D,KAAK,YAAY,iBAAiB,OAAQ,EAAgB,CAAE,WAG5D,KAAK,GAAG,eAAiB,IACvB,MAAM,EAAQ,MAAK,EAAc,EAAO,KAC3B,MAAT,GACF,KAAK,UAAU,OAAO,MAU1B,KAAK,GAAG,kBAAoB,IACtB,MAAK,EAAkB,EAAO,OAAO,KAAK,sBAElD,CACF,CAMA,EAAA,CAAkB,GAChB,MAAM,EAAe,MAAK,EAC1B,QAAK,EAAa,UACX,MAAK,EAAO,QAAQ,EAAO,IAChC,IACE,OAAO,EAAa,WAAW,EACjC,CAAA,MACE,MACF,GAEJ,CAOA,EAAA,CAAyB,EAAqB,GAE5C,KAAK,YAAY,iBACf,UACC,IACC,MAAM,EAAS,EAAE,OAIjB,GAAI,IAAW,KAAK,aAChB,EAAO,QAAA,uGAAoC,CAE7C,GAAI,MAAK,EAGP,OAFA,EAAO,YACP,KAAK,YAAY,QAMnB,MAAM,EAAW,EAAa,UACxB,EAAW,EAAa,UACxB,EAAO,MAAK,EAClB,IAAK,GAAQ,EAAK,WAAa,GAAY,EAAK,WAAa,EAAU,CACrE,MAAM,EAAS,EAAa,kBAAkB,GACxC,EAAU,EAAa,QAAQ,GACrC,GAAI,GAAQ,OAAS,EAAS,CAC5B,MAAM,EAAQ,EAAO,MACrB,MAAK,EAAwB,CAC3B,SAAU,EACV,SAAU,EACV,QACA,MAAO,EAAc,EAAS,GAElC,CACF,CAEA,MAAK,GAAwB,CAC/B,GAEF,CAAE,WAGJ,KAAK,YAAY,iBACf,WACC,IACC,MAAM,EAAS,EAAE,OACX,EAAU,EAAE,cAclB,GAAI,GAAU,EAAS,CACrB,GAAI,EAAO,SAAS,GAAU,OAC9B,MAAM,EAAgB,EAAkB,GACxC,GAAI,GAAiB,IAAkB,KAAK,aAAe,KAAK,YAAY,SAAS,GACnF,MAEJ,CAGG,IACC,KAAK,YAAY,SAAS,IAAa,KAAK,KAAK,gBAAgB,KAClE,EAAQ,QAAA,yGAET,MAAK,GAAwB,EAE7B,MAAK,EAAwB,OAGjC,CAAE,WAKJ,KAAK,YAAY,iBACf,UACC,IACC,GAAc,WAAV,EAAE,KAAoB,MAAK,EAAuB,CAEpD,GAAI,EAAuB,KAAK,OAAQ,GAQtC,YALA,eAAA,KACM,MAAK,GACP,MAAK,MAKX,MAAK,IACL,EAAE,iBACF,EAAE,iBACJ,GAEF,CAAE,SAAS,EAAM,WAInB,KAAK,YAAY,iBACf,YACC,IACgB,EAAE,OACN,QAAA,yGACT,MAAK,GAAsB,IAG/B,CAAE,UAEN,CAGA,MAAA,GACuB,MAAK,EACb,iBAAkB,EAC/B,KAAK,YAAY,UAAU,OAAO,iBAClC,MAAK,GAAiB,EACtB,MAAK,OAAmB,EACxB,MAAK,OAAoB,EACzB,MAAK,GAAiB,EACtB,MAAK,EAAkB,QACvB,MAAK,EAAO,QACZ,MAAK,EAAc,QACnB,MAAK,EAAsB,QAC3B,MAAK,GAAwB,EAC7B,MAAK,GAAsB,EAC3B,MAAK,EAAwB,KAC7B,MAAK,GAAkB,EACvB,MAAK,iBAAkB,IAAI,QAC3B,MAAM,QACR,CAMA,WAAA,CAAqB,GACnB,GAAmB,cAAf,EAAM,KAER,OAAO,MAAK,IAAuC,IAAxB,MAAK,EAElC,GAAmB,4BAAf,EAAM,KAAoC,CAK5C,MAAM,EAAW,MAAK,EAAc,iBAAiB,SAA6C,GAC5F,iBAAU,IAAI,IACpB,IAAK,MAAM,KAAO,EAAS,EAAQ,IAAI,EAAI,MAAO,GAKlD,MAJM,CAAmC,EAAO,KAC9C,MAAM,EAAM,EAAQ,IAAI,GACxB,QAAO,GAAM,MAAK,EAAgB,EAAK,GAG3C,CACA,MAAmB,oBAAf,EAAM,KACD,MAAK,EAAuB,EAAM,cAD3C,CAIF,CAaA,EAAA,CAAuB,GACrB,MAAM,EAAe,MAAK,EAEpB,GADW,EAAa,iBAAqD,IAEzE,KAAM,GAAM,EAAE,QAAU,EAAI,QACnC,EAAa,UAA4C,KAAM,GAAM,EAAE,QAAU,EAAI,OAClF,EACH,EAAI,MACS,MAAb,EAAI,MAAgB,EAAa,aAAa,EAAI,QAAQ,SAAM,IAChE,EAAa,QAAQ,EAAI,UAE5B,GAAmB,SAAf,EAAI,OAAmB,CAOzB,IAAK,IAAY,EAAkB,EAAI,OAAQ,OAC/C,EAAe,EAAS,EAAI,MAAO,EAAI,UACvC,EAAwB,GACxB,MAAM,EAAQ,MAAK,EAAc,GACjC,GAAI,GAAS,KAAK,OAAO,cAAe,CACtC,MAAK,EAAO,eAAe,EAAO,EAAI,MAAO,EAAI,UACjD,MAAM,EAAa,MAAK,EAAO,WAAW,EAAO,GAC5C,GAAY,MAAK,EAAO,cAAc,OAAO,GAClD,MAAK,EAAiB,EAAO,EAAS,EAAa,WAAa,WAClE,CACA,OAAO,CACT,CAEA,GAAK,GAAW,GAAY,EAAkB,EAAI,OAAlD,CAEA,GAAmB,YAAf,EAAI,OAAsB,CAI5B,EAAe,EAAS,EAAI,MAAO,EAAI,UACvC,EAAwB,GACxB,MAAM,EAAQ,MAAK,EAAc,GACjC,GAAI,GAAS,KAAK,OAAO,cAAe,CACtC,MAAM,EAAQ,MAAK,EAAO,WAAW,EAAO,GACvC,GAAO,MAAK,EAAO,cAAc,OAAO,GAC7C,MAAK,EAAiB,EAAO,EAAS,EAAQ,WAAa,WAC7D,CACA,OAAO,CACT,CAOA,OADA,MAAK,EAAiB,EAAI,SAAU,EAAQ,EAAI,SAAU,EAAS,EAAI,QAChE,EAAc,EAAS,EAAI,SAAW,EAAI,QAtBgB,CAuBnE,CAYA,WAAA,CAAqB,GAEnB,GAAI,MAAK,EAAa,OAAO,EAE7B,MAAM,EAAe,MAAK,EACpB,EAAS,KAAK,OAAO,QAAU,EAAa,iBAAiB,OAGnE,IAAe,IAAX,GAA+B,WAAX,EAAqB,OAAO,EAGpD,GAAe,UAAX,GAAiC,aAAX,EAAuB,OAAO,EAGxD,MAAM,EAA6C,aAA7B,EAAM,cAAc,KAC1C,GAAe,UAAX,GAAsB,EAAe,OAAO,EAChD,GAAe,aAAX,IAA0B,EAAe,OAAO,EAEpD,MAAM,SAAE,GAAa,EAIrB,QAD0B,EAAa,UAAU,KAAM,GAAQ,MAAK,EAAmB,QAIlF,MAAK,EAAe,EAAa,MAAM,MAG5C,EAAM,cAAc,kBACpB,KAAK,cAAc,IACZ,GACT,CAMA,SAAA,CAAmB,GACjB,OAAQ,EAAM,KACZ,IAAK,SACH,OAAO,MAAK,EAAa,GAC3B,IAAK,UACL,IAAK,YACL,IAAK,YACL,IAAK,aACH,OAAO,MAAK,EAAY,GAC1B,IAAK,MACH,OAAO,MAAK,EAAU,GACxB,IAAK,IACL,IAAK,WACH,OAAO,MAAK,EAAY,GAC1B,IAAK,QAEH,QAAI,EAAM,UAAY,EAAM,SAAW,EAAM,QAAU,EAAM,UACtD,MAAK,EAAY,GAC1B,IAAK,KACH,OAAO,MAAK,EAAS,GACvB,QAEE,OAAO,EAEb,CAWA,EAAA,CAAa,GAEX,OAAI,MAAK,GAAe,MAAK,GAC3B,MAAK,IAEL,KAAK,sBACE,IAImB,IAAxB,MAAK,IAA0B,MAAK,IAClC,EAAuB,KAAK,OAAQ,IACxC,MAAK,EAAa,MAAK,GAAgB,IADgB,EAM3D,CAYA,EAAA,CAAY,GACV,MAAM,EAAsB,EAAkB,EAAM,QAC9C,IAAmB,GAAuB,IAAwB,KAAK,YACvE,EAA2B,YAAd,EAAM,KAAmC,cAAd,EAAM,IAGpD,QAAI,MAAK,IAAgB,MAAK,IAA0B,QAKpD,MAAK,IAAgB,MAAK,IAAyB,IAAmB,OAgBtE,IAAsC,IAAxB,MAAK,GAA0B,MAAK,GAGxD,CAGA,EAAA,CAAU,GACR,SAA4B,IAAxB,MAAK,IAA0B,MAAK,KAExC,EAAM,iBAGF,MAAK,GACP,MAAK,EAAa,MAAK,GAAgB,IAChC,IAGT,MAAK,GAAsB,EAAM,WAC1B,GACT,CAGA,EAAA,CAAY,GAEV,GAAI,EAAM,UAAY,EAAM,SAAW,EAAM,SAAW,EAAM,OAAQ,OAAO,EAE7E,IAA4B,IAAxB,MAAK,EAAuB,OAAO,EAEvC,MAAM,EAAe,MAAK,EACpB,EAAW,EAAa,UACxB,EAAW,EAAa,UAC9B,GAAI,EAAW,GAAK,EAAW,EAAG,OAAO,EAEzC,MAAM,EAAS,EAAa,gBAAgB,GACtC,EAAU,EAAa,MAAM,GACnC,IAAK,IAAW,GAA2B,YAAhB,EAAO,KAAoB,OAAO,EAC7D,IAAK,MAAK,EAAgB,EAA2B,GAAe,OAAO,EAE3E,MAAM,EAAQ,EAAO,MAErB,QAAK,EAAkB,KAEvB,MAAK,EAAiB,EAAU,GAAS,EAAc,EAAS,GAAQ,GACxE,EAAM,iBAEN,KAAK,iBACE,EACT,CAGA,EAAA,CAAY,GAEV,GAAI,MAAK,IAAgB,MAAK,EAE5B,OADA,MAAK,KACE,EAGT,GAAI,MAAK,GAAe,MAAK,EAAuB,OAAO,MAAK,EAA2B,GAE3F,IAA4B,IAAxB,MAAK,EAAuB,CAM9B,MAAM,EAAa,EAAM,OACzB,QACE,GAC+C,SAA/C,EAAW,eAAe,mBAC1B,EAAW,eAAe,qBAIxB,EAAuB,KAAK,OAAQ,EAG1C,CAEA,OAAO,MAAK,EAAuB,EACrC,CAcA,EAAA,CAA2B,GACzB,MAAM,EAAsB,EAAkB,EAAM,QACpD,GAAI,GAAuB,IAAwB,EAAM,OAIvD,OAAO,EAGT,MAAK,EAAwB,KAC7B,MAAM,EAAW,SAAS,cAQ1B,OAPI,GAAY,KAAK,YAAY,SAAS,KACxC,EAAS,OACT,KAAK,YAAY,SAEnB,MAAK,GAAwB,EAC7B,MAAK,GAAsB,EAC3B,KAAK,sBACE,CACT,CAOA,EAAA,CAAuB,GACrB,MAAM,EAAe,MAAK,EACpB,EAAS,KAAK,OAAO,QAAU,EAAa,iBAAiB,OACnE,IAAe,IAAX,GAA+B,WAAX,EAAqB,OAAO,EAEpD,MAAM,EAAW,EAAa,UACxB,EAAW,EAAa,UAC9B,GAAI,EAAW,EAAG,OAAO,EAGzB,MAAM,EAAoB,EAAa,UAAU,KAAM,GAAQ,MAAK,EAAmB,IAEjF,EAAM,EAAa,MAAM,GAC/B,IAAK,IAAsB,MAAK,EAAe,GAAuB,OAAO,EAK7E,MAAM,EADS,EAAa,gBAAgB,IACtB,OAAS,GACzB,EAAQ,GAAS,EAAM,EAAc,EAAK,QAAS,EACnD,EAAS,KAAK,YAAY,cAAc,cAAc,iBAAwB,OAG9E,EAAgB,IAAI,YAAY,gBAAiB,CACrD,YAAY,EACZ,SAAS,EACT,OAAQ,CACN,SAAU,EACV,SAAU,EACV,QACA,QACA,MACA,SACA,QAAS,WACT,cAAe,KAMnB,OAHA,KAAK,YAAY,cAAc,GAG3B,EAAc,kBAChB,EAAM,kBACC,IAGT,KAAK,cAAc,IACZ,EACT,CAGA,EAAA,CAAS,GACP,IAA4B,IAAxB,MAAK,GAAyB,MAAK,EAAa,OAAO,EAE3D,MAAM,EAAe,MAAK,EAE1B,IAAe,KADA,KAAK,OAAO,QAAU,EAAa,iBAAiB,QAC7C,OAAO,EAE7B,MAAM,EAAW,EAAa,UACxB,EAAW,EAAa,UAC9B,GAAI,EAAW,GAAK,EAAW,EAAG,OAAO,EAEzC,MAAM,EAAS,EAAa,gBAAgB,GACtC,EAAU,EAAa,MAAM,GACnC,SAAK,GAAW,GAAY,EAAO,WAC9B,MAAK,EAAgB,EAA2B,KAErD,EAAM,iBACN,KAAK,cAAc,EAAU,EAAO,QAC7B,GACT,CASA,EAAA,CAAgB,GACd,MAAM,EAAkC,CAAC,EACzC,IAAK,MAAM,KAAQ,MAAM,KAAK,EAAO,YAAa,CAChD,IAAK,EAAK,KAAK,WAAW,SAAU,SACpC,MAAM,EAAM,EAAK,KACd,MAAM,GACN,QAAQ,YAAA,CAAc,EAAI,IAAmB,EAAO,eACpD,OACE,IACL,EAAO,GAAO,EAAK,MACrB,CACA,OAAO,CACT,CASA,EAAA,GACE,MAAM,EAAe,MAAK,EACpB,EAAO,EAAa,aAC1B,IAAK,EAAM,OAEX,MAAM,EAAkB,EAAa,gBACrC,IAAK,EAAiB,OAEtB,MAAM,EAAe,MAAM,KAAK,EAAK,UAAU,OAAQ,GAAoC,kBAA7B,EAAG,QAAQ,eACzE,IAAK,EAAa,OAAQ,OAE1B,MAAM,EAAU,EAAa,mBACvB,EAAgB,EAAgB,eAAiB,CAAC,EAExD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAa,OAAQ,IAAK,CAC5C,MAAM,EAAS,EAAa,GACtB,EAAO,EAAO,aAAa,SAAS,OAC1C,IAAK,EAAM,SAEX,MAAM,EAAW,EAAa,GAE9B,GAAI,GAAU,OAAQ,SAEtB,MAAM,EAAY,EAAO,cAAc,0BACjC,EAAgB,GAAa,EAEnC,IAAI,EAA+C,GAAS,YAAY,GACnE,EAAQ,eAAe,QACxB,EAEJ,IAAK,GAAU,EAAW,CACxB,MAAM,EAAS,MAAK,EAAgB,GAC9B,EAAW,EAAgB,EAAU,WAC3C,EAAW,GACT,EAAS,CACP,IAAK,EAAI,IACT,MAAO,EAAI,MACX,MAAO,EAAI,MACX,OAAQ,EAAI,OACZ,YAAa,GAEnB,CAEK,IAEL,EAAa,GAAQ,IAAM,GAAY,CAAC,EAAI,UAC9C,CACF,CAYA,cAAA,CAAwB,GACtB,MAAM,EAAe,MAAK,EAC1B,MAAK,IACL,MAAM,EAAgB,EAAqB,iBAAiB,aACtD,EAAU,EAAa,mBACvB,IAAoB,KAAkB,GAAS,eAErD,OAAO,EAAQ,IAAK,IAKlB,MAAM,EAAM,EAA0B,UACtC,GAAI,IAAO,MAAK,EAAgB,IAAI,GAAK,CACvC,MAAK,EAAgB,IAAI,GAGL,MAAhB,EAAI,UAAoB,EAAG,aAAa,cAC1C,EAAI,SAA2C,UAAhC,EAAG,aAAa,aAEjC,MAAM,EAAc,EACpB,IAAK,EAAY,iBAAkB,CACjC,MAAM,EAAY,EAAG,cAAc,0BAC/B,IAAW,EAAY,iBAAmB,EAChD,CACF,CAEA,IAAK,EAAI,OAAS,EAAiB,OAAO,EAG1C,IAAI,EAQJ,GALI,IAAe,EAAI,OAAO,eAC5B,EAAmB,EAAa,EAAI,MAAM,eAIvC,GAAoB,GAAS,eAAgB,CAChD,MAAM,EAAa,EAAQ,eAAkB,EAAI,KAAM,EAAa,cAChE,GAAY,eACd,EAAmB,EAAW,aAElC,CAGA,OAAK,EAGE,IACF,EACH,aAAc,IAAK,KAAqB,EAAI,eALhB,GAQlC,CAkBA,WAAA,CAAqB,GAKnB,GAHI,KAAK,OAAO,eAAe,MAAK,EAAkB,IAG1B,IAAxB,MAAK,GAAyB,MAAK,EAAa,OAAO,EAE3D,MAAM,EAAY,MAAK,EACjB,EAAa,MAAK,EAGxB,IAAK,IAAc,EAAY,OAAO,EAEtC,MAAM,EAAS,IAAI,GAGnB,IAAI,GAAW,EACf,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,OAAQ,IACjC,GAAI,MAAK,EAAc,EAAO,MAAQ,EAAW,CAC/C,EAAW,EACX,KACF,CAGF,OAAiB,IAAb,GAIF,WAAA,IAAiB,KAAK,sBAAuB,GACtC,IAIT,EAAO,GAAY,EAGf,MAAK,IAAmB,GAC1B,MAAK,EAAqB,MAAK,EAAgB,GAG1C,EACT,CAQA,WAAA,GACE,MAAM,EAAe,MAAK,EAM1B,IAA4B,IAAxB,MAAK,GAAyB,MAAK,IAAsB,MAAK,GAC5D,EAAa,MAAM,MAAK,KAAoB,MAAK,EAAmB,CACtE,MAAM,EAAY,EAAa,MAAc,QAAQ,MAAK,GAC1D,IAAiB,IAAb,EAKF,YADA,WAAA,IAAiB,KAAK,sBAAuB,GAH7C,MAAK,EAAqB,MAAK,EAAgB,EAMnD,CAUF,GANI,MAAK,IACP,MAAK,GAAuB,EAC5B,MAAK,EAAkB,KAIS,IAA9B,MAAK,EAA6B,CACpC,MAAM,EAAW,MAAK,EACtB,MAAK,GAAuB,EAC5B,EAAa,aAAa,EAAU,SACtC,CAKA,MAAM,EAAgB,MAAK,EAAO,oBAQlC,GAPqB,MAAjB,GACF,KAAK,KAA8B,qBAAsB,CACvD,MAAO,KAKP,MAAK,GAEuB,IAA5B,MAAK,EAAc,KAGvB,IAAK,MAAM,KAAW,MAAK,EAAe,CACxC,MAAO,EAAQ,GAAU,EAAQ,MAAM,KACjC,EAAW,SAAS,EAAQ,IAC5B,EAAW,SAAS,EAAQ,IAE5B,EAAS,MAAK,EAAS,EAAU,GACvC,IAAK,GAAU,EAAO,UAAU,SAAS,WAAY,SAGrD,MAAM,EAAU,EAAa,MAAM,GAC7B,EAAS,EAAa,gBAAgB,GACxC,GAAW,GACb,MAAK,EAAc,EAAS,EAAU,EAAQ,EAAU,GAAQ,EAEpE,CACF,CAOA,eAAA,CAAyB,GACvB,MAAM,IAAE,EAAA,SAAK,EAAA,OAAU,EAAA,SAAQ,EAAA,YAAU,GAAgB,EAEnD,EAAW,MAAK,EAAgB,EAA2B,GAGjE,GAAK,EAUH,EAAY,gBAAgB,sBAN5B,GAHA,EAAY,aAAa,gBAAiB,QAGtC,MAAK,GAAe,EAAY,UAAU,SAAS,WAAY,CACjE,EAAY,UAAU,OAAO,WAC7B,MAAM,EAAQ,EAAc,EAAM,EAA2B,OAC7D,EAAY,YAAuB,MAAT,EAAgB,GAAK,OAAO,EACxD,CAMG,MAAK,GAGL,IAGD,EAAY,UAAU,SAAS,YAInC,MAAK,EAAc,EAAU,EAAU,EAA2B,EAAU,GAAa,EAAM,EAAQ,YACzG,CAeA,cAAA,CAAwB,GACtB,IAAK,KAAK,OAAO,cAAe,OAGhC,MAAM,EADe,MAAK,EACC,WAAW,EAAQ,KAC9C,IAAK,EAAO,OAEZ,MAAM,MAAE,EAAA,iBAAO,EAAA,YAAkB,GAAgB,MAAK,EAAO,iBAAiB,EAAO,EAAQ,KAEvF,EAAK,EAAQ,WASnB,GANA,EAAG,UAAU,OAAO,gBAAiB,GACrC,EAAG,UAAU,OAAO,cAAe,GAK/B,EAAa,CACf,MAAM,EAAQ,EAAG,iBAAiB,qBAClC,IAAK,IAAI,EAAI,EAAG,EAAI,EAAM,OAAQ,IAAK,CACrC,MAAM,EAAO,EAAM,GACb,EAAQ,EAAK,aAAa,cAC5B,GACF,EAAK,UAAU,OAAO,iBAAkB,MAAK,EAAO,YAAY,EAAO,EAAQ,IAAU,GAE7F,CACF,KAAO,CAGL,MAAM,EAAa,EAAG,iBAAiB,mBACvC,IAAK,IAAI,EAAI,EAAG,EAAI,EAAW,OAAQ,IACrC,EAAW,GAAG,UAAU,OAAO,iBAEnC,CACF,CAMA,cAAA,GACE,KAAK,aACP,CAUA,eAAI,GACF,OAAO,MAAK,EAAO,eAAgB,GAAO,KAAK,KAAK,OAAO,GAC7D,CAKA,iBAAI,GACF,OAAO,MAAK,EAAO,kBACrB,CAKA,iBAAI,GACF,OAAO,MAAK,CACd,CAKA,iBAAI,GACF,OAAO,MAAK,CACd,CAKA,YAAA,CAAa,GACX,OAAO,MAAK,IAAmB,CACjC,CAKA,aAAA,CAAc,EAAkB,GAC9B,OAAO,MAAK,EAAc,IAAI,GAAG,KAAY,IAC/C,CAMA,YAAA,CAAa,GACX,MAAM,EAAe,MAAK,EACpB,EAAM,EAAa,MAAM,GAC/B,IAAK,EAAK,OAAO,EACjB,IACE,MAAM,EAAQ,EAAa,WAAW,GACtC,QAAO,GAAQ,MAAK,EAAO,aAAa,EAC1C,CAAA,MACE,OAAO,CACT,CACF,CAMA,gBAAA,CAAiB,GACf,OAAO,MAAK,EAAO,aAAa,EAClC,CAKA,OAAA,CAAQ,GACN,IAAK,KAAK,OAAO,cAAe,OAAO,EACvC,GAAI,MAAK,EAAO,UAAU,IAAI,GAAQ,OAAO,EAC7C,MAAM,EAAM,KAAK,KAAK,OAAO,GAC7B,QAAK,GACE,MAAK,EAAO,WAAW,EAAO,EACvC,CAGA,UAAA,CAAW,GACT,OAAQ,KAAK,QAAQ,EACvB,CAGA,SAAI,GACF,IAAK,KAAK,OAAO,cAAe,OAAO,EACvC,MAAM,EAAe,MAAK,EAC1B,OAAO,MAAK,EAAO,YAAa,GAAU,EAAa,aAAa,IAAQ,IAC9E,CAGA,YAAI,GACF,OAAQ,KAAK,KACf,CAGA,cAAA,CAAe,GACb,IAAK,KAAK,OAAO,cAAe,OAChC,MAAM,EAAM,KAAK,KAAK,OAAO,GACxB,IACL,MAAK,EAAO,aAAa,EAAO,GAEhC,KAAK,KAA2B,eAAgB,CAC9C,QACA,MACA,SAAU,EACV,KAAM,aAEV,CAGA,SAAA,CAAU,GACR,IAAK,KAAK,OAAO,cAAe,OAChC,MAAK,EAAO,QAAQ,GACpB,MAAM,EAAM,KAAK,KAAK,OAAO,GAI7B,KAAK,KAA2B,eAAgB,CAC9C,QACK,MACL,cAAU,EACV,KAAM,OAEV,CAGA,WAAA,CAAY,GACV,IAAK,KAAK,OAAO,cAAe,OAChC,MAAM,EAAM,KAAK,KAAK,OAAO,GACxB,IACL,MAAK,EAAO,UAAU,GACtB,MAAK,EAAiB,EAAO,EAAK,YACpC,CAGA,eAAA,GACE,IAAK,KAAK,OAAO,cAAe,OAChC,MAAM,EAAe,MAAK,EAC1B,MAAK,EAAO,gBAAiB,GAAU,EAAa,aAAa,IAAQ,IAC3E,CAGA,cAAA,CAAe,GACb,GAAK,KAAK,OAAO,cACjB,OAAO,MAAK,EAAO,eAAe,EACpC,CAGA,WAAA,CAAY,GACV,QAAK,KAAK,OAAO,eACV,MAAK,EAAO,YAAY,EACjC,CAGA,YAAA,GACE,IAAK,KAAK,OAAO,cAAe,MAAO,GACvC,MAAM,EAAe,MAAK,EAC1B,OAAO,MAAK,EAAO,aAAc,GAAU,EAAa,aAAa,IAAQ,IAC/E,CAKA,eAAI,GACF,IAAK,KAAK,OAAO,cAAe,MAAO,GACvC,MAAM,EAAe,MAAK,EAC1B,OAAO,MAAK,EAAO,eAAgB,GAAU,EAAa,aAAa,IAAQ,IACjF,CAQA,SAAA,CAAU,GACR,IAAK,KAAK,OAAO,cAAe,OAChC,MAAM,EAAM,KAAK,KAAK,OAAO,GACxB,GACD,MAAK,EAAO,UAAU,EAAO,KAC/B,MAAK,EAAiB,EAAO,EAAK,YAClC,KAAK,gBAAgB,qBAAsB,CAAE,UAC7C,KAAK,gBAET,CAKA,SAAA,GACE,IAAK,KAAK,OAAO,cAAe,OAChC,MAAM,EAAe,MAAK,EAC1B,MAAK,EAAO,UAAW,GAAU,EAAa,aAAa,IAAQ,KACnE,KAAK,gBAAgB,qBAAsB,CAAC,GAC5C,KAAK,eACP,CA2BA,UAAA,CAAW,EAAe,EAAe,EAAU,IACjD,MAAK,EAAY,WAAW,EAAO,EAAO,EAC5C,CAGA,YAAA,CAAa,EAAe,GAC1B,MAAK,EAAY,aAAa,EAAO,EACvC,CAGA,eAAA,CAAgB,GACd,MAAK,EAAY,gBAAgB,EACnC,CAGA,eAAA,GACE,MAAK,EAAY,iBACnB,CAGA,aAAA,CAAc,EAAe,GAC3B,OAAO,MAAK,EAAY,cAAc,EAAO,EAC/C,CAGA,iBAAA,CAAkB,EAAe,GAC/B,OAAO,MAAK,EAAY,kBAAkB,EAAO,EACnD,CAGA,eAAA,CAAgB,GACd,OAAO,MAAK,EAAY,gBAAgB,EAC1C,CAGA,gBAAA,CAAiB,GACf,OAAO,MAAK,EAAY,iBAAiB,EAC3C,CASA,gBAAA,CAAiB,GACf,MAAM,EAAO,KAAK,YACZ,EAAM,KAAK,cACjB,MAAK,EAAO,cAAc,QAC1B,MAAK,EAAO,qBAAqB,QACjC,MAAK,IAEA,GACH,KAAK,KAAgC,qBAAsB,CAAE,OAAM,QAKrE,MAD0B,EACb,UAAU,QAAS,GAAM,EAAE,UAAU,OAAO,WAC3D,CAQA,aAAA,CAAc,EAAkB,GAC9B,MAAM,EAAe,MAAK,EACpB,EAAW,EAAa,gBAAgB,UAAW,GAAM,EAAE,QAAU,GAC3E,IAAiB,IAAb,EAAiB,OAErB,MAAM,EAAS,EAAa,gBAAgB,GACtC,EAAU,EAAa,MAAM,GACnC,IAAK,IAAW,IAAY,MAAK,EAAgB,EAA2B,GAAe,OAE3F,MAAM,EAAS,MAAK,EAAS,EAAU,GAClC,IAEL,MAAK,GAAkB,EACvB,MAAK,EAAe,EAAU,EAAU,GAC1C,CAQA,aAAA,CAAc,GACZ,MAAM,EAAe,MAAK,EAE1B,IAAe,KADA,KAAK,OAAO,QAAU,EAAa,iBAAiB,QAC7C,OAGtB,IAD0B,EAAa,UAAU,KAAM,GAAQ,MAAK,EAAmB,IAC/D,OAExB,MAAM,EAAQ,EAAa,yBAAyB,GACpD,IAAK,EAAO,OAGZ,MAAM,EAAU,EAAa,MAAM,GAC9B,MAAK,EAAe,KAGzB,MAAK,GAAkB,EAGvB,MAAK,EAAc,EAAU,GAG7B,MAAM,KAAK,EAAM,UAAU,QAAA,CAAS,EAAM,KACxC,MAAM,EAAM,EAAa,gBAAgB,GACzC,GAAI,GAAO,MAAK,EAAgB,EAAwB,GAAe,CACrE,MAAM,EAAS,EACV,EAAO,UAAU,SAAS,YAC7B,MAAK,EAAc,EAAS,EAAU,EAAK,EAAG,GAAQ,EAE1D,IAIF,WAAA,KACE,IAAI,EAAa,EAAM,cAAc,mBAAmB,EAAa,eAIrE,GAHK,GAAY,UAAU,SAAS,aAClC,EAAa,EAAM,cAAc,kBAE/B,GAAY,UAAU,SAAS,WAAY,CAC7C,MAAM,EAAU,EAA2B,cAAc,GACzD,IACE,GAAQ,MAAM,CAAE,eAAe,GACjC,CAAA,MAEA,CACF,GACC,GACL,CAMA,mBAAA,IAC8B,IAAxB,MAAK,GACP,MAAK,EAAa,MAAK,GAAgB,EAE3C,CAKA,mBAAA,IAC8B,IAAxB,MAAK,GACP,MAAK,EAAa,MAAK,GAAgB,EAE3C,CAaA,EAAA,CAAc,GACZ,GAAK,EACL,IACE,OAAO,MAAK,EAAc,WAAW,EACvC,CAAA,MACE,MACF,CACF,CAMA,EAAA,CAAS,EAAkB,GAEzB,OADc,MAAK,EAAc,yBAAyB,KAC3C,cAAc,mBAAmB,QAAwC,IAC1F,CAOA,EAAA,CAAe,GACb,IAAK,EAAS,OAAO,EACrB,MAAM,EAAc,MAAK,EAAc,iBAAiB,YACxD,OAAK,IACkC,IAAhC,EAAY,EACrB,CAOA,EAAA,CAAiB,EAAe,EAAoB,GAClD,KAAK,KAA2B,eAAgB,CAC9C,QACK,MACL,SAAU,MAAK,EAAO,eAAe,GACrC,QAEJ,CASA,EAAA,GACE,MAAK,KACL,MAAM,EAAW,SAAS,cACtB,GAAY,KAAK,YAAY,SAAS,KACxC,EAAS,OACT,KAAK,YAAY,SAEnB,MAAK,GAAwB,EAC7B,MAAK,GAAsB,CAC7B,CAOA,GAAA,CAAuB,EAA2B,EAAY,GACvD,IACL,OAAO,KAAK,GAAoB,QAAS,IACvC,EAAqC,GAAM,EAAqC,KAE9E,IACF,MAAK,EAAO,cAAc,OAAO,GACjC,MAAK,EAAO,qBAAqB,OAAO,GACxC,KAAK,gBAAgB,IAEvB,KAAK,gBAAgB,qBAAsB,CAAE,UAC/C,CAMA,EAAA,CAA0B,EAAe,EAAe,GACtD,MAAM,EAAe,MAAK,EACpB,EAAW,EAAa,iBAAiB,UAAW,GAAM,EAAE,QAAU,GAC5E,IAAiB,IAAb,QAAgC,IAAb,EAAwB,OAI/C,MAAM,EADO,EAAa,OACH,UAAW,GAAM,MAAK,EAAc,KAAY,GACvE,IAAiB,IAAb,QAAgC,IAAb,EAAwB,OAE/C,MAAM,EAAS,MAAK,EAAS,EAAU,GACvC,GAAK,EAEL,GAAI,EAAS,CACX,EAAO,aAAa,eAAgB,QAGpC,EAAO,aAAa,eAAgB,QACpC,MAAM,EAAU,MAAK,EAAY,kBAAkB,EAAO,GACtD,IACF,EAAO,aAAa,QAAS,GAG7B,EAAO,gBAAgB,GAE3B,MACE,EAAO,gBAAgB,gBACvB,EAAO,gBAAgB,gBACvB,EAAO,gBAAgB,QAE3B,CASA,EAAA,CAAqB,EAAkB,GACrC,MAAK,EAAiB,EAGtB,MAAM,iBAAgB,IAAI,IACpB,EAAS,GAAG,KAClB,IAAK,MAAM,KAAW,MAAK,EACrB,EAAQ,WAAW,GACrB,EAAc,IAAI,GAAG,KAAY,EAAQ,UAAU,EAAO,WAE1D,EAAc,IAAI,GAGtB,MAAK,EAAc,QACnB,IAAK,MAAM,KAAO,EAChB,MAAK,EAAc,IAAI,GAIzB,MAAM,EAAW,MAAK,EAAkB,IAAI,QAC3B,IAAb,IACF,MAAK,EAAkB,OAAO,GAC9B,MAAK,EAAkB,IAAI,EAAU,IAIvC,MAAM,EAAmD,GACzD,IAAK,MAAO,EAAK,KAAO,MAAK,EACvB,EAAI,WAAW,KACjB,EAAQ,KAAK,CAAC,GAAG,KAAY,EAAI,UAAU,EAAO,UAAW,IAC7D,MAAK,EAAsB,OAAO,IAGtC,IAAK,MAAO,EAAK,KAAO,EACtB,MAAK,EAAsB,IAAI,EAAK,GAItC,MAAK,GACP,CAKA,EAAA,CAAe,EAAkB,EAAkB,GACjD,MAAM,EAAe,MAAK,EACpB,EAAU,EAAa,MAAM,GAC7B,EAAS,EAAa,gBAAgB,GAEvC,GAAY,GAAW,MAAK,EAAgB,EAA2B,KACxE,EAAO,UAAU,SAAS,aAG1B,MAAK,IAAmB,GAC1B,MAAK,EAAc,EAAU,GAG/B,MAAK,EAAiB,EACtB,MAAK,EAAc,EAAS,EAAU,EAAQ,EAAU,GAAQ,IAClE,CASA,GAAA,GACE,MAAM,EAAW,MAAK,EACtB,IAAK,EAAU,OAGf,MAAM,EADe,MAAK,EACG,QAAQ,EAAS,UAC1C,IACF,EAAe,EAAS,EAAS,MAAO,EAAS,OACjD,KAAK,gBAAgB,qBAAsB,CAAE,MAAO,MAAK,EAAc,MAQzE,MAAM,EAAc,GAAG,EAAS,YAAY,EAAS,QAC/C,EAAK,MAAK,EAAsB,IAAI,GAgB1C,GAfI,GAAI,EAAG,EAAS,OAGpB,KAAK,KAAuB,cAAe,CACzC,SAAU,EAAS,SACnB,SAAU,EAAS,SACnB,MAAO,EAAS,MAChB,cAAe,EAAS,QAQtB,EAAS,CACX,MAAM,EAAQ,MAAK,EAAc,GAC7B,IACE,KAAK,OAAO,cAET,MAAK,EAAO,WAAW,EAAO,KACjC,MAAK,EAAO,cAAc,OAAO,GACjC,MAAK,EAAiB,EAAO,EAAc,aAK7C,MAAK,EAAO,cAAc,OAAO,GAGvC,CAKA,KAAK,gBAEL,MAAK,EAAwB,IAC/B,CAMA,EAAA,GACE,MAAM,EAAe,MAAK,EACpB,EAAW,EAAa,UACxB,EAAW,EAAa,UAE9B,GAAI,EAAW,GAAK,EAAW,EAAG,OAElC,MAAM,EAAS,MAAK,EAAS,EAAU,GAEvC,GAAI,GAAQ,UAAU,SAAS,WAAY,CACzC,MAAM,EAAS,EAAO,cAAc,GAChC,IACF,MAAK,GAAsB,EAC3B,EAAO,QACP,MAAK,GAAwB,EAEzB,aAAkB,mBAAqC,SAAhB,EAAO,MAAmC,WAAhB,EAAO,OAC1E,EAAO,SAGb,CACF,CAOA,EAAA,CAAqB,GACnB,MAAM,EAAe,MAAK,EACpB,EAAO,EAAa,MAEpB,EAAa,MAAK,EAAc,EAAa,UAAY,MAAK,EAC9D,EAAiB,EAAK,GAGtB,EAAe,EAAa,gBAC/B,IAAA,CAAK,EAAG,IAAO,GAAkB,MAAK,EAAgB,EAAsB,GAAkB,GAAI,GAClG,OAAQ,GAAM,GAAK,GACtB,GAA4B,IAAxB,EAAa,OAAc,OAG/B,MAAM,EADa,EAAa,QAAQ,EAAa,YACvB,EAAU,GAAI,GAG5C,GAAI,GAAW,GAAK,EAAU,EAAa,OAAQ,CACjD,EAAa,UAAY,EAAa,GACtC,MAAM,EAAS,MAAK,EAAS,EAAY,EAAa,IAMtD,OALI,GAAQ,UAAU,SAAS,YAE7B,EADsB,cAAc,IAC5B,MAAM,CAAE,eAAe,SAEjC,EAAkB,EAAc,CAAE,uBAAuB,GAE3D,CAGA,MAAM,EAAU,GAAc,EAAU,GAAI,GAC5C,GAAI,GAAW,GAAK,EAAU,EAAK,OAAQ,CACzC,MAAM,EAAc,EAAK,GAEnB,EAAmB,EAAa,gBACnC,IAAA,CAAK,EAAG,IAAO,GAAe,MAAK,EAAgB,EAAsB,GAAe,GAAI,GAC5F,OAAQ,GAAM,GAAK,GACtB,GAAgC,IAA5B,EAAiB,OAAc,OAG/B,MAAK,GACP,EAAa,UAAY,EACzB,EAAa,UAAY,EAAU,EAAiB,GAAK,EAAiB,EAAiB,OAAS,GACpG,EAAkB,EAAc,CAAE,uBAAuB,IAEzD,KAAK,qBACL,WAAA,KACE,MAAM,EAAS,MAAK,EAAS,EAAS,EAAa,WAC/C,GAAQ,UAAU,SAAS,YAE7B,EADsB,cAAc,IAC5B,MAAM,CAAE,eAAe,KAEhC,KAGH,MAAK,EAAa,GAAY,GAC9B,EAAa,UAAY,EACzB,EAAa,UAAY,EAAU,EAAiB,GAAK,EAAiB,EAAiB,OAAS,GACpG,KAAK,cAAc,GACnB,EAAkB,EAAc,CAAE,uBAAuB,IAE7D,CAEF,CAKA,EAAA,GACE,MAAM,EAAe,MAAK,EAC1B,EAAa,gBAAkB,MAAK,EACpC,EAAa,kBAAoB,MAAK,CACxC,CAKA,EAAA,CAAc,EAAkB,GAC1B,MAAK,IAAmB,KAEE,IAAxB,MAAK,GACP,MAAK,EAAa,MAAK,GAAgB,GAEzC,MAAK,EAAkB,IAAI,EAAU,IAAK,IAC1C,MAAK,EAAiB,EACtB,MAAK,EAAoB,EAGzB,MAAK,EAAmB,MAAK,EAAc,GAE3C,MAAK,IAMA,MAAK,IACR,KAAK,UAA6B,YAAa,CAC7C,WACA,MAAO,MAAK,GAAoB,GAChC,IAAK,IAEP,EAAS,KAAK,YAAa,EAAe,KAAK,YAAa,iBAAkB,KAGpF,CAKA,EAAA,CAAa,EAAkB,GAC7B,GAAI,MAAK,IAAmB,EAAU,OAEtC,MAAM,EAAe,MAAK,EACpB,EAAW,MAAK,EAAkB,IAAI,GACtC,EAAQ,EAAa,yBAAyB,IAC9C,MAAE,EAAA,QAAO,GAAY,MAAK,GAAkB,IAG7C,GAAU,GAAS,GACtB,MAAK,GAAqB,EAAO,EAAU,GAMxC,GAAW,MAAK,IAAe,GAClC,KAAK,KAA+B,oBAAqB,CACvD,WACA,MAAO,GAAS,GAChB,IAAK,IAKL,GAAU,EACZ,MAAK,GAAuB,EAAO,EAAS,IAClC,GAAU,GACpB,MAAK,GAAmB,EAAU,EAAO,EAAS,GAGpD,MAAK,GAAmB,GAKxB,MAAK,GAAuB,EAGxB,EACF,MAAK,GAAoB,EAAO,IAGhC,MAAK,EAAkB,GACvB,MAAK,GAAuB,IAKzB,MAAK,GAAe,IACvB,KAAK,UAA8B,aAAc,CAC/C,WACA,MAAO,GAAS,GAChB,IAAK,EACL,SAAU,IAEP,GACH,EAAS,KAAK,YAAa,EAAe,KAAK,YAAa,mBAAoB,IAGtF,CAWA,GAAA,CAAkB,GAChB,MAAM,EAAe,MAAK,EAC1B,IAAI,EAAQ,MAAK,EAEjB,MAAM,GADQ,EAAQ,EAAa,aAAa,QAAS,IAClC,KAAO,MAAK,GAAqB,EAAa,MAAM,GAK3E,OAHK,GAAS,IACZ,EAAQ,MAAK,EAAc,IAEtB,CAAE,QAAO,UAClB,CAMA,GAAA,CAAqB,EAAoB,EAAkB,GACzD,MAAM,EAAe,MAAK,EAC1B,EAAM,iBAAiB,iBAAiB,QAAS,IAC/C,MAAM,EAAW,OAAQ,EAAqB,aAAa,aAC3D,GAAI,MAAM,GAAW,OACrB,MAAM,EAAM,EAAa,gBAAgB,GACzC,IAAK,EAAK,OAKV,GAAK,EAAqB,aAAa,uBAAwB,OAE/D,MAAM,EAAQ,EAAK,cAAc,yBAEjC,IAAK,EAAO,OAEZ,MAAM,EAAQ,EAAI,MACZ,EAAgB,EAAc,EAAS,GACvC,EAAM,EAAc,EAAO,EAAK,GAClC,IAAkB,GACpB,MAAK,EAAiB,EAAU,EAAK,EAAK,IAGhD,CAOA,GAAA,CAAmB,EAAkB,EAA2B,EAAY,GAE1E,MAAM,EFnrEV,SAAiC,EAAyB,GACxD,IAAK,EAAU,OAAO,EACtB,MAAM,EAAc,EACd,EAAa,EACb,iBAAU,IAAI,IAAI,IAAI,OAAO,KAAK,MAAiB,OAAO,KAAK,KACrE,IAAK,MAAM,KAAO,EAChB,GAAI,EAAY,KAAS,EAAW,GAAM,OAAO,EAEnD,OAAO,CACT,CE0qE+B,CAAc,EAAU,GAI7C,EAAU,EAAQ,MAAK,EAAO,cAAc,IAAI,GAAS,EAE7C,KAAK,eAAmC,aAAc,CACtE,WACA,MAAO,GAAS,GAChB,IAAK,EACL,SAAU,EACV,SAAU,EACV,UACA,YAAa,KAAK,YAClB,cAAe,KAAK,gBAKhB,GAAU,MAAK,GAAuB,EAAO,EAAS,IAKxD,GAAS,KAAK,OAAO,gBACnB,MAAK,EAAO,WAAW,EAAO,GAChC,MAAK,EAAO,qBAAqB,IAAI,GAErC,MAAK,EAAO,qBAAqB,OAAO,IAIxC,GAAsB,KAAK,qBAG7B,MAAK,EAAuB,GAEhC,CASA,GAAA,CAAmB,GACjB,MAAK,EAAkB,OAAO,GAC9B,MAAK,GAAiB,EACtB,MAAK,OAAmB,EACxB,MAAK,OAAoB,EACzB,MAAK,GAAiB,EACtB,MAAK,GAAkB,EACvB,MAAK,IAEL,IAAK,MAAM,KAAW,MAAK,EACrB,EAAQ,WAAW,GAAG,OACxB,MAAK,EAAc,OAAO,GAG9B,IAAK,MAAM,KAAe,MAAK,EAAsB,OAC/C,EAAY,WAAW,GAAG,OAC5B,MAAK,EAAsB,OAAO,EAGxC,CAMA,GAAA,CAAoB,EAAoB,GAStC,MAAM,EAAU,EAAa,mBACvB,EAAe,EAAM,iBAAiB,iBACxC,GAAS,aACX,EAAa,QAAS,IACpB,MAAM,EAAS,EAAuB,GACtC,EAAQ,YAAa,GACjB,GAAQ,EAAK,YAAY,KAKjC,EAAa,QAAS,IACpB,EAAK,UAAU,OAAO,WFv3E5B,SAAkC,GAChC,EAAM,mBAAqB,EAC3B,EAAM,gBAAgB,mBACxB,CEq3EM,CAAkB,EAAK,iBASzB,EAAa,sBAAqB,EACpC,CAMA,EAAA,CACE,EACA,EACA,EACA,EACA,EAAuB,QAEvB,MAAM,EAAQ,EAAO,MACrB,IAAK,EAAkB,GAAQ,OAC/B,MAAM,EAAW,EAAc,EAAS,GACxC,GAAI,IAAa,EAAU,OAE3B,MAAM,EAAe,MAAK,EAGpB,EAAQ,MAAK,EAAc,GAE3B,GAAY,IAAS,MAAK,EAAO,cAAc,IAAI,GAGnD,EAA2C,EAC5C,GAAY,KAAK,KAAK,UAAU,EAAQ,EAAoC,WAC7E,EAGJ,IAAI,GAAgB,EAGpB,MAAM,EAAa,EACd,IACC,GAAgB,EAChB,KAAK,WAAW,EAAQ,EAAO,GAAW,KAC5C,OAqBE,EAAY,GAAG,GAAS,MAAiB,IAC/C,GAAI,MAAK,EAAiB,IAAI,GAAY,OAE1C,IAAI,GAAY,EAChB,MAAK,EAAiB,IAAI,GAC1B,IACE,EAAY,KAAK,eAAoC,cAAe,CAClE,IAAK,EACL,MAAO,GAAS,GAChB,QACA,WACA,MAAO,EACP,SACA,WACA,YAAa,KAAK,YAClB,cAAe,KAAK,cACpB,gBAAiB,EACjB,YACA,cAEJ,CAAA,QACE,MAAK,EAAiB,OAAO,EAC/B,CAGA,GAAI,EAAW,OAqBf,GAjBI,IAAU,GAAiB,KAAK,cAAc,EAAO,IACvD,KAAK,aAAa,EAAO,GAQ3B,EAAe,EAAS,EAAO,GAC/B,EAAwB,GACpB,GACF,MAAK,EAAO,cAAc,IAAI,GAEhC,MAAK,IAGD,KAAK,OAAO,eAAiB,EAAO,CACtC,MAAM,EAAQ,MAAK,EAAO,WAAW,EAAO,GAC5C,MAAK,EAAiB,EAAO,EAAS,EAAQ,WAAa,WAC7D,CAGA,KAAK,gBAAgB,sBAAuB,CAC1C,WACA,QACA,QACA,WACA,aAIF,MAAM,EAAQ,EAAa,yBAAyB,GAChD,GACF,EAAM,UAAU,IAAI,UAExB,CAMA,EAAA,CACE,EACA,EACA,EACA,EACA,EACA,EACA,GAEA,EAAiB,MAAK,EAAa,EAAS,EAAU,EAAQ,EAAU,EAAM,EAAW,EAC3F,CAKA,EAAA,CAAkB,GAChB,eAAA,KACE,IACE,MAAM,EAAS,EAAa,UACtB,EAAS,EAAa,UACtB,EAAQ,EAAa,yBAAyB,GACpD,GAAI,EAAO,CACT,MAAM,KAAK,EAAa,QAAQ,iBAAiB,gBAAgB,QAAS,GACxE,EAAG,UAAU,OAAO,eAEtB,MAAM,EAAO,EAAM,cAAc,mBAAmB,iBAAsB,OACtE,IACF,EAAK,UAAU,IAAI,cACnB,EAAK,aAAa,gBAAiB,QAC9B,EAAK,aAAa,aAAa,EAAK,aAAa,WAAY,MAClE,EAAK,MAAM,CAAE,eAAe,IAEhC,CACF,CAAA,MAEA,GAEJ"}
1
+ {"version":3,"file":"index.js","names":["FOCUSABLE_EDITOR_SELECTOR","FOCUSABLE_EDITOR_SELECTOR","el","styles","BaseGridPlugin","DEFAULT_A11Y_MESSAGES","fromVirtualScrollTop","invalidateAccessorCache","readCellField","toVirtualScrollTop","writeCellField","GridClasses","GridDataAttrs","announce","gridEl","message","effectiveConfig","a11y","announcements","el","querySelector","textContent","requestAnimationFrame","getA11yMessage","key","args","customFn","messages","ensureCellVisible","grid","options","_virtualization","enabled","rowHeight","container","viewportEl","scrollMapping","scrollEl","visibleHeight","clientHeight","offsets","_getVerticalScrollOffsets","_focusRow","top","bottom","skipScroll","Math","max","usableHeight","y","virtualScrollTop","scrollTop","target","scrollFocusedRowIntoView","isEditing","_activeEditRows","isRowEditing","_isGridEditMode","root","refreshVirtualWindow","_bodyEl","querySelectorAll","forEach","classList","remove","Array","from","setAttribute","focused","rowIndex","vStart","start","vEnd","end","_rows","length","rowEl","cell","children","_focusCol","contains","findFocusedCell","add","forceHorizontalScroll","scrollArea","forceScrollLeft","scrollLeft","forceScrollRight","scrollWidth","clientWidth","_getHorizontalScrollOffsets","left","right","cellRect","getBoundingClientRect","scrollAreaRect","cellLeft","cellRight","width","visibleLeft","visibleRight","scrollCellIntoViewHorizontally","focusTarget","document","activeElement","focus","preventScroll","hasAttribute","active","HTMLElement","closest","applyFocusTarget","EXPR_RE","EMPTY_SENTINEL","SAFE_EXPR","_F","split","map","s","reverse","join","FORBIDDEN","RegExp","slice","Set","safeEval","expr","ctx","toks","tokens","i","ch","push","type","v","Number","test","tri","bi","includes","tokenize","pos","peek","advance","match","val","t","parseTernary","result","parseLogicalAnd","parseLogicalOr","consequent","alternate","parseEquality","parseComparison","parseAdditive","parseMultiplicative","parseUnary","parsePrimary","name","value","startsWith","row","typeDefault","resolveId","evalTemplateString","raw","indexOf","parts","finalStr","replace","_m","res","trim","REFLECTIVE_RE","String","dotChain","out","str","evalSingle","text","allEmpty","every","p","compileTemplate","fn","__blocked","parkCellLoadingOverlay","cellEl","firstElementChild","overlay","parent","parentElement","insertBefore","resolveOptions","column","createDateEditor","params","editorParams","input","createElement","Date","valueAsDate","min","placeholder","addEventListener","date","commit","nullable","fallback","default","getFullYear","getMonth","padStart","getDate","e","cancel","NULLABLE_BLANK_VALUE","optionValues","WeakMap","readMultiSelectValue","select","values","blankSelected","o","selected","has","get","find","opt","defaultEditorFor","step","createNumberEditor","checked","multi","multiple","includeEmpty","emptyOpt","emptyLabel","appendChild","label","set","isArray","next","isSameEditorValue","field","createSelectEditor","maxLength","pattern","inputVal","createTextEditor","readNumericValue","getInputValue","originalValue","HTMLInputElement","HTMLSelectElement","readTextControlValue","toISOString","readDateValue","readTextInputValue","setInputValue","configured","some","isBlankSentinel","writeMultiSelectValue","writeSelectValue","original","size","CellValidationManager","cells","Map","syncAttribute","constructor","this","setInvalid","rowId","rowInvalids","clearInvalid","delete","clearRowInvalid","fields","keys","clearAllInvalid","entries","clear","_","isCellInvalid","getInvalidMessage","hasInvalidCells","getInvalidFields","isRowDirty","baselines","currentRow","baseline","deepEqual","a","b","getTime","aObj","bObj","keysA","Object","keysB","markPristine","structuredClone","revertToBaseline","baselineObj","currentObj","DirtyTrackingManager","baselinesWereCaptured","newRowIds","changedRowIds","committedDirtyRowIds","capture","rows","getRowId","sizeBefore","__loading","id","captureBaselines","drainCapturedFlag","hasAnyDirty","resolveRow","isCellDirty","getRowDirtyState","isNew","isCommittedDirty","hasBaseline","rebaselineCell","markNew","markDirty","markAllPristine","getOriginalRow","getDirtyRows","current","newId","getDirtyRowIds","ids","revertRow","reverted","revertAll","getChangedRows","getChangedRowIds","isRowChanged","FOCUSABLE_EDITOR_SELECTOR","getEditorAncestor","isSafePropertyKey","noopUpdateRow","_changes","shouldPreventEditClose","config","event","onBeforeEditClose","injectEditor","deps","rowData","colIndex","skipFocus","parentRowEl","editable","isGridMode","editingCells","editorValueCallbacks","updateRow","changes","__editingCellCount","incrementEditingCount","editFinalized","newValue","isEditSessionActive","entry","_getRowEntry","currentRowData","currentIndex","index","commitCellValue","editorHost","className","loadingOverlay","__frameworkAdapter","releaseCell","innerHTML","defaultPrevented","ariaTarget","getAttribute","enterTarget","enterEditorAncestor","preventDefault","stopPropagation","exitRowEdit","colInternal","tplHolder","__editorTemplate","editorSpec","col","editor","gridTypeDefaults","typeDefaults","adapter","getTypeDefault","appDefault","_hostElement","resolveEditor","callbackKey","callbacks","newVal","cb","onValueChange","clone","cloneNode","compiledEditor","__compiledEditor","node","childNodes","firstChild","nodeType","Node","TEXT_NODE","g","evt","setTimeout","renderTemplateEditor","isMultiSelect","isDateIntoDateInput","queueMicrotask","produced","wireEditorInputs","HTMLTextAreaElement","hasChildNodes","context","mount","spec","code","gridId","console","warn","pluginName","gridPrefix","toLowerCase","docsUrl","formatDiagnostic","dispatchEvent","CustomEvent","detail","header","control","CSS","escape","nameEditorFromHeader","EditingPlugin","static","ownedProperties","property","level","description","isUsed","events","queries","styles","defaultConfig","mode","editOn","isCellEditable","rowEditable","internalGrid","hasEditableConfig","activeEditRow","activeEditRowId","activeEditRowRef","activeEditCol","rowEditSnapshots","seededFromAttr","WeakSet","pendingFocusRestore","pendingRowAnimation","validation","gridModeInputFocused","gridModeEditLocked","singleCellEdit","gridModeCellSnapshot","dirty","committingCells","editorDeps","attach","super","signal","disconnectSignal","invalid","syncInvalidCellAttribute","ri","revert","syncGridEditState","defineProperty","changedRows","configurable","resetChangedRows","silent","attachRowModeListeners","gridElement","requestRender","attachGridModeListeners","findRenderedRowElement","composedPath","containsFocus","scopeEl","triggers","panel","ownerDocument","getElementById","isInsideOpenAriaOverlay","focusTrap","related","relatedTarget","focusCurrentCellEditor","source","dirtyTracking","handleUndoRedo","action","edits","actions","seen","edit","safeGetRowId","emitDirtyChange","on","markAsNew","requestAfterRender","r","matches","blur","focusRow","focusCol","snap","_visibleColumns","relatedEditor","enterGridModeNavigation","detach","handleQuery","query","columns","byField","handleCommitCellValue","c","_columns","stillDirty","onCellClick","isDoubleClick","originalEvent","isRowEditable","beginBulkEdit","onKeyDown","onEscapeKey","onArrowKey","onTabKey","onSpaceKey","shiftKey","ctrlKey","altKey","metaKey","onEnterKey","onF2Key","arrowEditorAncestor","targetIsEditor","isVertical","handleTabNavigation","onEnterWhileEditorFocused","beginRowEditFromEnter","activeEl","hasEditableColumn","activateEvent","cancelable","bubbles","trigger","beginCellEdit","readTypeParams","typeEl","attr","attributes","letter","toUpperCase","seedTypeEditorsFromLightDom","host","typeElements","filter","tagName","existing","editorTpl","editorTarget","canHandle","createEditor","compiled","processColumns","hasTypeDefaults","__element","internalCol","typeEditorParams","processRows","editRowId","editRowRef","newIndex","cancelActiveRowEdit","migrateEditRowIndex","afterRender","restoreCellFocus","animateRow","capturedCount","emit","count","cellKey","rowStr","colStr","parseInt","getCell","afterCellRender","cellElement","removeAttribute","rowElement","afterRowRender","toggle","dirtyCells","onScrollRender","getRow","isCellEditing","isRowChangedById","isDirty","isPristine","pristine","markAsPristine","markAsDirty","dirtyRowIds","emitPluginEvent","_rowPool","findIndex","startRowEdit","targetCell","commitActiveRowEdit","revertGridModeCellEdit","revertRowFromSnapshot","snapshot","k","oldIndex","migratedCells","prefix","substring","updates","previousValue","forward","editableCols","nextIdx","nextRow","nextRowData","nextEditableCols","_rowEditSnapshots","broadcast","resolveEditedRow","commitActiveEditors","finalizeRowCommit","clearRowEditState","teardownRowEditors","isNaN","changedThisSession","snapshotObj","allKeys","hasRowChanged","changed","emitCancelable","oldValue","isAnimationEnabled","loader","clearEditingState","firstTime","invalidWasSet","commitKey","cancelled","firstTimeForRow","rowIdx","colIdx"],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../../libs/grid/src/lib/core/internal/utils.ts","../../../../../../libs/grid/src/lib/core/internal/keyboard.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/internal/loading.ts","../../../../../../libs/grid/src/lib/plugins/editing/editing.css?inline","../../../../../../libs/grid/src/lib/plugins/editing/editors.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/cell-validation.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/dirty-tracking.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/dirty-tracking-manager.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/helpers.ts","../../../../../../libs/grid/src/lib/plugins/editing/internal/editor-injection.ts","../../../../../../libs/grid/src/lib/plugins/editing/editing-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","// #region Environment Helpers\n\n/**\n * Check if we're running in a development environment.\n * Returns true for localhost or when NODE_ENV !== 'production'.\n * Used to show warnings only in development.\n */\nexport function isDevelopment(): boolean {\n // Check for localhost (browser environment)\n if (typeof window !== 'undefined' && window.location) {\n const hostname = window.location.hostname;\n if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {\n return true;\n }\n }\n // Check for NODE_ENV (build-time or SSR)\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Cell Rendering Helpers\n\n/**\n * Generate accessible HTML for a boolean cell.\n * Uses role=\"checkbox\" with proper aria attributes.\n */\nexport function booleanCellHTML(value: boolean): string {\n return `<span role=\"checkbox\" aria-checked=\"${value}\" aria-label=\"${value}\">${value ? '&#x1F5F9;' : '&#9744;'}</span>`;\n}\n\n/**\n * Format a date value for display.\n * Handles Date objects, timestamps, and date strings.\n * Returns empty string for invalid dates.\n */\nexport function formatDateValue(value: unknown): string {\n if (value == null || value === '') return '';\n if (value instanceof Date) {\n return isNaN(value.getTime()) ? '' : value.toLocaleDateString();\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return isNaN(d.getTime()) ? '' : d.toLocaleDateString();\n }\n return '';\n}\n\n/**\n * Get the row index from a cell element's data-row attribute.\n * Falls back to calculating from parent row's DOM position if data-row is missing.\n * Returns -1 if no valid row index is found.\n */\nexport function getRowIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-row');\n if (attr) return parseInt(attr, 10);\n\n // Fallback: find the parent .data-grid-row and calculate index from siblings\n const rowEl = cell.closest('.data-grid-row');\n if (!rowEl) return -1;\n\n const parent = rowEl.parentElement;\n if (!parent) return -1;\n\n // Get all data-grid-row siblings and find this row's index\n const rows = parent.querySelectorAll(':scope > .data-grid-row');\n for (let i = 0; i < rows.length; i++) {\n if (rows[i] === rowEl) return i;\n }\n return -1;\n}\n\n/**\n * Get the column index from a cell element's data-col attribute.\n * Returns -1 if no valid column index is found.\n */\nexport function getColIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-col');\n return attr ? parseInt(attr, 10) : -1;\n}\n\n/**\n * Clear all cell-focus styling from a root element.\n * Used when changing focus or when selection plugin takes over focus management.\n */\nexport function clearCellFocus(root: Element | null): void {\n if (!root) return;\n root.querySelectorAll('.cell-focus').forEach((el) => el.classList.remove('cell-focus'));\n}\n// #endregion\n\n// #region RTL Helpers\n\n/** Text direction */\nexport type TextDirection = 'ltr' | 'rtl';\n\n/**\n * Get the text direction for an element.\n * Reads from the computed style, which respects the `dir` attribute on the element\n * or any ancestor, as well as CSS `direction` property.\n *\n * @param element - The element to check direction for\n * @returns 'ltr' or 'rtl'\n *\n * @example\n * ```typescript\n * // Detect grid's direction\n * const dir = getDirection(gridElement);\n * if (dir === 'rtl') {\n * // Handle RTL layout\n * }\n * ```\n */\nexport function getDirection(element: Element): TextDirection {\n // Try computed style first (works in real browsers)\n try {\n const computedDir = getComputedStyle(element).direction;\n if (computedDir === 'rtl') return 'rtl';\n } catch {\n // getComputedStyle may fail in some test environments\n }\n\n // Fallback: check dir attribute on element or ancestors\n // This handles test environments where getComputedStyle may not reflect dir attribute\n try {\n const dirAttr = element.closest?.('[dir]')?.getAttribute('dir');\n if (dirAttr === 'rtl') return 'rtl';\n } catch {\n // closest may not be available on mock elements\n }\n\n return 'ltr';\n}\n\n/**\n * Check if an element is in RTL mode.\n *\n * @param element - The element to check\n * @returns true if the element's text direction is right-to-left\n */\nexport function isRTL(element: Element): boolean {\n return getDirection(element) === 'rtl';\n}\n\n/**\n * Resolve a logical inline position to a physical position based on text direction.\n *\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n * - `'left'` / `'right'` → unchanged (physical values)\n *\n * @param position - Logical or physical position\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical position ('left' or 'right')\n *\n * @example\n * ```typescript\n * resolveInlinePosition('start', 'ltr'); // 'left'\n * resolveInlinePosition('start', 'rtl'); // 'right'\n * resolveInlinePosition('left', 'rtl'); // 'left' (unchanged)\n * ```\n */\nexport function resolveInlinePosition(\n position: 'left' | 'right' | 'start' | 'end',\n direction: TextDirection,\n): 'left' | 'right' {\n if (position === 'left' || position === 'right') {\n return position;\n }\n if (direction === 'rtl') {\n return position === 'start' ? 'right' : 'left';\n }\n return position === 'start' ? 'left' : 'right';\n}\n// #endregion\n","/**\n * Central keyboard handler attached to the host element. Manages navigation, paging,\n * and edit lifecycle triggers while respecting active form field interactions.\n */\nimport { FOCUSABLE_EDITOR_SELECTOR, GridClasses } from '../constants';\nimport type { GridHost } from '../types';\nimport { clearCellFocus, isRTL } from './utils';\nimport { readCellField } from './value-accessor';\nimport { fromVirtualScrollTop, getRowIndexAtOffset, toVirtualScrollTop } from './virtualization';\n\n/** Commit active row edit if the editing plugin provides this method. */\nfunction tryCommitEdit(grid: GridHost): void {\n if (typeof grid.commitActiveRowEdit === 'function') grid.commitActiveRowEdit();\n}\n\nfunction isFormField(el: HTMLElement | null): boolean {\n if (!el) return false;\n const tag = el.tagName;\n if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return true;\n if (el.isContentEditable) return true;\n return false;\n}\n\n/** True while a row-scoped edit session is active. */\nfunction isRowEditing(grid: GridHost): boolean {\n return grid._activeEditRows !== undefined && grid._activeEditRows !== -1;\n}\n\n// #region Keyboard Handler\n/**\n * Guard chain deciding whether this keydown belongs to something other than\n * cell navigation and must be left alone.\n */\nfunction shouldIgnoreKeyDown(grid: GridHost, e: KeyboardEvent, target: HTMLElement | null): boolean {\n // Keyboard events whose target lives inside the grid host but OUTSIDE the\n // rows body (toolpanel inputs/buttons, shell-header controls, light-DOM\n // children of <tbw-grid>) own their own keyboard interactions. Treating\n // ArrowUp/ArrowDown/Enter on such a target as cell navigation steals the\n // user's actual interaction target on the very next render cycle.\n // Cells and the grid host itself remain valid navigation targets.\n if (target && target !== grid && !target.closest?.('.rows-body')) return true;\n\n if (isFormField(target)) {\n switch (e.key) {\n // Home/End and horizontal arrows navigate within the text input; Enter\n // and Escape are handled by the input's own handlers first.\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n case 'Enter':\n case 'Escape':\n return true;\n case 'ArrowUp':\n case 'ArrowDown':\n // Number spinners own the vertical arrows; other fields fall through.\n if ((target as HTMLInputElement).tagName === 'INPUT' && (target as HTMLInputElement).type === 'number') {\n return true;\n }\n break;\n }\n }\n\n const colType = grid._visibleColumns[grid._focusCol]?.type;\n return isRowEditing(grid) && colType === 'select' && (e.key === 'ArrowDown' || e.key === 'ArrowUp');\n}\n\n/** Tab / Shift+Tab: advance one cell, wrapping to the next/previous row. */\nfunction navigateTab(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number): void {\n e.preventDefault();\n if (!e.shiftKey) {\n if (grid._focusCol < maxCol) {\n grid._focusCol += 1;\n return;\n }\n tryCommitEdit(grid);\n if (grid._focusRow < maxRow) {\n grid._focusRow += 1;\n grid._focusCol = 0;\n }\n return;\n }\n if (grid._focusCol > 0) {\n grid._focusCol -= 1;\n } else if (grid._focusRow > 0) {\n if (grid._activeEditRows === grid._focusRow) tryCommitEdit(grid);\n grid._focusRow -= 1;\n grid._focusCol = maxCol;\n }\n}\n\n/**\n * ArrowLeft/ArrowRight. `towardEnd` is the LOGICAL direction (ArrowRight); in\n * RTL the physical arrow maps to the opposite column index.\n */\nfunction navigateHorizontal(grid: GridHost, e: KeyboardEvent, maxCol: number, towardEnd: boolean): void {\n const forward = isRTL(grid) ? !towardEnd : towardEnd;\n grid._focusCol = forward ? Math.min(maxCol, grid._focusCol + 1) : Math.max(0, grid._focusCol - 1);\n e.preventDefault();\n}\n\n/** Commit any active row edit, then focus the first/last row (column unchanged). */\nfunction jumpToRowEdge(grid: GridHost, maxRow: number, toEnd: boolean): void {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = toEnd ? maxRow : 0;\n}\n\n/** ArrowUp/ArrowDown: one row, or the first/last row with Ctrl/Cmd. */\nfunction navigateVertical(grid: GridHost, e: KeyboardEvent, maxRow: number, down: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, down);\n else {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = down ? Math.min(maxRow, grid._focusRow + 1) : Math.max(0, grid._focusRow - 1);\n }\n e.preventDefault();\n}\n\n/**\n * PageUp/PageDown target: one viewport of rows (less any overlay band), so an\n * unconstrained grid pages straight to its edge. 20 rows until layout exists.\n */\nfunction pageTargetRow(grid: GridHost, maxRow: number, down: boolean): number {\n const from = grid._focusRow;\n const virt = grid._virtualization;\n const viewportH = virt?.viewportEl?.clientHeight ?? virt?.container?.clientHeight ?? 0;\n const offsets = grid._getVerticalScrollOffsets?.(from);\n const usable = viewportH - Math.max(0, offsets?.top ?? 0) - Math.max(0, offsets?.bottom ?? 0);\n const pc = virt?.variableHeights ? virt.positionCache : null;\n let target: number;\n if (usable > 0 && pc?.[from]) {\n target = getRowIndexAtOffset(pc, pc[from].offset + (down ? usable : -usable));\n // A row taller than the viewport would otherwise pin focus in place.\n if (target === from) target = from + (down ? 1 : -1);\n } else {\n const rowH = virt?.rowHeight ?? 0;\n const step = usable > 0 && rowH > 0 ? Math.max(1, Math.floor(usable / rowH)) : 20;\n target = from + (down ? step : -step);\n }\n return Math.max(0, Math.min(maxRow, target));\n}\n\n/** Home/End (plus Ctrl/Cmd variants which also jump to the first/last row). */\nfunction navigateRowEdge(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number, toEnd: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, toEnd);\n grid._focusCol = toEnd ? maxCol : 0;\n e.preventDefault();\n ensureCellVisible(grid, toEnd ? { forceScrollRight: true } : { forceScrollLeft: true });\n}\n\n/** Dispatch the unified `cell-activate` event. Returns true when cancelled. */\nfunction emitCellActivate(grid: GridHost, e: KeyboardEvent): boolean {\n const rowIndex = grid._focusRow;\n const colIndex = grid._focusCol;\n const column = grid._visibleColumns[colIndex];\n const row = grid._rows[rowIndex];\n const field = column?.field ?? '';\n const activateEvent = new CustomEvent('cell-activate', {\n cancelable: true,\n detail: {\n rowIndex,\n colIndex,\n column,\n field,\n value: field && row ? readCellField(row, field) : undefined,\n row,\n cellEl: grid.querySelector(`[data-row=\"${rowIndex}\"][data-col=\"${colIndex}\"]`) as HTMLElement | undefined,\n trigger: 'keyboard' as const,\n originalEvent: e,\n },\n });\n grid.dispatchEvent(activateEvent);\n return activateEvent.defaultPrevented;\n}\n\nexport function handleGridKeyDown(grid: GridHost, e: KeyboardEvent): void {\n // Dispatch to plugin system first - if any plugin handles it, stop here\n if (grid._dispatchKeyDown?.(e)) return;\n\n const path = e.composedPath?.() ?? [];\n const target = (path.length ? path[0] : e.target) as HTMLElement | null;\n if (shouldIgnoreKeyDown(grid, e, target)) return;\n\n const maxRow = grid._rows.length - 1;\n const maxCol = grid._visibleColumns.length - 1;\n\n switch (e.key) {\n case 'Tab':\n navigateTab(grid, e, maxRow, maxCol);\n break;\n case 'ArrowDown':\n navigateVertical(grid, e, maxRow, true);\n break;\n case 'ArrowUp':\n navigateVertical(grid, e, maxRow, false);\n break;\n case 'ArrowRight':\n navigateHorizontal(grid, e, maxCol, true);\n break;\n case 'ArrowLeft':\n navigateHorizontal(grid, e, maxCol, false);\n break;\n case 'Home':\n navigateRowEdge(grid, e, maxRow, maxCol, false);\n return;\n case 'End':\n navigateRowEdge(grid, e, maxRow, maxCol, true);\n return;\n case 'PageDown':\n grid._focusRow = pageTargetRow(grid, maxRow, true);\n e.preventDefault();\n break;\n case 'PageUp':\n grid._focusRow = pageTargetRow(grid, maxRow, false);\n e.preventDefault();\n break;\n // NOTE: Enter is normally handled by EditingPlugin. If no plugin handled\n // it, dispatch the unified cell-activate event for custom handling.\n case 'Enter':\n if (emitCellActivate(grid, e)) {\n e.preventDefault();\n return;\n }\n break;\n default:\n return;\n }\n ensureCellVisible(grid);\n}\n// #endregion\n\n// #region Cell Visibility\n/**\n * Options for ensureCellVisible to control scroll behavior.\n */\ninterface EnsureCellVisibleOptions {\n /** Force scroll to the leftmost position (for Home key) */\n forceScrollLeft?: boolean;\n /** Force scroll to the rightmost position (for End key) */\n forceScrollRight?: boolean;\n /** Force horizontal scroll even in edit mode (for Tab navigation) */\n forceHorizontalScroll?: boolean;\n}\n\n/**\n * Vertical: scroll the virtualized container so the focused row is in view.\n *\n * `y` is in raw row-content space (rowHeight × index). Above the browser height\n * cap (#326), `scrollEl.scrollTop` is in clamped spacer space — the two\n * coordinate systems are not interchangeable, so translate scrollTop into\n * virtual space for the comparison and back into native space when writing.\n * For sub-cap datasets the mapping is identity, so this is a no-op.\n *\n * Plugins that paint over the viewport (sticky rows) report the space they\n * obscure via `_getVerticalScrollOffsets`, so the row lands clear of the\n * overlay instead of underneath it — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n */\nfunction scrollFocusedRowIntoView(grid: GridHost): void {\n if (!grid._virtualization?.enabled) return;\n const { rowHeight, container, viewportEl, scrollMapping } = grid._virtualization;\n // container is the faux scrollbar element that handles actual scrolling;\n // viewportEl is the visible area element that has the correct height.\n const scrollEl = container as HTMLElement | undefined;\n const visibleHeight = viewportEl?.clientHeight ?? scrollEl?.clientHeight ?? 0;\n if (!scrollEl || visibleHeight <= 0) return;\n\n const offsets = grid._getVerticalScrollOffsets?.(grid._focusRow) ?? { top: 0, bottom: 0 };\n if (offsets.skipScroll) return;\n // A malformed or oversized report must never invert the visible band.\n const top = Math.max(0, offsets.top);\n const bottom = Math.max(0, offsets.bottom);\n const usableHeight = visibleHeight - top - bottom;\n if (usableHeight <= 0) return;\n\n const y = grid._focusRow * rowHeight;\n const virtualScrollTop = scrollMapping ? toVirtualScrollTop(scrollEl.scrollTop, scrollMapping) : scrollEl.scrollTop;\n if (y < virtualScrollTop + top) {\n const target = Math.max(0, y - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n } else if (y + rowHeight > virtualScrollTop + top + usableHeight) {\n const target = Math.max(0, y - usableHeight + rowHeight - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n }\n}\n\n/**\n * Resolve the rendered cell for the focused row/col, or `undefined` when the\n * focused row is outside the current virtual window.\n */\nfunction findFocusedCell(grid: GridHost): { rowEl: HTMLElement | null; cell: HTMLElement } | undefined {\n const rowIndex = grid._focusRow;\n const vStart = grid._virtualization.start ?? 0;\n const vEnd = grid._virtualization.end ?? grid._rows.length;\n if (rowIndex < vStart || rowIndex >= vEnd) return undefined;\n\n const rowEl = grid._bodyEl.querySelectorAll('.data-grid-row')[rowIndex - vStart] as HTMLElement | null;\n // Try exact column match first, then query by data-col, then fall back to the\n // first cell (full-width group rows).\n let cell = rowEl?.children[grid._focusCol] as HTMLElement | undefined;\n if (!cell || !cell.classList?.contains('cell')) {\n cell = (rowEl?.querySelector(`.cell[data-col=\"${grid._focusCol}\"]`) ?? rowEl?.querySelector('.cell[data-col]')) as\n HTMLElement | undefined;\n }\n return cell ? { rowEl: rowEl ?? null, cell } : undefined;\n}\n\n/** Horizontal: bring the focused cell into the `.tbw-scroll-area` viewport. */\nfunction scrollCellIntoViewHorizontally(\n grid: GridHost,\n rowEl: HTMLElement | null,\n cell: HTMLElement,\n options?: EnsureCellVisibleOptions,\n): void {\n const scrollArea = grid.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (!scrollArea) return;\n\n // Home/End always scroll to the edge.\n if (options?.forceScrollLeft) {\n scrollArea.scrollLeft = 0;\n return;\n }\n if (options?.forceScrollRight) {\n scrollArea.scrollLeft = scrollArea.scrollWidth - scrollArea.clientWidth;\n return;\n }\n\n // Plugins (e.g. pinned columns) report how much of the scroll area they\n // obscure, and whether the focused cell should skip scrolling entirely\n // (pinned cells are always visible).\n const offsets = grid._getHorizontalScrollOffsets?.(rowEl ?? undefined, cell) ?? { left: 0, right: 0 };\n if (offsets.skipScroll) return;\n\n const cellRect = cell.getBoundingClientRect();\n const scrollAreaRect = scrollArea.getBoundingClientRect();\n const cellLeft = cellRect.left - scrollAreaRect.left + scrollArea.scrollLeft;\n const cellRight = cellLeft + cellRect.width;\n const visibleLeft = scrollArea.scrollLeft + offsets.left;\n const visibleRight = scrollArea.scrollLeft + scrollArea.clientWidth - offsets.right;\n if (cellLeft < visibleLeft) {\n scrollArea.scrollLeft = cellLeft - offsets.left;\n } else if (cellRight > visibleRight) {\n scrollArea.scrollLeft = cellRight - scrollArea.clientWidth + offsets.right;\n }\n}\n\n/** Move real DOM focus to the editor, the cell, or the grid host. */\nfunction applyFocusTarget(grid: GridHost, cell: HTMLElement, isEditing: boolean): void {\n if (isEditing && cell.classList.contains(GridClasses.EDITING)) {\n // Editing cell: focus the editor input inside it\n const focusTarget = cell.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (focusTarget && document.activeElement !== focusTarget) {\n try {\n focusTarget.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n }\n return;\n }\n\n if (isEditing) {\n // Active edit row but this cell isn't the editing cell — focus it so Tab\n // navigation within the row can attach editors.\n if (cell.contains(document.activeElement)) return;\n if (!cell.hasAttribute('tabindex')) cell.setAttribute('tabindex', '-1');\n try {\n cell.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n return;\n }\n\n // NOT editing: keep focus on the grid element (tabindex=0) rather than\n // individual cells. In a virtualized grid, cells can be detached by\n // subsequent render cycles (e.g., SelectionPlugin's requestAfterRender\n // → RAF → row recycling). A detached focused cell causes activeElement\n // to revert to <body>, breaking keyboard navigation.\n // Visual focus is managed by the .cell-focus CSS class + data-has-focus.\n //\n // BUT: don't steal focus from a real focused descendant (toolpanel\n // input, shell-header button, registered overlay control). Cell focus\n // is virtual; if the user is actively typing in a non-cell input, we\n // must leave that input alone. We only reclaim focus when nothing\n // meaningful inside the grid currently holds it (active element is\n // outside grid, on the grid host itself, or on a bare cell).\n const active = document.activeElement;\n const meaningful =\n active instanceof HTMLElement &&\n active !== grid &&\n typeof grid.contains === 'function' &&\n grid.contains(active) &&\n !active.classList.contains('cell') &&\n !active.closest('.cell');\n if (!meaningful && active !== grid) {\n grid.focus({ preventScroll: true });\n }\n}\n\n/**\n * Scroll the viewport (virtualized or static) so the focused cell's row is visible\n * and apply visual focus styling / tabindex management.\n */\nexport function ensureCellVisible(grid: GridHost, options?: EnsureCellVisibleOptions): void {\n scrollFocusedRowIntoView(grid);\n\n const isEditing = isRowEditing(grid) || !!grid._isGridEditMode;\n // Skip refreshVirtualWindow when in edit mode to avoid wiping editors\n if (!isEditing) grid.refreshVirtualWindow(false);\n\n clearCellFocus(grid._bodyEl);\n // Clear previous aria-selected markers\n Array.from(grid._bodyEl.querySelectorAll('[aria-selected=\"true\"]')).forEach((el) => {\n el.setAttribute('aria-selected', 'false');\n });\n\n const focused = findFocusedCell(grid);\n if (!focused) return;\n const { rowEl, cell } = focused;\n\n cell.classList.add('cell-focus');\n cell.setAttribute('aria-selected', 'true');\n\n // Skip horizontal scrolling in edit mode to prevent scroll jumps when editors\n // are created — unless forceHorizontalScroll is set (Tab while editing).\n if (!isEditing || options?.forceHorizontalScroll) {\n scrollCellIntoViewHorizontally(grid, rowEl, cell, options);\n }\n\n applyFocusTarget(grid, cell, isEditing);\n}\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Loading State Module\n *\n * Handles loading overlays, row loading, and cell loading states.\n * Provides DOM manipulation helpers for loading indicators.\n *\n * @module internal/loading\n */\n\nimport type { FrameworkAdapter, GridConfig, LoadingContext, RowElementInternal } from '../types';\nimport { setSanitizedHTML } from './sanitize';\n\n/**\n * Create the default spinner element.\n * @param size - 'large' for grid overlay, 'small' for row/cell\n */\nexport function createDefaultSpinner(size: 'large' | 'small'): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = `tbw-spinner tbw-spinner--${size}`;\n spinner.setAttribute('role', 'progressbar');\n spinner.setAttribute('aria-label', 'Loading');\n return spinner;\n}\n\n/**\n * Create loading content using custom renderer or default spinner.\n *\n * Strings returned by a custom renderer are passed through `setSanitizedHTML()`\n * — mirroring the cell, header and empty-state render paths — so a message\n * interpolating server data (e.g. `Loading ${dataset.name}…`) is not an XSS\n * sink.\n *\n * @param size - 'large' for grid overlay, 'small' for row/cell\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingContent(size: 'large' | 'small', renderer?: GridConfig['loadingRenderer']): HTMLElement {\n if (renderer) {\n const context: LoadingContext = { size };\n const result = renderer(context);\n if (typeof result === 'string') {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, result);\n return wrapper;\n }\n return result;\n }\n\n return createDefaultSpinner(size);\n}\n\n/**\n * Create or update the loading overlay element.\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingOverlay(renderer?: GridConfig['loadingRenderer']): HTMLElement {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-loading-overlay';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n overlay.appendChild(createLoadingContent('large', renderer));\n return overlay;\n}\n\n/**\n * Show the loading overlay on the grid root element.\n * @param gridRoot - The .tbw-grid-root element\n * @param overlayEl - The overlay element (will be cached by caller)\n */\nexport function showLoadingOverlay(gridRoot: Element, overlayEl: HTMLElement): void {\n gridRoot.appendChild(overlayEl);\n}\n\n/**\n * Hide the loading overlay.\n * @param overlayEl - The overlay element to remove\n */\nexport function hideLoadingOverlay(overlayEl: HTMLElement | undefined): void {\n overlayEl?.remove();\n}\n\n/** Remove a cell's owned loading overlay and release adapter-managed content. */\nexport function removeCellLoadingOverlay(cellEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!cellEl.firstElementChild) return;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Move a cell's owned loading overlay in front of the cell while its content is rewritten,\n * so the still-active loader survives adapter release and content replacement. The caller\n * appends it back. The overlay stays connected because React prunes detached portal containers.\n */\nexport function parkCellLoadingOverlay(cellEl: HTMLElement): HTMLElement | null {\n if (!cellEl.firstElementChild) return null;\n const overlay = cellEl.querySelector<HTMLElement>(':scope > .tbw-cell-loading-overlay');\n if (!overlay) return null;\n const parent = cellEl.parentElement;\n if (parent) parent.insertBefore(overlay, cellEl);\n else overlay.remove();\n return overlay;\n}\n\n/** Remove a row's owned loading overlay and release adapter-managed content. */\nexport function removeRowLoadingOverlay(rowEl: HTMLElement, adapter?: Pick<FrameworkAdapter, 'unmount'>): void {\n if (!rowEl.firstElementChild) return;\n const overlay = rowEl.querySelector<HTMLElement>(':scope > .tbw-row-loading-overlay');\n if (!overlay) return;\n adapter?.unmount?.(overlay);\n overlay.remove();\n}\n\n/**\n * Update a row element's loading state.\n * Uses real DOM elements instead of ::before/::after pseudo-elements\n * because the selection plugin already uses ::after on rows for border styling.\n * @param rowEl - The row element\n * @param loading - Whether the row is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * in place of the default spinner. Only invoked when the overlay is created.\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setRowLoadingState(\n rowEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n rowEl.classList.add('tbw-row-loading');\n rowEl.setAttribute('aria-busy', 'true');\n\n // Create overlay + spinner DOM elements if not already present.\n // Direct children only — a nested grid's row overlay must not be mistaken for this row's.\n if (!rowEl.querySelector(':scope > .tbw-row-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-row-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n\n if (renderer) {\n overlay.appendChild(createLoadingContent('small', renderer));\n } else {\n const spinner = document.createElement('div');\n spinner.className = 'tbw-row-loading-spinner';\n overlay.appendChild(spinner);\n }\n\n rowEl.appendChild(overlay);\n }\n } else {\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n // Remove overlay + spinner DOM elements\n removeRowLoadingOverlay(rowEl, adapter);\n }\n}\n\n/**\n * Update a cell element's loading state.\n * @param cellEl - The cell element\n * @param loading - Whether the cell is loading\n * @param renderer - Optional `gridConfig.loadingRenderer`; called with `{ size: 'small' }`\n * @param adapter - Framework adapter used to release renderer-owned content on removal.\n */\nexport function setCellLoadingState(\n cellEl: HTMLElement,\n loading: boolean,\n renderer?: GridConfig['loadingRenderer'],\n adapter?: Pick<FrameworkAdapter, 'unmount'>,\n): void {\n if (loading) {\n cellEl.classList.add('tbw-cell-loading');\n cellEl.setAttribute('aria-busy', 'true');\n\n // Breadcrumb so the render hot path can skip the per-cell scan on the\n // overwhelming majority of rows. Conservative: only ever set here, so it can\n // read stale-true after the last loading cell clears (costs one extra scan).\n const rowEl = cellEl.parentElement as RowElementInternal | null;\n if (rowEl) rowEl.__hasCellLoading = true;\n\n if (renderer) {\n cellEl.classList.add('tbw-cell-loading-custom');\n if (!cellEl.querySelector(':scope > .tbw-cell-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-cell-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n overlay.appendChild(createLoadingContent('small', renderer));\n cellEl.appendChild(overlay);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading-custom');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n } else {\n cellEl.classList.remove('tbw-cell-loading');\n cellEl.classList.remove('tbw-cell-loading-custom');\n cellEl.removeAttribute('aria-busy');\n removeCellLoadingOverlay(cellEl, adapter);\n }\n}\n","/**\n * Editing Plugin Styles\n *\n * These styles are injected into the document when the EditingPlugin is loaded.\n * CSS custom properties use fallbacks to core grid variables for consistency.\n * Uses CSS nesting for scoping to tbw-grid elements.\n */\n\n@layer tbw-plugins {\n /* ============================================================================\n CSS Custom Properties for Editing\n ============================================================================\n These are defined here so they can be overridden by consumers.\n They reference core grid variables with sensible defaults.\n ============================================================================ */\n tbw-grid {\n /* Editing cell/row backgrounds */\n --tbw-editing-bg: var(--tbw-color-selection);\n --tbw-editing-row-bg: var(--tbw-editing-bg);\n\n /* Editing input styling */\n --tbw-editing-border: var(--tbw-border-input, 1px solid var(--tbw-color-border-strong));\n --tbw-padding-editing-input: var(--tbw-cell-padding-input, 2px 6px);\n --tbw-font-size-editor: inherit;\n\n /* Row outline when editing */\n --tbw-editing-row-outline-color: var(--tbw-color-accent);\n --tbw-editing-row-outline-width: 1px;\n\n /* ============================================================================\n Row Editing State\n ============================================================================ */\n\n /* Highlight entire row when any cell is being edited (only in row mode, not grid mode) */\n &:not(.tbw-grid-mode) .data-grid-row:has(.editing) {\n background: var(--tbw-editing-row-bg);\n outline: var(--tbw-editing-row-outline-width) solid var(--tbw-editing-row-outline-color);\n outline-offset: calc(-1 * var(--tbw-editing-row-outline-width));\n }\n\n /* ============================================================================\n Cell Editing State\n ============================================================================ */\n\n /* Cell in edit mode */\n .data-grid-row > .cell.editing {\n overflow: hidden;\n padding: 0;\n /* Override block to flex for editing inputs */\n display: flex;\n /* Match the non-editing cell height exactly. Non-editing height is:\n max(--tbw-row-height [border-box], line-height + padding + border [content-driven])\n Since editing cells have padding:0, min-height must compensate for both cases:\n - Content-driven: line-height (1lh) + original vertical padding + border\n - Min-height-driven: just var(--tbw-row-height) (border-box already includes padding+border) */\n min-height: max(\n var(--tbw-row-height),\n calc(1lh + 2 * var(--tbw-cell-padding-v, 0.25em) + var(--tbw-border-width, 1px))\n );\n align-items: center;\n justify-content: center;\n }\n\n /* Standard form inputs inside editing cells */\n .data-grid-row > .cell.editing input:not([type='checkbox']),\n .data-grid-row > .cell.editing select,\n .data-grid-row > .cell.editing textarea {\n width: 100%;\n height: 100%;\n flex: 1 1 auto;\n align-self: stretch;\n min-width: 0;\n border: var(--tbw-editing-border);\n padding: var(--tbw-padding-editing-input);\n font-size: var(--tbw-font-size-editor);\n }\n\n /* ============================================================================\n Editor Host\n ============================================================================ */\n\n /* Editor host wrapper uses display:contents to let editor fill cell */\n .tbw-editor-host {\n display: contents;\n }\n\n /* ============================================================================\n Changed Row Indicator\n ============================================================================ */\n\n /* Visual indicator for rows that have been modified */\n .data-grid-row.changed {\n /* Could add a left border or background tint here */\n }\n\n /* ============================================================================\n Invalid Cell Indicator\n ============================================================================ */\n\n /* Default invalid cell color - can be overridden by consumers */\n --tbw-invalid-bg: light-dark(#fef2f2, #450a0a);\n --tbw-invalid-border-color: light-dark(#ef4444, #f87171);\n\n /* Visual indicator for cells marked as invalid */\n .data-grid-row > .cell[data-invalid='true'] {\n background: var(--tbw-invalid-bg);\n outline: 1px solid var(--tbw-invalid-border-color);\n outline-offset: -1px;\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Default Editors Module\n *\n * Provides built-in editor factories for different column types.\n * Each editor type has its own factory function for consistency and readability.\n *\n * IMPORTANT: Editor factories should NOT call focus() on elements - they are called\n * before the element is appended to the DOM. The calling code (beginBulkEdit,\n * inlineEnterEdit) is responsible for focusing the correct editor after insertion.\n */\n\nimport { readCellField } from '../../core/internal/value-accessor';\nimport type { ColumnConfig, ColumnEditorContext } from '../../core/types';\nimport type { DateEditorParams, NumberEditorParams, SelectEditorParams, TextEditorParams } from './types';\n\n// ============================================================================\n// Type Aliases\n// ============================================================================\n\n/** Option shape used by select editor (matches column.options) */\ntype ColumnOption = { label: string; value: unknown };\n\n/** Column with any row type (used for editor factories) */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyColumn = ColumnConfig<any>;\n\n// ============================================================================\n// Utilities\n// ============================================================================\n\n/** Resolve column.options (handles both array and function forms) */\nfunction resolveOptions(column: AnyColumn): ColumnOption[] {\n const raw = column.options;\n if (!raw) return [];\n return typeof raw === 'function' ? raw() : raw;\n}\n\n/** Format a Date as YYYY-MM-DD string */\nfunction toISODate(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\n// ============================================================================\n// Editor Factories\n// ============================================================================\n\n/** Creates a number input editor */\nfunction createNumberEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as NumberEditorParams | undefined;\n const input = document.createElement('input');\n input.type = 'number';\n input.value = ctx.value != null ? String(ctx.value) : '';\n\n if (params?.min !== undefined) input.min = String(params.min);\n if (params?.max !== undefined) input.max = String(params.max);\n if (params?.step !== undefined) input.step = String(params.step);\n if (params?.placeholder) input.placeholder = params.placeholder;\n\n const commit = () => {\n if (input.value === '') {\n if (column.nullable) {\n ctx.commit(null);\n } else {\n // Non-nullable: fall back to min value or 0\n ctx.commit(params?.min ?? 0);\n }\n } else {\n ctx.commit(Number(input.value));\n }\n };\n input.addEventListener('blur', commit);\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') commit();\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return input;\n };\n}\n\n/** Creates a checkbox editor for boolean values */\nfunction createBooleanEditor(): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const input = document.createElement('input');\n input.type = 'checkbox';\n input.checked = !!ctx.value;\n input.addEventListener('change', () => ctx.commit(input.checked));\n return input;\n };\n}\n\n/** Creates a date input editor */\nfunction createDateEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as DateEditorParams | undefined;\n const input = document.createElement('input');\n input.type = 'date';\n\n // Set initial value - handle both Date objects and string dates\n if (ctx.value instanceof Date) {\n input.valueAsDate = ctx.value;\n } else if (typeof ctx.value === 'string' && ctx.value) {\n // String date like \"2019-10-09\" - set directly as value\n input.value = ctx.value.split('T')[0]; // Handle ISO strings too\n }\n if (params?.min) input.min = params.min;\n if (params?.max) input.max = params.max;\n if (params?.placeholder) input.placeholder = params.placeholder;\n\n // Commit function preserves original type (string vs Date)\n const commit = () => {\n if (!input.value) {\n if (column.nullable) {\n ctx.commit(null);\n } else {\n // Non-nullable: use configured default or today\n const fallback = params?.default;\n if (typeof ctx.value === 'string' || typeof fallback === 'string') {\n // String mode: use default string or today as YYYY-MM-DD\n ctx.commit(typeof fallback === 'string' ? fallback : toISODate(fallback ?? new Date()));\n } else {\n // Date object mode\n ctx.commit(fallback instanceof Date ? fallback : new Date());\n }\n }\n return;\n }\n if (typeof ctx.value === 'string') {\n // Original was string, return string in YYYY-MM-DD format\n ctx.commit(input.value);\n } else {\n ctx.commit(input.valueAsDate);\n }\n };\n\n input.addEventListener('change', commit);\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return input;\n };\n}\n\n/** Sentinel value used internally to identify the nullable \"(Blank)\" option */\nconst NULLABLE_BLANK_VALUE = '__tbw_null__';\n\n/** Configured value behind each built-in select `<option>` — string matching would conflate `1` and `'1'`. */\nconst optionValues = new WeakMap<HTMLOptionElement, unknown>();\n\n/**\n * Read a `multiple` select as an array of the configured option values, preserving\n * their original type (DOM option values are always strings). For nullable columns the\n * blank sentinel maps to `null` when it is the only selection and is dropped otherwise.\n */\nfunction readMultiSelectValue(select: HTMLSelectElement, column?: AnyColumn): unknown {\n let options: ReturnType<typeof resolveOptions> | undefined;\n const values: unknown[] = [];\n let blankSelected = false;\n // Filter `options` rather than read `selectedOptions`: happy-dom caches that collection\n // and misses programmatic `option.selected` writes (e.g. setInputValue's external push).\n for (const o of Array.from(select.options)) {\n if (!o.selected) continue;\n if (optionValues.has(o)) {\n values.push(optionValues.get(o));\n continue;\n }\n if (column?.nullable && o.value === NULLABLE_BLANK_VALUE) {\n blankSelected = true;\n continue;\n }\n // Fallback for selects not built by createSelectEditor (custom editors).\n options ??= column ? resolveOptions(column) : [];\n const match = options.find((opt) => String(opt.value) === o.value);\n values.push(match ? match.value : o.value);\n }\n return blankSelected && values.length === 0 ? null : values;\n}\n\n/** Creates a select dropdown editor */\nfunction createSelectEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as SelectEditorParams | undefined;\n const select = document.createElement('select');\n if (column.multi) select.multiple = true;\n\n // Add blank option: nullable columns always get one, otherwise respect includeEmpty\n const showBlank = column.nullable || params?.includeEmpty;\n if (showBlank) {\n const emptyOpt = document.createElement('option');\n emptyOpt.value = column.nullable ? NULLABLE_BLANK_VALUE : '';\n emptyOpt.textContent = column.nullable ? (params?.emptyLabel ?? '(Blank)') : (params?.emptyLabel ?? '');\n if (ctx.value == null) emptyOpt.selected = true;\n select.appendChild(emptyOpt);\n }\n\n // Populate options from column.options\n const options = resolveOptions(column);\n options.forEach((opt) => {\n const o = document.createElement('option');\n o.value = String(opt.value);\n o.textContent = opt.label;\n optionValues.set(o, opt.value);\n if (column.multi && Array.isArray(ctx.value) && ctx.value.includes(opt.value)) {\n o.selected = true;\n } else if (!column.multi && ctx.value === opt.value) {\n o.selected = true;\n }\n select.appendChild(o);\n });\n\n const commit = () => {\n if (column.multi) {\n const next = readMultiSelectValue(select, column);\n // Compare with the row, not a local cache: a vetoed commit leaves it unchanged, so blur retries.\n if (isSameEditorValue(readCellField(ctx.row, ctx.field), next)) return;\n ctx.commit(next);\n } else if (column.nullable && select.value === NULLABLE_BLANK_VALUE) {\n ctx.commit(null);\n } else {\n ctx.commit(select.value);\n }\n };\n\n select.addEventListener('change', commit);\n select.addEventListener('blur', commit);\n select.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return select;\n };\n}\n\n/** Creates a text input editor (default) */\nfunction createTextEditor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement {\n return (ctx) => {\n const params = column.editorParams as TextEditorParams | undefined;\n const input = document.createElement('input');\n input.type = 'text';\n input.value = ctx.value != null ? String(ctx.value) : '';\n\n if (params?.maxLength !== undefined) input.maxLength = params.maxLength;\n if (params?.pattern) input.pattern = params.pattern;\n if (params?.placeholder) input.placeholder = params.placeholder;\n\n // Commit function preserves original type when possible\n const commit = () => {\n const inputVal = input.value;\n\n // Empty input handling\n if (inputVal === '') {\n if (column.nullable) {\n ctx.commit(null);\n } else {\n // Non-nullable: commit empty string so the field is never null\n ctx.commit('');\n }\n return;\n }\n\n // Preserve values with characters that <input> can't represent (newlines, etc.).\n // If stripping those characters produces the same string, the user didn't change anything.\n if (typeof ctx.value === 'string' && inputVal === ctx.value.replace(/[\\n\\r]/g, '')) {\n return;\n }\n // Preserve numeric type for custom column types (e.g., currency)\n if (typeof ctx.value === 'number') {\n ctx.commit(Number(inputVal));\n } else {\n ctx.commit(inputVal);\n }\n };\n\n input.addEventListener('blur', commit);\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') commit();\n if (e.key === 'Escape') ctx.cancel();\n });\n\n return input;\n };\n}\n\n// ============================================================================\n// Main Entry Point\n// ============================================================================\n\n/**\n * Returns a default editor factory function for the given column type.\n * Each editor handles commit on blur/Enter, and cancel on Escape.\n *\n * Note: Focus is NOT called here - the calling code handles focusing after DOM insertion.\n * @since 0.4.0\n */\nexport function defaultEditorFor(column: AnyColumn): (ctx: ColumnEditorContext) => HTMLElement | string {\n switch (column.type) {\n case 'number':\n return createNumberEditor(column);\n case 'boolean':\n return createBooleanEditor();\n case 'date':\n return createDateEditor(column);\n case 'select':\n return createSelectEditor(column);\n default:\n return createTextEditor(column);\n }\n}\n\n// ============================================================================\n// Utility Export (used by EditingPlugin)\n// ============================================================================\n\n/**\n * Resolve a numeric editor value. Empty input falls back to `null` for nullable\n * columns, otherwise to the configured `min` (or `0`).\n */\nfunction readNumericValue(raw: string, column?: AnyColumn): unknown {\n if (raw === '') {\n if (column?.nullable) return null;\n const params = column?.editorParams as NumberEditorParams | undefined;\n return params?.min ?? 0;\n }\n return Number(raw);\n}\n\n/**\n * Resolve a `<input type=\"date\">` value, preserving the original value's type\n * (string dates stay `YYYY-MM-DD` strings, Date objects stay `Date`).\n */\nfunction readDateValue(input: HTMLInputElement, column?: AnyColumn, originalValue?: unknown): unknown {\n if (!input.value) {\n if (column?.nullable) return null;\n // Non-nullable: preserve original or fall back to today\n if (typeof originalValue === 'string') return originalValue || new Date().toISOString().slice(0, 10);\n return (originalValue as Date) ?? new Date();\n }\n // Preserve original type: if original was a string, return string (YYYY-MM-DD format)\n if (typeof originalValue === 'string') {\n return input.value; // input.value is already in YYYY-MM-DD format\n }\n return input.valueAsDate;\n}\n\n/**\n * Resolve a plain text `<input>` value, preserving numeric originals and values\n * containing characters `<input>` cannot represent.\n */\nfunction readTextInputValue(input: HTMLInputElement, column?: AnyColumn, originalValue?: unknown): unknown {\n // For text inputs, check if original value was a number to preserve type\n if (typeof originalValue === 'number') return readNumericValue(input.value, column);\n // Nullable text: empty → null; non-nullable: empty → ''\n if (input.value === '' && (originalValue === null || originalValue === undefined)) {\n return column?.nullable ? null : '';\n }\n // Preserve values with characters <input> can't represent (newlines, etc.)\n if (typeof originalValue === 'string' && input.value === originalValue.replace(/[\\n\\r]/g, '')) {\n return originalValue;\n }\n return input.value;\n}\n\n/**\n * Resolve a `<textarea>` / `<select>` value using the column type or the\n * original value's type.\n */\nfunction readTextControlValue(\n input: HTMLTextAreaElement | HTMLSelectElement,\n column?: AnyColumn,\n originalValue?: unknown,\n): unknown {\n // `input.value` on a multi-select is only the FIRST selected option — read the full set.\n if (input instanceof HTMLSelectElement && input.multiple) return readMultiSelectValue(input, column);\n // Nullable select's \"(Blank)\" option carries an internal sentinel — map it back to null\n // (before the numeric branch, which would turn it into NaN). Mirrors createSelectEditor's commit.\n if (input instanceof HTMLSelectElement && column?.nullable && input.value === NULLABLE_BLANK_VALUE) {\n return null;\n }\n // Check column type OR original value type — preserves numeric type for\n // custom column types (e.g., currency).\n if (input.value !== '' && (column?.type === 'number' || typeof originalValue === 'number')) {\n return Number(input.value);\n }\n // Nullable: empty → null; non-nullable: empty → ''\n if ((originalValue === null || originalValue === undefined) && input.value === '') {\n return column?.nullable ? null : '';\n }\n return input.value;\n}\n\n/**\n * Get the typed value from an input element based on its type, column config, and original value.\n * Preserves the type of the original value (e.g., numeric currency values stay as numbers,\n * string dates stay as strings, null/undefined for empty fields).\n */\nexport function getInputValue(\n input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,\n column?: AnyColumn,\n originalValue?: unknown,\n): unknown {\n if (!(input instanceof HTMLInputElement)) return readTextControlValue(input, column, originalValue);\n switch (input.type) {\n case 'checkbox':\n return input.checked;\n case 'number':\n return readNumericValue(input.value, column);\n case 'date':\n return readDateValue(input, column, originalValue);\n default:\n return readTextInputValue(input, column, originalValue);\n }\n}\n\n/**\n * Write a value into a native editor — the inverse of {@link getInputValue}. Used to push\n * external row updates (`updateRow`, cascades, reverts) into an open editor, where a blind\n * `input.value = String(value)` would corrupt it: a multi-select matches no option for\n * `'a,c'` and drops its whole selection, a date input rejects `Date.toString()`, and a\n * nullable select has no `''` option for `null`. Pass the column so the nullable blank\n * sentinel is only honoured where {@link getInputValue} decodes it.\n */\nexport function setInputValue(\n input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,\n value: unknown,\n column?: AnyColumn,\n): void {\n if (input instanceof HTMLSelectElement) {\n writeSelectValue(input, value, !!column?.nullable);\n } else if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.checked = !!value;\n } else if (input instanceof HTMLInputElement && input.type === 'date' && value instanceof Date) {\n input.valueAsDate = value;\n } else if (input instanceof HTMLInputElement && input.type === 'date' && typeof value === 'string') {\n input.value = value.split('T')[0];\n } else {\n input.value = String(value ?? '');\n }\n}\n\n/**\n * Whether `o` is the nullable \"(Blank)\" option built by `createSelectEditor`. Like the readers,\n * only on nullable columns: elsewhere a `'__tbw_null__'` option is an ordinary value.\n */\nfunction isBlankSentinel(o: HTMLOptionElement, nullable: boolean): boolean {\n return nullable && o.value === NULLABLE_BLANK_VALUE && !optionValues.has(o);\n}\n\nfunction writeSelectValue(select: HTMLSelectElement, value: unknown, nullable: boolean): void {\n if (select.multiple) {\n writeMultiSelectValue(select, value, nullable);\n } else if (value == null && Array.from(select.options).some((o) => isBlankSentinel(o, nullable))) {\n select.value = NULLABLE_BLANK_VALUE;\n } else {\n select.value = String(value ?? '');\n }\n}\n\n/**\n * Select the options of a `multiple` select that match `value` (an array, a lone value, or\n * `null` → the blank sentinel on nullable columns). Mirrors {@link readMultiSelectValue}: configured options\n * match their original value strictly, others fall back to string matching.\n */\nfunction writeMultiSelectValue(select: HTMLSelectElement, value: unknown, nullable: boolean): void {\n const values = Array.isArray(value) ? value : value == null ? [] : [value];\n for (const o of Array.from(select.options)) {\n if (optionValues.has(o)) {\n const configured = optionValues.get(o);\n o.selected = values.some((v) => v === configured);\n } else if (isBlankSentinel(o, nullable)) {\n o.selected = value == null;\n } else {\n o.selected = values.some((v) => String(v) === o.value);\n }\n }\n}\n\n/**\n * Whether a value re-read from an editor equals the cell's original value. Arrays\n * (multi-select) compare as sets, since the DOM reports selections in option order\n * and every read yields a new array instance.\n */\nexport function isSameEditorValue(originalValue: unknown, value: unknown): boolean {\n if (Array.isArray(originalValue) && Array.isArray(value)) {\n const original = new Set(originalValue);\n const next = new Set(value);\n return original.size === next.size && [...next].every((v) => original.has(v));\n }\n return originalValue === value;\n}\n","/**\n * Cell Validation Manager\n *\n * Manages invalid-cell state for the Editing Plugin.\n * Extracted from EditingPlugin to reduce the main plugin file size.\n *\n * The manager owns the `Map<rowId, Map<field, message>>` state and exposes\n * read/write methods. DOM synchronization is handled via an injected callback.\n *\n * @internal\n */\n\n/** Callback to sync the data-invalid attribute on a cell element in the DOM. */\nexport type SyncInvalidAttributeFn = (rowId: string, field: string, invalid: boolean) => void;\n\n/**\n * Manages validation state for grid cells.\n *\n * Tracks which cells are marked invalid and their validation messages.\n * The DOM side-effect (adding/removing `data-invalid` attribute) is delegated\n * to the `syncAttribute` callback provided at construction time.\n */\nexport class CellValidationManager {\n /** Invalid cell tracking: Map<rowId, Map<field, message>> */\n readonly #cells = new Map<string, Map<string, string>>();\n\n /** Callback to sync DOM attributes when validation state changes */\n readonly #syncAttribute: SyncInvalidAttributeFn;\n\n constructor(syncAttribute: SyncInvalidAttributeFn) {\n this.#syncAttribute = syncAttribute;\n }\n\n // #region Write Operations\n\n /**\n * Mark a cell as invalid with an optional validation message.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @param message - Optional validation message (for tooltips or display)\n */\n setInvalid(rowId: string, field: string, message = ''): void {\n let rowInvalids = this.#cells.get(rowId);\n if (!rowInvalids) {\n rowInvalids = new Map();\n this.#cells.set(rowId, rowInvalids);\n }\n rowInvalids.set(field, message);\n this.#syncAttribute(rowId, field, true);\n }\n\n /**\n * Clear the invalid state for a specific cell.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n */\n clearInvalid(rowId: string, field: string): void {\n const rowInvalids = this.#cells.get(rowId);\n if (rowInvalids) {\n rowInvalids.delete(field);\n if (rowInvalids.size === 0) {\n this.#cells.delete(rowId);\n }\n }\n this.#syncAttribute(rowId, field, false);\n }\n\n /**\n * Clear all invalid cells for a specific row.\n *\n * @param rowId - The row ID (from getRowId)\n */\n clearRowInvalid(rowId: string): void {\n const rowInvalids = this.#cells.get(rowId);\n if (rowInvalids) {\n const fields = Array.from(rowInvalids.keys());\n this.#cells.delete(rowId);\n fields.forEach((field) => this.#syncAttribute(rowId, field, false));\n }\n }\n\n /**\n * Clear all invalid cell states across all rows.\n */\n clearAllInvalid(): void {\n const entries = Array.from(this.#cells.entries());\n this.#cells.clear();\n entries.forEach(([rowId, fields]) => {\n fields.forEach((_, field) => this.#syncAttribute(rowId, field, false));\n });\n }\n\n // #endregion\n\n // #region Read Operations\n\n /**\n * Check if a specific cell is marked as invalid.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @returns True if the cell is marked as invalid\n */\n isCellInvalid(rowId: string, field: string): boolean {\n return this.#cells.get(rowId)?.has(field) ?? false;\n }\n\n /**\n * Get the validation message for an invalid cell.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @returns The validation message, or undefined if cell is valid\n */\n getInvalidMessage(rowId: string, field: string): string | undefined {\n return this.#cells.get(rowId)?.get(field);\n }\n\n /**\n * Check if a row has any invalid cells.\n *\n * @param rowId - The row ID (from getRowId)\n * @returns True if the row has at least one invalid cell\n */\n hasInvalidCells(rowId: string): boolean {\n const rowInvalids = this.#cells.get(rowId);\n return rowInvalids ? rowInvalids.size > 0 : false;\n }\n\n /**\n * Get all invalid fields for a row.\n *\n * @param rowId - The row ID (from getRowId)\n * @returns Map of field names to validation messages\n */\n getInvalidFields(rowId: string): Map<string, string> {\n return new Map(this.#cells.get(rowId) ?? []);\n }\n\n // #endregion\n}\n","/**\n * Pure functions for dirty tracking baseline management.\n *\n * Extracted from EditingPlugin to keep the plugin under the 2,000 line target.\n * All functions are stateless — the caller owns the baseline `Map`.\n *\n * @internal\n */\n\nimport { readCellField } from '../../../core/internal/value-accessor';\n\n// #region Types\n\n/**\n * Detail for `dirty-change` custom events.\n * @since 1.23.0\n */\nexport interface DirtyChangeDetail<T = unknown> {\n /** Row ID (from getRowId) */\n rowId: string;\n /** Current row data */\n row: T;\n /** Baseline (original) row data, or undefined for newly inserted rows */\n original: T | undefined;\n /**\n * Transition type:\n * - `'modified'` — row differs from baseline\n * - `'new'` — row was inserted via `insertRow()` and has no baseline\n * - `'reverted'` — row was reverted to baseline via `revertRow()`\n * - `'pristine'` — row was explicitly marked pristine via `markAsPristine()`\n */\n type: 'modified' | 'new' | 'reverted' | 'pristine';\n}\n\n/**\n * Result of getDirtyRows(): each entry has the row ID, original (baseline),\n * and current data.\n * @since 1.23.0\n */\nexport interface DirtyRowEntry<T = unknown> {\n id: string;\n /** The original (baseline) row data, or `undefined` for newly inserted rows. */\n original: T | undefined;\n current: T;\n}\n\n/**\n * Detail for `baselines-captured` custom events.\n *\n * Emitted after the render pipeline completes when new baseline snapshots\n * were captured during `processRows`.\n * @since 1.23.0\n */\nexport interface BaselinesCapturedDetail {\n /** Total number of tracked baselines (not just newly captured). */\n count: number;\n}\n\n// #endregion\n\n// #region Baseline Capture\n\n/**\n * Capture baselines for rows not already tracked (first-write-wins).\n *\n * Uses `structuredClone` for deep copy so nested objects cannot be mutated\n * through shared references.\n *\n * @param baselines - Map of rowId → deep-cloned baseline row data\n * @param rows - Rows to snapshot\n * @param getRowId - Function to resolve row ID (may throw; exceptions are swallowed)\n */\nexport function captureBaselines<T>(\n baselines: Map<string, T>,\n rows: readonly T[],\n getRowId: (row: T) => string | undefined,\n): void {\n for (const row of rows) {\n // Loading placeholders (ServerSide blocks in flight) have a stable internal id but\n // hold no data — snapshotting them would retain one clone per unloaded slot.\n if ((row as { __loading?: unknown } | null)?.__loading === true) continue;\n try {\n const id = getRowId(row);\n if (id != null && !baselines.has(id)) {\n baselines.set(id, structuredClone(row));\n }\n } catch {\n // Row has no resolvable ID — skip\n }\n }\n}\n\n// #endregion\n\n// #region Dirty Detection\n\n/**\n * Check whether a row's current data differs from its baseline.\n *\n * Uses deep property comparison so that `structuredClone`'d baselines\n * with nested objects/arrays/Dates compare correctly.\n */\nexport function isRowDirty<T>(baselines: Map<string, T>, rowId: string, currentRow: T): boolean {\n const baseline = baselines.get(rowId);\n if (!baseline) return false; // No baseline → row is \"new\" or untracked\n return !deepEqual(baseline, currentRow);\n}\n\n/**\n * Check whether a single cell (field) differs from its baseline value.\n *\n * Returns `false` when no baseline exists for the row.\n */\nexport function isCellDirty<T>(baselines: Map<string, T>, rowId: string, currentRow: T, field: string): boolean {\n const baseline = baselines.get(rowId);\n if (!baseline) return false;\n const baselineValue = readCellField(baseline, field);\n const currentValue = readCellField(currentRow, field);\n return !deepEqual(baselineValue, currentValue);\n}\n\n/**\n * Deep comparison of two values. Handles primitives, plain objects, arrays,\n * and Dates — the value types produced by `structuredClone` on row data.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return false;\n if (typeof a !== typeof b) return false;\n\n // Date comparison\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();\n\n // Array comparison\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n\n // Object comparison\n if (typeof a === 'object') {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const keysA = Object.keys(aObj);\n const keysB = Object.keys(bObj);\n if (keysA.length !== keysB.length) return false;\n for (const key of keysA) {\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n// #endregion\n\n// #region State Transitions\n\n/**\n * Mark a row as pristine: re-snapshot baseline from current data.\n *\n * After calling this, `isRowDirty` returns `false` for the row (until it\n * is edited again).\n */\nexport function markPristine<T>(baselines: Map<string, T>, rowId: string, currentRow: T): void {\n baselines.set(rowId, structuredClone(currentRow));\n}\n\n/**\n * Get the original (baseline) row data as a deep clone.\n *\n * Returns `undefined` if no baseline exists (e.g. newly inserted row).\n */\nexport function getOriginalRow<T>(baselines: Map<string, T>, rowId: string): T | undefined {\n const baseline = baselines.get(rowId);\n return baseline ? structuredClone(baseline) : undefined;\n}\n\n/**\n * Revert a row to its baseline values by mutating the current row in-place.\n *\n * Returns `true` if a baseline existed and the row was reverted, `false` otherwise.\n */\nexport function revertToBaseline<T>(baselines: Map<string, T>, rowId: string, currentRow: T): boolean {\n const baseline = baselines.get(rowId);\n if (!baseline) return false;\n const baselineObj = baseline as Record<string, unknown>;\n const currentObj = currentRow as Record<string, unknown>;\n for (const key of Object.keys(baselineObj)) {\n currentObj[key] = baselineObj[key];\n }\n return true;\n}\n\n// #endregion\n","/**\n * Stateful manager for dirty tracking in the EditingPlugin.\n *\n * Owns all dirty-tracking-related state (baselines, changed/new/committed sets)\n * and exposes high-level methods that coordinate pure functions from\n * `dirty-tracking.ts`.\n *\n * The EditingPlugin delegates all dirty tracking operations to this manager,\n * keeping the plugin class focused on orchestration (row resolution, event\n * emission, render scheduling).\n *\n * @internal\n */\n\nimport { writeCellField } from '../../../core/internal/value-accessor';\nimport {\n captureBaselines,\n getOriginalRow,\n isCellDirty,\n isRowDirty,\n markPristine,\n revertToBaseline,\n type DirtyRowEntry,\n} from './dirty-tracking';\n\n// #region Types\n\n/**\n * Callback that resolves a row object by its ID.\n * Returns `undefined` when the row is not found.\n */\nexport type RowResolver<T> = (rowId: string) => T | undefined;\n\n// #endregion\n\n// #region DirtyTrackingManager\n\nexport class DirtyTrackingManager<T> {\n // --- Owned state ---\n\n /** Baseline snapshots: rowId → deep-cloned original row data (first-write-wins) */\n readonly baselines = new Map<string, T>();\n\n /** Whether new baselines were captured during the current processRows cycle */\n private baselinesWereCaptured = false;\n\n /** Row IDs inserted via `insertRow()` (no baseline available) */\n readonly newRowIds = new Set<string>();\n\n /** Row IDs that have been modified (edit committed) */\n readonly changedRowIds = new Set<string>();\n\n /** Row IDs whose edit session was committed (gates `tbw-row-dirty` CSS class) */\n readonly committedDirtyRowIds = new Set<string>();\n\n // --- Lifecycle ---\n\n /** Reset all dirty tracking state (called from detach / resetChangedRows). */\n clear(): void {\n this.changedRowIds.clear();\n this.committedDirtyRowIds.clear();\n this.baselines.clear();\n this.newRowIds.clear();\n this.baselinesWereCaptured = false;\n }\n\n // --- Baseline capture (called from processRows) ---\n\n /**\n * Capture baselines for rows not already tracked (first-write-wins).\n * Sets the `baselinesWereCaptured` flag when new rows are snapshotted.\n * @returns whether any new baseline was captured\n */\n capture(rows: readonly T[], getRowId: (r: T) => string | undefined): boolean {\n const sizeBefore = this.baselines.size;\n captureBaselines(this.baselines, rows, getRowId);\n if (this.baselines.size > sizeBefore) {\n this.baselinesWereCaptured = true;\n return true;\n }\n return false;\n }\n\n /**\n * Drain the baselines-captured flag (called from afterRender).\n * Returns `null` when no new captures occurred, or the baseline count.\n */\n drainCapturedFlag(): number | null {\n if (!this.baselinesWereCaptured) return null;\n this.baselinesWereCaptured = false;\n return this.baselines.size;\n }\n\n // --- Row dirty queries ---\n\n /** Check if a specific row differs from its baseline. */\n isRowDirty(rowId: string, row: T): boolean {\n return isRowDirty(this.baselines, rowId, row);\n }\n\n /** Check if ANY row is dirty (requires row resolver for baseline iteration). */\n hasAnyDirty(resolveRow: RowResolver<T>): boolean {\n if (this.newRowIds.size > 0) return true;\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row && isRowDirty(this.baselines, rowId, row)) return true;\n }\n return false;\n }\n\n // --- Cell dirty queries ---\n\n /** Check if a single cell differs from its baseline value. */\n isCellDirty(rowId: string, row: T, field: string): boolean {\n return isCellDirty(this.baselines, rowId, row, field);\n }\n\n // --- Render-time helpers ---\n\n /**\n * Get the dirty-state tuple needed by afterRowRender to toggle CSS classes.\n *\n * `isCommittedDirty` requires BOTH committed status AND actual data divergence\n * from baseline (handles undo: after CTRL+Z, row is no longer visually dirty).\n */\n getRowDirtyState(rowId: string, row: T): { isNew: boolean; isCommittedDirty: boolean; hasBaseline: boolean } {\n const isNew = this.newRowIds.has(rowId);\n const isCommittedDirty = !isNew && this.committedDirtyRowIds.has(rowId) && isRowDirty(this.baselines, rowId, row);\n return { isNew, isCommittedDirty, hasBaseline: this.baselines.has(rowId) };\n }\n\n // --- Mark operations ---\n\n /** Re-snapshot baseline from current data and remove from all sets. */\n markPristine(rowId: string, row: T): void {\n markPristine(this.baselines, rowId, row);\n this.newRowIds.delete(rowId);\n this.changedRowIds.delete(rowId);\n this.committedDirtyRowIds.delete(rowId);\n }\n\n /**\n * Update a single field's baseline to an externally-synced value.\n *\n * Used for `source: 'sync'` (declarative host data replacement): the incoming\n * value becomes the new pristine truth for that cell without touching other\n * fields' baselines or the row's dirty membership sets. No-op when the row has\n * no captured baseline (an untracked row is not dirty anyway).\n */\n rebaselineCell(rowId: string, field: string, value: unknown): void {\n const baseline = this.baselines.get(rowId);\n if (baseline) writeCellField(baseline, field, value);\n }\n\n /** Mark a row as newly inserted (no baseline). */\n markNew(rowId: string): void {\n this.newRowIds.add(rowId);\n this.committedDirtyRowIds.add(rowId);\n }\n\n /** Mark a row as dirty (external mutation). */\n markDirty(rowId: string): void {\n this.changedRowIds.add(rowId);\n this.committedDirtyRowIds.add(rowId);\n }\n\n /** Re-snapshot all baselines and clear all tracking sets. */\n markAllPristine(resolveRow: RowResolver<T>): void {\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row) markPristine(this.baselines, rowId, row);\n }\n this.newRowIds.clear();\n this.changedRowIds.clear();\n this.committedDirtyRowIds.clear();\n }\n\n // --- Original row access ---\n\n /** Get deep-cloned baseline (undefined for new/untracked rows). */\n getOriginalRow(rowId: string): T | undefined {\n return getOriginalRow<T>(this.baselines, rowId);\n }\n\n /** Lightweight check whether a baseline exists (no cloning). */\n hasBaseline(rowId: string): boolean {\n return this.baselines.has(rowId);\n }\n\n // --- Aggregate queries ---\n\n /** Get all dirty rows with original + current data. Requires a row resolver. */\n getDirtyRows(resolveRow: RowResolver<T>): DirtyRowEntry<T>[] {\n const result: DirtyRowEntry<T>[] = [];\n for (const [rowId, baseline] of this.baselines) {\n const row = resolveRow(rowId);\n if (row && isRowDirty(this.baselines, rowId, row)) {\n result.push({ id: rowId, original: structuredClone(baseline), current: row });\n }\n }\n for (const newId of this.newRowIds) {\n const row = resolveRow(newId);\n if (row) {\n result.push({ id: newId, original: undefined, current: row });\n }\n }\n return result;\n }\n\n /** Get IDs of all dirty rows. Requires a row resolver. */\n getDirtyRowIds(resolveRow: RowResolver<T>): string[] {\n const ids: string[] = [];\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row && isRowDirty(this.baselines, rowId, row)) ids.push(rowId);\n }\n for (const newId of this.newRowIds) ids.push(newId);\n return ids;\n }\n\n // --- Revert operations ---\n\n /**\n * Revert a row to its baseline values (mutates row in-place).\n * Returns `true` when baseline existed and row was reverted.\n */\n revertRow(rowId: string, row: T): boolean {\n const reverted = revertToBaseline(this.baselines, rowId, row);\n if (reverted) {\n this.changedRowIds.delete(rowId);\n this.committedDirtyRowIds.delete(rowId);\n }\n return reverted;\n }\n\n /** Revert all rows to baseline values. Requires a row resolver. */\n revertAll(resolveRow: RowResolver<T>): void {\n for (const [rowId] of this.baselines) {\n const row = resolveRow(rowId);\n if (row) revertToBaseline(this.baselines, rowId, row);\n }\n this.changedRowIds.clear();\n this.committedDirtyRowIds.clear();\n }\n\n // --- Changed row queries ---\n\n /** Resolve changed row IDs to row objects. */\n getChangedRows(resolveRow: RowResolver<T>): T[] {\n const rows: T[] = [];\n for (const id of this.changedRowIds) {\n const row = resolveRow(id);\n if (row) rows.push(row);\n }\n return rows;\n }\n\n /** Get a copy of changed row IDs. */\n getChangedRowIds(): string[] {\n return Array.from(this.changedRowIds);\n }\n\n /** Check if a row has been marked as changed. */\n isRowChanged(rowId: string): boolean {\n return this.changedRowIds.has(rowId);\n }\n}\n\n// #endregion\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","/**\n * Pure helper functions for the Editing Plugin.\n *\n * Extracted from EditingPlugin to reduce the main plugin file size.\n * All functions are stateless — they have no `this` access.\n *\n * @internal\n */\n\nimport type {\n ColumnConfig,\n ColumnEditorSpec,\n ColumnInternal,\n InternalGrid,\n RowElementInternal,\n} from '../../../core/types';\nimport { getInputValue } from '../editors';\nimport type { EditingConfig } from '../types';\n\n// #region Constants\n\n/**\n * CSS selector for focusable editor elements inside a cell.\n * Duplicated from core/internal/rows to avoid cross-boundary imports\n * that Vite externalises to `@toolbox-web/grid` during plugin bundling.\n *\n * @internal\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Returns the closest ancestor (or `target` itself) that matches\n * {@link FOCUSABLE_EDITOR_SELECTOR}, or `null` if none exists.\n *\n * Always prefer this over `target.matches(FOCUSABLE_EDITOR_SELECTOR)` —\n * non-focusable editor descendants (e.g. `<option>` inside an open\n * native `<select>` popup; spans inside `contenteditable`) need to be\n * recognised as \"inside the editor\" but do not themselves match the\n * selector.\n *\n * @internal\n */\nexport function getEditorAncestor(target: EventTarget | null | undefined): Element | null {\n const el = target as Element | null | undefined;\n return el?.closest?.(FOCUSABLE_EDITOR_SELECTOR) ?? null;\n}\n\n// #endregion\n\n// #region Editor Resolution\n\n/**\n * Resolves the editor for a column using the priority chain:\n * 1. Column-level (`column.editor`)\n * 2. Light DOM template (`__editorTemplate` → returns 'template')\n * 3. Grid-level (`gridConfig.typeDefaults[column.type]`)\n * 4. App-level (framework adapter's `getTypeDefault`)\n * 5. Returns undefined (caller uses built-in defaultEditorFor)\n */\nexport function resolveEditor<TRow>(\n grid: InternalGrid<TRow>,\n col: ColumnInternal<TRow>,\n): ColumnEditorSpec<TRow, unknown> | 'template' | undefined {\n // 1. Column-level editor (highest priority)\n if (col.editor) return col.editor;\n\n // 2. Light DOM template\n const tplHolder = col.__editorTemplate;\n if (tplHolder) return 'template';\n\n // No type specified - no type defaults to check\n if (!col.type) return undefined;\n\n // 3. Grid-level typeDefaults (access via effectiveConfig)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const gridTypeDefaults = (grid as any).effectiveConfig?.typeDefaults;\n if (gridTypeDefaults?.[col.type]?.editor) {\n return gridTypeDefaults[col.type].editor as ColumnEditorSpec<TRow, unknown>;\n }\n\n // 4. App-level registry (via framework adapter)\n const adapter = grid.__frameworkAdapter;\n if (adapter?.getTypeDefault) {\n const appDefault = adapter.getTypeDefault<TRow>(col.type, grid._hostElement);\n if (appDefault?.editor) {\n return appDefault.editor as ColumnEditorSpec<TRow, unknown>;\n }\n }\n\n // 5. No custom editor - caller uses built-in defaultEditorFor\n return undefined;\n}\n\n// #endregion\n\n// #region Property Key Safety\n\n/**\n * Returns true if the given property key is safe to use on a plain object.\n */\nexport function isSafePropertyKey(key: unknown): key is string {\n if (typeof key !== 'string') return false;\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') return false;\n return true;\n}\n\n// #endregion\n\n// #region Row Element State\n\n/**\n * Increment the editing cell count on a row element.\n */\nexport function incrementEditingCount(rowEl: RowElementInternal): void {\n const count = (rowEl.__editingCellCount ?? 0) + 1;\n rowEl.__editingCellCount = count;\n rowEl.setAttribute('data-has-editing', '');\n}\n\n/**\n * Clear all editing state from a row element.\n */\nexport function clearEditingState(rowEl: RowElementInternal): void {\n rowEl.__editingCellCount = 0;\n rowEl.removeAttribute('data-has-editing');\n}\n\n// #endregion\n\n// #region No-op Helpers\n\n/**\n * No-op updateRow function for rows without IDs.\n * Extracted to a named function to satisfy eslint no-empty-function.\n */\nexport function noopUpdateRow(_changes: unknown): void {\n // Row has no ID - cannot update\n}\n\n// #endregion\n\n// #region Editor Input Wiring\n\n/**\n * Auto-wire commit/cancel lifecycle for input elements in string-returned editors.\n */\nexport function wireEditorInputs(\n editorHost: HTMLElement,\n column: ColumnConfig<unknown>,\n commit: (value: unknown) => void,\n originalValue?: unknown,\n): void {\n const input = editorHost.querySelector('input,textarea,select') as\n | HTMLInputElement\n | HTMLTextAreaElement\n | HTMLSelectElement\n | null;\n if (!input) return;\n\n input.addEventListener('blur', () => {\n commit(getInputValue(input, column, originalValue));\n });\n\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.addEventListener('change', () => commit(input.checked));\n } else if (input instanceof HTMLSelectElement) {\n input.addEventListener('change', () => commit(getInputValue(input, column, originalValue)));\n }\n}\n\n// #endregion\n\n// #region Edit Guard\n\n/**\n * Returns `true` when the configured `onBeforeEditClose` callback vetoes\n * the close (i.e. returns `false`). Use as a one-liner guard:\n *\n * ```ts\n * if (shouldPreventEditClose(config, e)) return;\n * ```\n */\nexport function shouldPreventEditClose(config: EditingConfig, event: MouseEvent | KeyboardEvent): boolean {\n return config.onBeforeEditClose?.(event) === false;\n}\n\n// #endregion\n\n// #region ARIA Overlay Detection\n\n/**\n * Returns `true` when `target` is inside an overlay panel that is currently\n * \"owned\" by an open combobox/listbox/menu inside `scopeEl`.\n *\n * Detection uses the WAI-ARIA pattern: a control with\n * `aria-expanded=\"true\"` and `aria-controls=\"<id>\"` declares ownership of\n * the panel with that id. Used by the editing plugin as a generic fallback\n * so portal-rendered overlays from libraries like Downshift, Material UI,\n * or Headless UI are recognised as part of the active editor without the\n * consumer having to call `registerExternalFocusContainer` (#251).\n *\n * Cheap: only walks elements with `aria-expanded=\"true\"` inside the\n * editor scope (typically zero or one).\n *\n * @internal\n */\nexport function isInsideOpenAriaOverlay(target: Node | null | undefined, scopeEl: HTMLElement): boolean {\n if (!target) return false;\n const triggers = scopeEl.querySelectorAll<HTMLElement>('[aria-expanded=\"true\"][aria-controls]');\n for (let i = 0; i < triggers.length; i++) {\n const id = triggers[i].getAttribute('aria-controls');\n if (!id) continue;\n const panel = scopeEl.ownerDocument?.getElementById(id);\n if (panel && panel.contains(target as Node)) return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Row Comparison\n\n/**\n * Shallow-compare a snapshot against the current row to detect changes.\n * Returns `true` if any own-property value differs between the two objects.\n */\nexport function hasRowChanged<T>(snapshot: T | undefined, current: T): boolean {\n if (!snapshot) return false;\n const snapshotObj = snapshot as Record<string, unknown>;\n const currentObj = current as Record<string, unknown>;\n const allKeys = new Set([...Object.keys(snapshotObj), ...Object.keys(currentObj)]);\n for (const key of allKeys) {\n if (snapshotObj[key] !== currentObj[key]) return true;\n }\n return false;\n}\n\n// #endregion\n","/**\n * Editor injection logic for the Editing Plugin.\n *\n * Extracted from EditingPlugin to reduce the main file size.\n * Contains the DOM-heavy editor creation and template rendering,\n * while the plugin retains state management and event emission.\n *\n * @internal\n */\n\nimport { EDITOR_MOUNT_ERROR, warnDiagnostic } from '../../../core/internal/diagnostics';\nimport { parkCellLoadingOverlay } from '../../../core/internal/loading';\nimport { readCellField, writeCellField } from '../../../core/internal/value-accessor';\nimport type {\n ColumnConfig,\n ColumnEditorContext,\n ColumnInternal,\n GridHost,\n RowElementInternal,\n} from '../../../core/types';\nimport { defaultEditorFor, getInputValue, setInputValue } from '../editors';\nimport type { EditingConfig } from '../types';\nimport {\n FOCUSABLE_EDITOR_SELECTOR,\n getEditorAncestor,\n incrementEditingCount,\n isSafePropertyKey,\n noopUpdateRow,\n resolveEditor,\n shouldPreventEditClose,\n wireEditorInputs,\n} from './helpers';\n\n// #region Types\n\n/**\n * Dependencies injected by the EditingPlugin so the extraction\n * can call back into plugin-owned state and methods.\n */\nexport interface EditorInjectionDeps<T> {\n /** Internal grid reference (also serves as HTMLElement). */\n grid: GridHost<T>;\n /** Whether the grid is in always-editing \"grid\" mode. */\n isGridMode: boolean;\n /** Plugin configuration. */\n config: EditingConfig;\n /** Set of cells currently in edit mode (\"rowIndex:colIndex\"). */\n editingCells: Set<string>;\n /** Value-change callbacks keyed by \"rowIndex:field\". */\n editorValueCallbacks: Map<string, (newValue: unknown) => void>;\n /** Returns `true` when an edit session is active (#activeEditRow !== -1). */\n isEditSessionActive: () => boolean;\n /** Commit a single cell value change. */\n commitCellValue: (rowIndex: number, column: ColumnConfig<T>, newValue: unknown, rowData: T) => void;\n /** Exit editing for a row (commit or revert). */\n exitRowEdit: (rowIndex: number, revert: boolean) => void;\n}\n\n// #endregion\n\n// #region Editor Injection\n\n/**\n * Inject an editor into a cell element.\n *\n * Handles the full editor lifecycle: creates the editor host, resolves\n * the editor spec (template / custom-element / factory / component),\n * wires commit/cancel callbacks, and registers value-change listeners.\n */\nexport function injectEditor<T>(\n deps: EditorInjectionDeps<T>,\n rowData: T,\n rowIndex: number,\n column: ColumnConfig<T>,\n colIndex: number,\n cell: HTMLElement,\n skipFocus: boolean,\n parentRowEl?: HTMLElement,\n): void {\n if (!column.editable) return;\n if (cell.classList.contains('editing')) return;\n\n const { grid, isGridMode, config, editingCells, editorValueCallbacks } = deps;\n\n // Get row ID for updateRow helper (may not exist)\n let rowId: string | undefined;\n try {\n rowId = grid.getRowId?.(rowData);\n } catch {\n // Row has no ID\n }\n\n // Create updateRow helper for cascade updates (noop if row has no ID)\n const updateRow: (changes: Partial<T>) => void = rowId\n ? // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (changes) => (grid as any).updateRow(rowId!, changes as Record<string, unknown>, 'cascade')\n : noopUpdateRow;\n\n const originalValue = isSafePropertyKey(column.field) ? readCellField(rowData, column.field) : undefined;\n\n cell.classList.add('editing');\n editingCells.add(`${rowIndex}:${colIndex}`);\n\n // Use explicit parentRowEl when cell is in a DocumentFragment (not yet in DOM),\n // otherwise fall back to cell.parentElement for cells already attached to a row.\n const rowEl = (parentRowEl ?? cell.parentElement) as RowElementInternal | null;\n if (rowEl) incrementEditingCount(rowEl);\n\n let editFinalized = false;\n const commit = (newValue: unknown) => {\n // In grid mode, always allow commits (we're always editing)\n // In row mode, only allow commits if we're in an active edit session\n if (editFinalized || (!isGridMode && !deps.isEditSessionActive())) return;\n // Resolve row and index fresh at commit time.\n // With a row ID we use _getRowEntry for O(1) lookup — this is resilient\n // against _rows being replaced (e.g. Angular directive effect).\n // Without a row ID we fall back to the captured rowData reference.\n // Using _rows[rowIndex] without an ID is unsafe: the index may be stale\n // after _rows replacement, which would commit to the WRONG row.\n const entry = rowId ? grid._getRowEntry(rowId) : undefined;\n const currentRowData = (entry?.row ?? rowData) as T;\n const currentIndex = entry?.index ?? rowIndex;\n deps.commitCellValue(currentIndex, column, newValue, currentRowData);\n };\n const cancel = () => {\n editFinalized = true;\n if (isSafePropertyKey(column.field)) {\n // Same ID-first / captured-rowData fallback as commit — see comment above.\n const entry = rowId ? grid._getRowEntry(rowId) : undefined;\n const currentRowData = (entry?.row ?? rowData) as T;\n writeCellField(currentRowData, column.field, originalValue);\n }\n };\n\n const editorHost = document.createElement('div');\n editorHost.className = 'tbw-editor-host';\n const loadingOverlay = parkCellLoadingOverlay(cell);\n // Let the framework adapter (React/Vue/Angular) tear down any renderer\n // it mounted into this cell BEFORE we wipe the DOM. The `cell.innerHTML`\n // assignment below removes adapter-managed nodes synchronously without\n // notifying the framework — which leaves React's fiber tree pointing at\n // orphan DOM and throws `removeChild` on the next user-triggered commit\n // (issue #250). Calling `releaseCell` here unmounts cleanly while the\n // adapter's container is still attached to its remembered children.\n // Guard with `firstElementChild` so default-rendered cells (no\n // adapter-managed nodes) skip the no-op call — matches the wipe-guard\n // pattern in `core/internal/rows.ts`.\n if (cell.firstElementChild) grid.__frameworkAdapter?.releaseCell?.(cell);\n cell.innerHTML = '';\n cell.appendChild(editorHost);\n if (loadingOverlay) cell.appendChild(loadingOverlay);\n\n // Keydown handler for Enter/Escape\n editorHost.addEventListener('keydown', (e: KeyboardEvent) => {\n // Respect editor-level preventDefault: portal-based pickers (combobox,\n // autocomplete, calendar) call preventDefault on Enter to confirm an\n // option without exiting the row. Bubbling reaches editorHost with\n // defaultPrevented=true; honor that and skip the row-exit logic (#250).\n if (e.defaultPrevented) return;\n // ARIA-expanded fallback (#251): when the focused control declares\n // an open overlay via aria-expanded=\"true\" + aria-controls, defer\n // Enter to the overlay so combobox confirmation does not close the\n // row edit. Covers libraries like Downshift / Headless UI / MUI\n // Autocomplete that do not call preventDefault on confirm.\n // Only short-circuit Enter — Escape and other keys must still flow\n // through the normal handlers (Escape closes the overlay first via\n // its own listener, then dispatches up to cancel-edit if unhandled).\n if (e.key === 'Enter') {\n const ariaTarget = e.target as HTMLElement | null;\n if (\n ariaTarget &&\n ariaTarget.getAttribute?.('aria-expanded') === 'true' &&\n ariaTarget.hasAttribute?.('aria-controls')\n ) {\n return;\n }\n }\n if (e.key === 'Enter') {\n // Native <select> open-popup guard — applies in BOTH grid and row mode.\n // When Enter's target is a *descendant* of the editor (most notably an\n // <option> inside an open native <select> popup), do nothing: let the\n // browser commit the highlighted option and close the popup, which fires\n // `change` on the editor (committing via the change listener wired below).\n // Calling preventDefault / stopPropagation / exitRowEdit here would\n // destroy the open popup and discard the selection. A subsequent Enter\n // (popup closed → target IS the editor) then exits the row as usual.\n // Row mode previously lacked this guard, so keyboard selection in custom\n // <select> editors closed the row instead of picking the value (#427).\n const enterTarget = e.target as Element | null;\n const enterEditorAncestor = getEditorAncestor(enterTarget);\n if (enterEditorAncestor && enterEditorAncestor !== enterTarget) {\n return;\n }\n // In grid mode, Enter just commits without exiting\n if (isGridMode) {\n // Target IS the editor itself (SELECT with no popup open, or\n // INPUT/TEXTAREA): commit the current value. preventDefault\n // suppresses the browser opening the SELECT popup on Enter.\n // We deliberately do NOT stopPropagation — the event must\n // bubble so EditingPlugin can blur the editor and return\n // focus to the grid for cell navigation.\n e.preventDefault();\n const input = editorHost.querySelector('input,textarea,select') as\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null;\n if (input) {\n commit(getInputValue(input, column as ColumnConfig<unknown>, originalValue));\n }\n return;\n }\n if (shouldPreventEditClose(config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n editFinalized = true;\n deps.exitRowEdit(rowIndex, false);\n }\n if (e.key === 'Escape') {\n // In grid mode, Escape doesn't exit edit mode\n if (isGridMode) {\n e.stopPropagation();\n e.preventDefault();\n return;\n }\n if (shouldPreventEditClose(config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n cancel();\n deps.exitRowEdit(rowIndex, true);\n }\n });\n\n const colInternal = column as ColumnInternal<T>;\n const tplHolder = colInternal.__editorTemplate;\n // Resolve editor using priority chain: column → template → typeDefaults → adapter → built-in\n const editorSpec = resolveEditor(grid, colInternal) ?? defaultEditorFor(column);\n const value = originalValue;\n\n // Value-change callback registration.\n // Editors call onValueChange(cb) to receive pushes when the underlying row\n // is mutated externally (e.g., via updateRow from another cell's commit).\n // Multiple callbacks can be registered (user + auto-wire).\n const callbackKey = `${rowIndex}:${column.field}`;\n const callbacks: Array<(newValue: unknown) => void> = [];\n editorValueCallbacks.set(callbackKey, (newVal) => {\n for (const cb of callbacks) cb(newVal);\n });\n const onValueChange = (cb: (newValue: unknown) => void) => {\n callbacks.push(cb);\n };\n\n if (editorSpec === 'template' && tplHolder) {\n renderTemplateEditor(deps, editorHost, colInternal, rowData, originalValue, commit, cancel, skipFocus, rowIndex);\n // Auto-update built-in template editors when value changes externally.\n // Skip non-primitive values (arrays, objects) — framework adapters manage\n // those via the cell-cancel event — except those setInputValue can map:\n // arrays onto a multi-select's options, and a Date into a date input (#555).\n onValueChange((newVal) => {\n const input = editorHost.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(\n 'input,textarea,select',\n );\n if (!input) return;\n const isMultiSelect = input instanceof HTMLSelectElement && input.multiple;\n const isDateIntoDateInput = newVal instanceof Date && input instanceof HTMLInputElement && input.type === 'date';\n if (!isMultiSelect && !isDateIntoDateInput && newVal != null && typeof newVal === 'object') return;\n setInputValue(input, newVal, column);\n });\n } else if (typeof editorSpec === 'string') {\n const el = document.createElement(editorSpec) as HTMLElement & { value?: unknown };\n el.value = value;\n el.addEventListener('change', () => commit(el.value));\n // Auto-update custom element editors when value changes externally\n onValueChange((newVal) => {\n el.value = newVal;\n });\n editorHost.appendChild(el);\n if (!skipFocus) {\n queueMicrotask(() => {\n const focusable = editorHost.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n focusable?.focus({ preventScroll: true });\n });\n }\n } else if (typeof editorSpec === 'function') {\n const ctx: ColumnEditorContext<T> = {\n row: rowData,\n rowId: rowId ?? '',\n value,\n field: column.field,\n column,\n commit,\n cancel,\n updateRow,\n onValueChange,\n grid: deps.grid as ColumnEditorContext<T>['grid'],\n };\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const produced = (editorSpec as any)(ctx);\n if (typeof produced === 'string') {\n // NOT sanitized on purpose: sanitizeHTML strips `input`/`select`/\n // `textarea`/`button`, which is exactly what an editor is made of. The\n // string comes from an author-supplied editor factory (code, not row\n // data) — row values reach the DOM through the compiled-template path\n // below, which escapes interpolated values.\n // eslint-disable-next-line no-restricted-syntax\n editorHost.innerHTML = produced;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n wireEditorInputs(editorHost, column as any, commit, originalValue);\n // Auto-update wired inputs when value changes externally\n onValueChange((newVal) => {\n const input = editorHost.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(\n 'input,textarea,select',\n );\n if (input) setInputValue(input, newVal, column);\n });\n } else if (produced instanceof Node) {\n editorHost.appendChild(produced);\n const isSimpleInput =\n produced instanceof HTMLInputElement ||\n produced instanceof HTMLSelectElement ||\n produced instanceof HTMLTextAreaElement;\n if (!isSimpleInput) {\n cell.setAttribute('data-editor-managed', '');\n } else {\n // Auto-update simple inputs returned by factory functions\n onValueChange((newVal) => setInputValue(produced, newVal, column));\n }\n } else if (!produced && editorHost.hasChildNodes()) {\n // Factory returned void but mounted content into the editor host\n // (e.g. Angular/React/Vue adapter component editor). Mark the cell\n // as externally managed so the native commit loop in #exitRowEdit\n // does not read raw input values from framework editor DOM.\n cell.setAttribute('data-editor-managed', '');\n }\n if (!skipFocus) {\n queueMicrotask(() => {\n const focusable = editorHost.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n focusable?.focus({ preventScroll: true });\n });\n }\n } else if (editorSpec && typeof editorSpec === 'object') {\n const placeholder = document.createElement('div');\n placeholder.setAttribute('data-external-editor', '');\n placeholder.setAttribute('data-field', column.field);\n editorHost.appendChild(placeholder);\n cell.setAttribute('data-editor-managed', '');\n const context: ColumnEditorContext<T> = {\n row: rowData,\n rowId: rowId ?? '',\n value,\n field: column.field,\n column,\n commit,\n cancel,\n updateRow,\n onValueChange,\n grid: deps.grid as ColumnEditorContext<T>['grid'],\n };\n if (editorSpec.mount) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n editorSpec.mount({ placeholder, context: context as any, spec: editorSpec });\n } catch (e) {\n warnDiagnostic(\n EDITOR_MOUNT_ERROR,\n `External editor mount error for column '${column.field}': ${e}`,\n deps.grid.id,\n );\n }\n } else {\n grid.dispatchEvent(\n new CustomEvent('mount-external-editor', { detail: { placeholder, spec: editorSpec, context } }),\n );\n }\n }\n\n nameEditorFromHeader(editorHost, column);\n}\n\n/**\n * Give the editor an accessible name taken from its column header.\n *\n * A screen reader announces the header when focus lands on a *gridcell*, but an\n * `<input>` inside that cell is a control in its own right and needs its own\n * name (SC 4.1.2 / SC 3.3.2) — otherwise it is announced as a bare \"edit text\".\n * Deferred a microtask so editors that mount asynchronously are covered, and\n * skipped entirely whenever the control already has a name of any kind.\n */\nfunction nameEditorFromHeader<T>(editorHost: HTMLElement, column: ColumnConfig<T>): void {\n const label = typeof column.header === 'string' ? column.header : column.field;\n if (!label) return;\n\n queueMicrotask(() => {\n const control = editorHost.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (!control) return;\n const named =\n control.hasAttribute('aria-label') ||\n control.hasAttribute('aria-labelledby') ||\n control.hasAttribute('title') ||\n (control.id && editorHost.querySelector(`label[for=\"${CSS.escape(control.id)}\"]`)) ||\n control.closest('label');\n if (!named) control.setAttribute('aria-label', label);\n });\n}\n\n// #endregion\n\n// #region Template Editor\n\n/**\n * Render a template-based editor inside an editor host element.\n */\nfunction renderTemplateEditor<T>(\n deps: Pick<EditorInjectionDeps<T>, 'config' | 'exitRowEdit'>,\n editorHost: HTMLElement,\n column: ColumnInternal<T>,\n rowData: T,\n originalValue: unknown,\n commit: (value: unknown) => void,\n cancel: () => void,\n skipFocus: boolean,\n rowIndex: number,\n): void {\n const tplHolder = column.__editorTemplate;\n if (!tplHolder) return;\n\n const clone = tplHolder.cloneNode(true) as HTMLElement;\n const compiledEditor = column.__compiledEditor;\n\n if (compiledEditor) {\n // NOT sanitized: editor markup is author light-DOM (`<template editor>`) and\n // contains form controls the sanitizer would strip. Row data interpolated\n // via `{{ }}` is HTML-escaped by the compiled template itself.\n // eslint-disable-next-line no-restricted-syntax\n clone.innerHTML = compiledEditor({\n row: rowData,\n value: originalValue,\n field: column.field,\n column,\n commit,\n cancel,\n });\n } else {\n clone.querySelectorAll<HTMLElement>('*').forEach((node) => {\n if (node.childNodes.length === 1 && node.firstChild?.nodeType === Node.TEXT_NODE) {\n node.textContent =\n node.textContent\n ?.replace(/{{\\s*value\\s*}}/g, originalValue == null ? '' : String(originalValue))\n .replace(/{{\\s*row\\.([a-zA-Z0-9_]+)\\s*}}/g, (_m, g: string) => {\n if (!isSafePropertyKey(g)) return '';\n const v = (rowData as Record<string, unknown>)[g];\n return v == null ? '' : String(v);\n }) || '';\n }\n });\n }\n\n const input = clone.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(\n 'input,textarea,select',\n );\n if (input) {\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.checked = !!originalValue;\n } else {\n input.value = String(originalValue ?? '');\n }\n\n let editFinalized = false;\n input.addEventListener('blur', () => {\n if (editFinalized) return;\n commit(getInputValue(input, column, originalValue));\n });\n input.addEventListener('keydown', (evt) => {\n const e = evt as KeyboardEvent;\n if (e.key === 'Enter') {\n if (shouldPreventEditClose(deps.config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n editFinalized = true;\n commit(getInputValue(input, column, originalValue));\n deps.exitRowEdit(rowIndex, false);\n }\n if (e.key === 'Escape') {\n if (shouldPreventEditClose(deps.config, e)) return;\n e.stopPropagation();\n e.preventDefault();\n cancel();\n deps.exitRowEdit(rowIndex, true);\n }\n });\n if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n input.addEventListener('change', () => commit(input.checked));\n }\n if (!skipFocus) {\n setTimeout(() => input.focus({ preventScroll: true }), 0);\n }\n }\n editorHost.appendChild(clone);\n}\n\n// #endregion\n","/**\n * Editing Plugin\n *\n * Provides complete editing functionality for tbw-grid.\n * This plugin is FULLY SELF-CONTAINED - the grid has ZERO editing knowledge.\n *\n * The plugin:\n * - Owns all editing state (active cell, snapshots, changed rows)\n * - Uses event distribution (onCellClick, onKeyDown) to handle edit lifecycle\n * - Uses afterRender() hook to inject editors into cells\n * - Uses processColumns() to augment columns with editing metadata\n * - Emits its own events (cell-commit, row-commit, changed-rows-reset)\n *\n * Without this plugin, the grid cannot edit. With this plugin, editing\n * is fully functional without any core changes.\n */\n\nimport { GridDataAttrs } from '../../core/constants';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { ensureCellVisible } from '../../core/internal/keyboard';\nimport { parkCellLoadingOverlay } from '../../core/internal/loading';\nimport { compileTemplate } from '../../core/internal/sanitize';\nimport { invalidateAccessorCache, readCellField, writeCellField } from '../../core/internal/value-accessor';\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n PluginManifest,\n PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport { BaseGridPlugin, type CellClickEvent, type GridElement } from '../../core/plugin/base-plugin';\nimport type { CellEditablePredicate, CommitCellValueContext } from '../../core/plugin/types';\nimport type {\n ColumnConfig,\n ColumnInternal,\n GridHost,\n InternalGrid,\n RowElementInternal,\n TypeDefault,\n UpdateSource,\n} from '../../core/types';\nimport styles from './editing.css?inline';\nimport { getInputValue, isSameEditorValue } from './editors';\nimport { CellValidationManager } from './internal/cell-validation';\nimport { type BaselinesCapturedDetail, type DirtyChangeDetail, type DirtyRowEntry } from './internal/dirty-tracking';\nimport { DirtyTrackingManager } from './internal/dirty-tracking-manager';\nimport { type EditorInjectionDeps, injectEditor as injectEditorImpl } from './internal/editor-injection';\nimport {\n clearEditingState,\n FOCUSABLE_EDITOR_SELECTOR,\n getEditorAncestor,\n hasRowChanged,\n isInsideOpenAriaOverlay,\n isSafePropertyKey,\n noopUpdateRow,\n shouldPreventEditClose,\n} from './internal/helpers';\nimport type {\n BeforeEditCloseDetail,\n CellCancelDetail,\n CellCommitDetail,\n ChangedRowsResetDetail,\n EditCloseDetail,\n EditingConfig,\n EditOpenDetail,\n EditorParams,\n RowCommitDetail,\n} from './types';\n\n// ============================================================================\n// EditingPlugin\n// ============================================================================\n\n/**\n * Editing Plugin for tbw-grid\n *\n * Enables inline cell editing in the grid. Provides built-in editors for common data types\n * and supports custom editor functions for specialized input scenarios.\n *\n * ## Why Opt-In?\n *\n * Editing is delivered as a plugin rather than built into the core grid:\n *\n * - **Smaller bundle** — Apps that only display data don't pay for editing code\n * - **Clear intent** — Explicit plugin registration makes editing capability obvious\n * - **Runtime validation** — Using `editable: true` without the plugin throws a helpful error\n *\n * ## Installation\n *\n * ```ts\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * ```\n *\n * ## Edit Triggers\n *\n * Configure how editing is triggered with the `editOn` option:\n *\n * | Value | Behavior |\n * |-------|----------|\n * | `'click'` | Single click enters edit mode (default) |\n * | `'dblclick'` | Double-click enters edit mode |\n *\n * ## Keyboard Shortcuts\n *\n * | Key | Action |\n * |-----|--------|\n * | `Enter` | Commit edit and move down |\n * | `Tab` | Commit edit and move right |\n * | `Escape` | Cancel edit, restore original value |\n * | `Arrow Keys` | Navigate between cells (when not editing) |\n *\n * @example Basic editing with double-click trigger\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', editable: true },\n * { field: 'price', type: 'number', editable: true },\n * { field: 'active', type: 'boolean', editable: true },\n * ],\n * plugins: [new EditingPlugin({ editOn: 'dblclick' })],\n * };\n *\n * grid.on('cell-commit', ({ field, oldValue, newValue }) => {\n * console.log(`${field}: ${oldValue} → ${newValue}`);\n * });\n * ```\n *\n * @example Custom editor function\n * ```ts\n * columns: [\n * {\n * field: 'status',\n * editable: true,\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * ['pending', 'active', 'completed'].forEach(opt => {\n * const option = document.createElement('option');\n * option.value = opt;\n * option.textContent = opt;\n * option.selected = ctx.value === opt;\n * select.appendChild(option);\n * });\n * select.addEventListener('change', () => ctx.commit(select.value));\n * return select;\n * },\n * },\n * ]\n * ```\n *\n * @see {@link EditingConfig} for configuration options\n * @see `ColumnEditorContext` for custom editor context\n * @see {@link EditingConfig} for interactive examples in the docs site\n * @since 0.4.0\n */\nexport class EditingPlugin<T = unknown> extends BaseGridPlugin<EditingConfig> {\n /**\n * Plugin manifest - declares owned properties for configuration validation.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n ownedProperties: [\n {\n property: 'editable',\n level: 'column',\n description: 'the \"editable\" column property',\n isUsed: (v) => v === true || typeof v === 'function',\n },\n {\n property: 'editor',\n level: 'column',\n description: 'the \"editor\" column property',\n },\n {\n property: 'editorParams',\n level: 'column',\n description: 'the \"editorParams\" column property',\n },\n {\n property: 'nullable',\n level: 'column',\n description: 'the \"nullable\" column property (allows null values)',\n },\n ],\n events: [\n {\n type: 'cell-edit-committed',\n description: 'Emitted when a cell edit is committed (for plugin-to-plugin coordination)',\n },\n {\n type: 'cell-edit-reverted',\n description:\n 'Emitted when row values are rolled back in place (revertRow/revertAll, row-edit cancel, grid-mode Escape) — not recorded as history',\n },\n ],\n queries: [\n {\n type: 'isEditing',\n description: 'Returns whether any cell is currently being edited',\n },\n {\n type: 'getCellEditableResolver',\n description:\n 'Returns a predicate (field, row) => boolean resolving per-cell editability (rowEditable gate + column.editable true/false/function), for consumers like clipboard paste',\n },\n {\n type: 'commitCellValue',\n description:\n 'Commits a single cell value coming from a core row mutation (updateRow/updateRows), running the full edit pipeline: cancelable cell-commit (validation/abortion), dirty tracking, history, and cascade. Returns true (applied), false (vetoed), or undefined (not handled).',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'editing';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<EditingConfig> {\n return {\n mode: 'row',\n editOn: 'click',\n };\n }\n\n /**\n * Whether the grid is in 'grid' mode (all cells always editable).\n */\n get #isGridMode(): boolean {\n return this.config.mode === 'grid';\n }\n\n /**\n * Resolve whether a given cell is editable.\n *\n * Resolution order:\n * 1. `gridConfig.rowEditable(row)` — row-level gate (if provided). Returns `false` → not editable.\n * 2. `column.editable` — `true`, `false`, or `(row) => boolean`.\n *\n * @returns `true` when the cell should be editable, `false` otherwise.\n */\n #isCellEditable(column: ColumnConfig<T>, row: T): boolean {\n // Row-level gate\n const rowEditable = this.#internalGrid.effectiveConfig?.rowEditable;\n if (rowEditable && !rowEditable(row as any)) return false;\n\n // Column-level check\n const { editable } = column;\n if (typeof editable === 'function') return editable(row);\n return editable === true;\n }\n\n /**\n * Check whether a column has ANY editability configured (static `true` or a\n * function). This is used for quick checks where no specific row is available\n * (e.g. \"does this row have any potentially-editable columns?\").\n */\n #hasEditableConfig(column: ColumnConfig<T>): boolean {\n return column.editable === true || typeof column.editable === 'function';\n }\n\n // #region Editing State (fully owned by plugin)\n\n /** Currently active edit row index, or -1 if not editing */\n #activeEditRow = -1;\n\n /** Row ID of the currently active edit row (stable across _rows replacement) */\n #activeEditRowId: string | undefined;\n\n /** Reference to the row object at edit-open time. Used as fallback in\n * #exitRowEdit when no row ID is available (prevents stale-index access). */\n #activeEditRowRef: T | undefined;\n\n /** Currently active edit column index, or -1 if not editing */\n #activeEditCol = -1;\n\n /** Snapshots of row data before editing started */\n #rowEditSnapshots = new Map<number, T>();\n\n /** Set of cells currently in edit mode: \"rowIndex:colIndex\" */\n #editingCells = new Set<string>();\n\n /**\n * Light-DOM elements whose declarative attributes have already been read.\n * The `editable` attribute (and `<tbw-grid-column-editor>` template) supply\n * the INITIAL state only — seeding once guarantees a later runtime/config\n * change to a falsy value is not re-applied on the next `processColumns`\n * pass (issue #272). Reset in `detach()` so a re-attached instance re-seeds.\n */\n #seededFromAttr = new WeakSet<HTMLElement>();\n\n /**\n * Value-change callbacks for active editors.\n * Keyed by \"rowIndex:field\" → callback that pushes updated values to the editor.\n * Populated during #injectEditor, cleaned up when editors are removed.\n */\n #editorValueCallbacks = new Map<string, (newValue: unknown) => void>();\n\n /** Flag to restore focus after next render (used when exiting edit mode) */\n #pendingFocusRestore = false;\n\n /** Row index pending animation after render, or -1 if none */\n #pendingRowAnimation = -1;\n\n /**\n * Cell validation manager — handles invalid-cell state tracking and DOM sync.\n * Initialized lazily in `attach()` since the sync callback needs grid access.\n */\n #validation!: CellValidationManager;\n\n /**\n * In grid mode, tracks whether an input field is currently focused.\n * When true: arrow keys work within input (edit mode).\n * When false: arrow keys navigate between cells (navigation mode).\n * Escape switches to navigation mode, Enter switches to edit mode.\n */\n #gridModeInputFocused = false;\n\n /**\n * In grid mode, when true, prevents inputs from auto-focusing.\n * This is set when Escape is pressed (navigation mode) and cleared\n * when Enter is pressed or user explicitly clicks an input.\n */\n #gridModeEditLocked = false;\n\n /**\n * When true, only a single cell is being edited (triggered by F2 or `beginCellEdit`).\n * Tab and Arrow keys commit and close the editor instead of navigating to adjacent cells.\n */\n #singleCellEdit = false;\n\n /**\n * In grid mode, snapshot of the focused cell's value when the editor first\n * receives focus. Used to revert on Escape (cell-level cancel).\n */\n #gridModeCellSnapshot: { rowIndex: number; colIndex: number; field: string; value: unknown } | null = null;\n\n // --- Dirty Tracking State (delegated to DirtyTrackingManager) ---\n\n /** Manages all dirty tracking state: baselines, changed/new/committed sets. */\n readonly #dirty = new DirtyTrackingManager<T>();\n\n /**\n * Cells currently mid-`cell-commit`, keyed `` `${rowId ?? rowIndex}\\0${field}` ``.\n * A `cell-commit` listener commonly cascades by calling `updateRow`, which\n * routes back through this commit pipeline synchronously. When such a nested\n * update targets a cell already in this set, `#commitCellValue` returns early\n * (the outer commit owns the single apply + `cell-edit-committed`), preventing\n * the infinite `updateRow → cell-commit → updateRow` recursion WITHOUT any\n * consumer-side re-entrancy guard and WITHOUT duplicating history or bypassing\n * a pending `preventDefault()`.\n */\n readonly #committingCells = new Set<string>();\n\n // --- Editor Injection Deps (cached for #injectEditor delegation) ---\n\n /** Dependency bag created once in `attach()` and reused by every `#injectEditor` call. */\n #editorDeps!: EditorInjectionDeps<T>;\n\n /**\n * Narrows the grid to its row type `T`. `GridElement` is intentionally\n * non-generic, so this accessor re-applies the plugin's own `T`.\n */\n get #internalGrid(): GridHost<T> {\n return this.grid;\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n const signal = this.disconnectSignal;\n const internalGrid = this.#internalGrid;\n\n // Initialize cell validation manager with DOM sync callback\n this.#validation = new CellValidationManager((rowId, field, invalid) => {\n this.#syncInvalidCellAttribute(rowId, field, invalid);\n });\n\n // Initialize editor injection deps (cached for all #injectEditor calls)\n this.#editorDeps = {\n grid: internalGrid,\n isGridMode: this.#isGridMode,\n config: this.config,\n editingCells: this.#editingCells,\n editorValueCallbacks: this.#editorValueCallbacks,\n isEditSessionActive: () => this.#activeEditRow !== -1,\n commitCellValue: (ri, col, val, row) => this.#commitCellValue(ri, col, val, row),\n exitRowEdit: (ri, revert) => this.#exitRowEdit(ri, revert),\n };\n\n // Inject editing state and methods onto grid for backward compatibility\n this.#syncGridEditState();\n\n // Inject changedRows getter\n Object.defineProperty(grid, 'changedRows', {\n get: () => this.changedRows,\n configurable: true,\n });\n\n // Inject changedRowIds getter (new ID-based API)\n Object.defineProperty(grid, 'changedRowIds', {\n get: () => this.changedRowIds,\n configurable: true,\n });\n\n // Inject raw Set for O(1) lookup in the render hot path\n Object.defineProperty(grid, '_changedRowIdSet', {\n get: () => this.#dirty.changedRowIds,\n configurable: true,\n });\n\n // Inject resetChangedRows method\n (grid as any).resetChangedRows = (silent?: boolean) => this.resetChangedRows(silent);\n\n this.#attachRowModeListeners(signal, internalGrid);\n\n // In grid mode, request a full render to trigger afterCellRender hooks\n if (this.#isGridMode) {\n internalGrid._isGridEditMode = true;\n this.gridElement.classList.add('tbw-grid-mode');\n this.requestRender();\n this.#attachGridModeListeners(signal, internalGrid);\n }\n }\n\n /**\n * Row-mode editing wiring: document-level Escape/click-outside exit, the optional\n * focus trap, external `cell-change` push-down into open editors, and undo/redo\n * dirty re-evaluation. Registered unconditionally — each handler self-guards on\n * `#isGridMode` / its own config flag.\n */\n #attachRowModeListeners(signal: AbortSignal, internalGrid: GridHost<T>): void {\n // Document-level Escape to cancel editing (only in 'row' mode)\n document.addEventListener(\n 'keydown',\n (e: KeyboardEvent) => {\n // In grid mode, Escape doesn't exit edit mode\n if (this.#isGridMode) return;\n if (e.key === 'Escape' && this.#activeEditRow !== -1) {\n if (shouldPreventEditClose(this.config, e)) return;\n this.#exitRowEdit(this.#activeEditRow, true);\n }\n },\n { capture: true, signal },\n );\n\n // Click outside to commit editing (only in 'row' mode)\n // Use queueMicrotask to allow pending change events to fire first.\n // This is important for Angular/React editors where the (change) event\n // fires after mousedown but before mouseup/click.\n document.addEventListener(\n 'mousedown',\n (e: MouseEvent) => {\n // In grid mode, clicking outside doesn't exit edit mode\n if (this.#isGridMode) return;\n if (this.#activeEditRow === -1) return;\n const rowEl = internalGrid.findRenderedRowElement?.(this.#activeEditRow);\n if (!rowEl) return;\n const path = (e.composedPath && e.composedPath()) || [];\n if (path.includes(rowEl)) return;\n\n // Check if click is inside a registered external focus container\n // (e.g., overlays, datepickers, dropdowns at <body> level).\n // Only check targets OUTSIDE the grid — clicks on other rows inside\n // the grid should still commit the active edit row.\n const target = e.target as Node | null;\n if (target && !this.gridElement.contains(target) && this.grid.containsFocus?.(target)) {\n return;\n }\n\n // ARIA-expanded fallback (#251): when the click target lives inside\n // an overlay declared by an open combobox/listbox in the active edit\n // row (aria-expanded=\"true\" + aria-controls=<id>), treat it as part\n // of the editor. Covers Downshift / Headless UI / MUI Autocomplete\n // panels that opt out of registerExternalFocusContainer.\n if (target && !this.gridElement.contains(target)) {\n const rowEl = internalGrid.findRenderedRowElement?.(this.#activeEditRow);\n if (rowEl && isInsideOpenAriaOverlay(target, rowEl)) return;\n }\n\n if (shouldPreventEditClose(this.config, e)) return;\n\n // Delay exit to allow pending change/commit events to fire\n queueMicrotask(() => {\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, false);\n }\n });\n },\n { signal },\n );\n\n // Focus trap: when enabled, prevent focus from leaving the grid\n // while a row is being edited. If focus moves outside the grid\n // (and its registered external containers), reclaim it.\n if (this.config.focusTrap) {\n this.gridElement.addEventListener(\n 'focusout',\n (e: FocusEvent) => {\n // Only trap in row mode when actively editing\n if (this.#isGridMode) return;\n if (this.#activeEditRow === -1) return;\n\n const related = e.relatedTarget as Node | null;\n // If focus is going to an external container, that's fine\n if (related && this.grid.containsFocus?.(related)) return;\n // If focus is going to another element inside the grid, allow it\n if (related && this.gridElement.contains(related)) return;\n\n // Focus left the grid entirely — reclaim it\n queueMicrotask(() => {\n // Re-check in case editing was committed in the meantime\n if (this.#activeEditRow === -1) return;\n this.#focusCurrentCellEditor();\n });\n },\n { signal },\n );\n }\n\n // Listen for external row mutations to push updated values to active editors.\n // When field A commits and sets field B via updateRow(), field B's editor\n // (if open) must reflect the new value.\n this.gridElement.addEventListener(\n 'cell-change',\n (e: Event) => {\n const detail = (e as CustomEvent).detail as {\n rowIndex: number;\n field: string;\n newValue: unknown;\n source: string;\n };\n // Only push updates from cascade/api sources — not from the editor's own commit\n if (detail.source === 'user') return;\n const key = `${detail.rowIndex}:${detail.field}`;\n const cb = this.#editorValueCallbacks.get(key);\n if (cb) cb(detail.newValue);\n },\n { signal },\n );\n\n // --- Dirty tracking: listen for undo/redo events to re-evaluate dirty state ---\n if (this.config.dirtyTracking) {\n type HistoryEdit = { rowIndex: number; rowId?: string };\n const handleUndoRedo = (e: Event) => {\n const action = (e as CustomEvent).detail?.action as\n (HistoryEdit & { type: 'cell-edit' }) | { type: 'compound'; actions: HistoryEdit[] } | undefined;\n if (!action) return;\n const edits = action.type === 'compound' ? action.actions : [action];\n const seen = new Set<string>();\n for (const edit of edits) {\n // Resolve by id (#526) — the display index is stale once the view is sorted/filtered.\n // O(1) visible-map lookup only: rows outside the view were already re-evaluated by\n // the 'history' branch of #handleCommitCellValue, so a source scan would buy nothing.\n const row = (\n edit.rowId !== undefined ? this.#internalGrid._getRowEntry(edit.rowId)?.row : this.rows[edit.rowIndex]\n ) as T | undefined;\n if (!row) continue;\n const rowId = edit.rowId ?? this.#safeGetRowId(row);\n if (!rowId || seen.has(rowId)) continue;\n seen.add(rowId);\n const dirty = this.#dirty.isRowDirty(rowId, row);\n this.#emitDirtyChange(rowId, row, dirty ? 'modified' : 'pristine');\n }\n };\n this.gridElement.addEventListener('undo', handleUndoRedo, { signal });\n this.gridElement.addEventListener('redo', handleUndoRedo, { signal });\n\n // Listen for row-inserted events to auto-mark new rows for dirty tracking\n this.on('row-inserted', (detail: { row: T; index: number }) => {\n const rowId = this.#safeGetRowId(detail.row);\n if (rowId != null) {\n this.markAsNew(String(rowId));\n }\n });\n\n // ServerSide blocks can land via an in-place swap + lightweight refresh that\n // never runs processRows, so capture their baselines on arrival — otherwise\n // the loaded rows have no original and an edit could never be reverted.\n // That refresh skips afterRender() on the virtualized path, so request a\n // STYLE pass to emit `baselines-captured` post-render (drained once, so a\n // coalesced full render still emits a single event).\n this.on('datasource:data', (detail: { rows: readonly T[] }) => {\n if (this.#captureBaselines(detail.rows)) this.requestAfterRender();\n });\n }\n }\n\n /**\n * Capture dirty-tracking baselines for `rows` (first-write-wins; rows without an id are skipped).\n * @returns whether any new baseline was captured\n */\n #captureBaselines(rows: readonly T[]): boolean {\n const internalGrid = this.#internalGrid;\n if (!internalGrid.getRowId) return false;\n return this.#dirty.capture(rows, (r) => {\n try {\n return internalGrid.getRowId?.(r);\n } catch {\n return undefined;\n }\n });\n }\n\n /**\n * Grid-mode (\"spreadsheet\") editing wiring: focus/blur tracking that distinguishes\n * navigation mode from edit mode, Escape-to-navigation, and click-to-unlock.\n * Only registered when `editing.mode === 'grid'`.\n */\n #attachGridModeListeners(signal: AbortSignal, internalGrid: GridHost<T>): void {\n // Track focus/blur on inputs to maintain navigation vs edit mode state\n this.gridElement.addEventListener(\n 'focusin',\n (e: FocusEvent) => {\n const target = e.target as HTMLElement;\n // Ignore focus on the grid element itself — it has tabindex=0 so it\n // matches FOCUSABLE_EDITOR_SELECTOR, but blurring + re-focusing it\n // would cause infinite recursion.\n if (target === this.gridElement) return;\n if (target.matches(FOCUSABLE_EDITOR_SELECTOR)) {\n // If edit is locked (navigation mode), blur the input immediately\n if (this.#gridModeEditLocked) {\n target.blur();\n this.gridElement.focus();\n return;\n }\n\n // Snapshot cell value on initial focus or when moving to a different cell.\n // This allows Escape to revert the cell to its pre-edit value.\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n const snap = this.#gridModeCellSnapshot;\n if (!snap || snap.rowIndex !== focusRow || snap.colIndex !== focusCol) {\n const column = internalGrid._visibleColumns?.[focusCol];\n const rowData = internalGrid._rows?.[focusRow];\n if (column?.field && rowData) {\n const field = column.field as string;\n this.#gridModeCellSnapshot = {\n rowIndex: focusRow,\n colIndex: focusCol,\n field,\n value: readCellField(rowData, field),\n };\n }\n }\n\n this.#gridModeInputFocused = true;\n }\n },\n { signal },\n );\n\n this.gridElement.addEventListener(\n 'focusout',\n (e: FocusEvent) => {\n const target = e.target as HTMLElement | null;\n const related = e.relatedTarget as HTMLElement | null;\n // Intra-editor focus moves MUST NOT flip the flag — the editor\n // is still logically focused. Cases observed in the wild:\n // - Native <select> opening: SELECT → child OPTION\n // - Native <select> popup ArrowDown: OPTION → sibling OPTION\n // - Framework editors moving focus to inner popup descendants\n // Detect via two ancestry checks: (a) `target` contains `related`\n // (parent-to-descendant), or (b) `related` lives inside an\n // editor element (input/select/textarea/contenteditable) — this\n // covers OPTION-to-OPTION and any other intra-editor traversal.\n // Exclude `gridElement` itself: it has tabindex=0 and so matches\n // FOCUSABLE_EDITOR_SELECTOR, but focus moving to the grid host\n // means we ARE leaving the editor — the explicit Escape/Enter\n // paths handle that by setting the flag to false themselves.\n if (target && related) {\n if (target.contains(related)) return;\n const relatedEditor = getEditorAncestor(related);\n if (relatedEditor && relatedEditor !== this.gridElement && this.gridElement.contains(relatedEditor)) {\n return;\n }\n }\n // Only clear if focus went outside grid (and external containers) or to a non-input element\n if (\n !related ||\n (!this.gridElement.contains(related) && !this.grid.containsFocus?.(related)) ||\n !related.matches(FOCUSABLE_EDITOR_SELECTOR)\n ) {\n this.#gridModeInputFocused = false;\n // Clear cell snapshot when leaving edit mode normally (not via Escape)\n this.#gridModeCellSnapshot = null;\n }\n },\n { signal },\n );\n\n // Handle Escape key directly on the grid element (capture phase)\n // This ensures we intercept Escape even when focus is inside an input\n this.gridElement.addEventListener(\n 'keydown',\n (e: KeyboardEvent) => {\n if (e.key === 'Escape' && this.#gridModeInputFocused) {\n // Check onBeforeEditClose to let overlays close first.\n if (shouldPreventEditClose(this.config, e)) {\n // Overlay is open — schedule deferred nav-mode transition\n // after the overlay tears down.\n queueMicrotask(() => {\n if (this.#gridModeInputFocused) {\n this.#enterGridModeNavigation();\n }\n });\n return;\n }\n this.#enterGridModeNavigation();\n e.preventDefault();\n e.stopPropagation();\n }\n },\n { capture: true, signal },\n );\n\n // Handle click on inputs - unlock edit mode when user explicitly clicks\n this.gridElement.addEventListener(\n 'mousedown',\n (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n if (target.matches(FOCUSABLE_EDITOR_SELECTOR)) {\n this.#gridModeEditLocked = false; // User clicked input - allow edit\n }\n },\n { signal },\n );\n }\n\n /** @internal */\n override detach(): void {\n const internalGrid = this.#internalGrid;\n internalGrid._isGridEditMode = false;\n this.gridElement.classList.remove('tbw-grid-mode');\n this.#activeEditRow = -1;\n this.#activeEditRowId = undefined;\n this.#activeEditRowRef = undefined;\n this.#activeEditCol = -1;\n this.#rowEditSnapshots.clear();\n this.#dirty.clear();\n this.#editingCells.clear();\n this.#editorValueCallbacks.clear();\n this.#gridModeInputFocused = false;\n this.#gridModeEditLocked = false;\n this.#gridModeCellSnapshot = null;\n this.#singleCellEdit = false;\n this.#seededFromAttr = new WeakSet<HTMLElement>();\n super.detach();\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'isEditing') {\n // In grid mode, we're always editing\n return this.#isGridMode || this.#activeEditRow !== -1;\n }\n if (query.type === 'getCellEditableResolver') {\n // Return a predicate that applies the plugin's full per-cell editability\n // resolution (rowEditable gate + column.editable true/false/function).\n // Fields are mapped to columns once; consumers (e.g. clipboard paste)\n // then ask per cell without importing editing-owned config.\n const columns = (this.#internalGrid.effectiveConfig?.columns as ColumnConfig<T>[] | undefined) ?? [];\n const byField = new Map<string, ColumnConfig<T>>();\n for (const col of columns) byField.set(col.field, col);\n const resolver: CellEditablePredicate = (field, row) => {\n const col = byField.get(field);\n return col ? this.#isCellEditable(col, row as T) : false;\n };\n return resolver;\n }\n if (query.type === 'commitCellValue') {\n return this.#handleCommitCellValue(query.context as CommitCellValueContext);\n }\n return undefined;\n }\n\n /**\n * Answer the core `commitCellValue` query dispatched from `updateRow`/\n * `updateRows`. Routes the change through the full edit pipeline so\n * programmatic mutations participate in validation, dirty tracking, history,\n * and cascade — exactly like an interactive edit.\n *\n * @returns `true` when applied, `false` when a `cell-commit` handler vetoed\n * it (abortion), or `undefined` when the field/row can't be resolved (core\n * then applies the value directly).\n * @internal\n */\n #handleCommitCellValue(ctx: CommitCellValueContext): boolean | undefined {\n const internalGrid = this.#internalGrid;\n const columns = (internalGrid._visibleColumns as ColumnConfig<T>[] | undefined) ?? [];\n const column =\n columns.find((c) => c.field === ctx.field) ??\n (internalGrid._columns as ColumnConfig<T>[] | undefined)?.find((c) => c.field === ctx.field);\n const rowData =\n (ctx.row as T | undefined) ??\n (ctx.rowId != null ? internalGrid._getRowEntry(ctx.rowId)?.row : undefined) ??\n (internalGrid._rows?.[ctx.rowIndex] as T | undefined);\n\n if (ctx.source === 'sync') {\n // Declarative host sync (a framework adapter replacing its `rows` prop in\n // place). Authoritative external data, NOT a user edit: apply the value and\n // re-baseline the cell so the new value is the pristine truth — the row is\n // never marked dirty and no undo/redo history is recorded (and no cascade).\n // Handled before the column guard so non-column fields (dirty tracking\n // snapshots the whole row) are re-baselined too.\n if (!rowData || !isSafePropertyKey(ctx.field)) return undefined;\n writeCellField(rowData, ctx.field, ctx.newValue);\n invalidateAccessorCache(rowData as object);\n const rowId = this.#safeGetRowId(rowData);\n if (rowId && this.config.dirtyTracking) {\n this.#dirty.rebaselineCell(rowId, ctx.field, ctx.newValue);\n const stillDirty = this.#dirty.isRowDirty(rowId, rowData);\n if (!stillDirty) this.#dirty.changedRowIds.delete(rowId);\n this.#emitDirtyChange(rowId, rowData, stillDirty ? 'modified' : 'pristine');\n }\n return true;\n }\n\n if (!column || !rowData || !isSafePropertyKey(ctx.field)) return undefined; // core applies directly\n\n if (ctx.source === 'history') {\n // Undo/redo re-application: apply the value but DO NOT record a fresh\n // history entry or mark the row changed — the undo stack owns this change.\n // Recompute dirty so the row's changed flag reflects its baseline.\n writeCellField(rowData, ctx.field, ctx.newValue);\n invalidateAccessorCache(rowData as object);\n const rowId = this.#safeGetRowId(rowData);\n if (rowId && this.config.dirtyTracking) {\n const dirty = this.#dirty.isRowDirty(rowId, rowData);\n if (!dirty) this.#dirty.changedRowIds.delete(rowId);\n this.#emitDirtyChange(rowId, rowData, dirty ? 'modified' : 'pristine');\n }\n return true;\n }\n\n // Normal path: full commit pipeline (cancelable cell-commit for\n // validation/abortion, dirty tracking, history via cell-edit-committed,\n // cascade). #commitCellValue leaves the value unchanged when a handler\n // vetoes, so compare to detect abortion.\n this.#commitCellValue(ctx.rowIndex, column, ctx.newValue, rowData, ctx.source);\n return readCellField(rowData, ctx.field) === ctx.newValue;\n }\n\n // #endregion\n\n // #region Event Handlers (event distribution)\n\n /**\n * Handle cell clicks - start editing if configured for click mode.\n * Both click and dblclick events come through this handler.\n * Starts row-based editing (all editable cells in the row get editors).\n * @internal\n */\n override onCellClick(event: CellClickEvent): boolean | void {\n // In grid mode, all cells are already editable - no need to trigger row edit\n if (this.#isGridMode) return false;\n\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n\n // Check if editing is disabled\n if (editOn === false || editOn === 'manual') return false;\n\n // Check if this is click or dblclick mode\n if (editOn !== 'click' && editOn !== 'dblclick') return false;\n\n // Check if the event type matches the edit mode\n const isDoubleClick = event.originalEvent.type === 'dblclick';\n if (editOn === 'click' && isDoubleClick) return false; // In click mode, only handle single clicks\n if (editOn === 'dblclick' && !isDoubleClick) return false; // In dblclick mode, only handle double clicks\n\n const { rowIndex } = event;\n\n // Check if any column in the row is potentially editable\n const hasEditableColumn = internalGrid._columns?.some((col) => this.#hasEditableConfig(col as ColumnConfig<T>));\n if (!hasEditableColumn) return false;\n\n // Row-level gate\n if (!this.#isRowEditable(internalGrid._rows[rowIndex] as T | undefined)) return false;\n\n // Start row-based editing (all editable cells get editors)\n event.originalEvent.stopPropagation();\n this.beginBulkEdit(rowIndex);\n return true; // Handled\n }\n\n /**\n * Handle keyboard events for edit lifecycle.\n * @internal\n */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n switch (event.key) {\n case 'Escape':\n return this.#onEscapeKey(event);\n case 'ArrowUp':\n case 'ArrowDown':\n case 'ArrowLeft':\n case 'ArrowRight':\n return this.#onArrowKey(event);\n case 'Tab':\n return this.#onTabKey(event);\n case ' ':\n case 'Spacebar':\n return this.#onSpaceKey(event);\n case 'Enter':\n // Modified Enter (Shift/Ctrl/Alt/Meta) is not ours — don't block it.\n if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) return false;\n return this.#onEnterKey(event);\n case 'F2':\n return this.#onF2Key(event);\n default:\n // Don't block other keyboard events\n return false;\n }\n }\n\n /**\n * Escape: cancel the current edit (row mode) or leave edit mode (grid mode).\n *\n * NOTE: No `onBeforeEditClose` check here — the capture-phase handler is the\n * authoritative overlay guard. If the event reaches this bubble-phase handler,\n * either (a) the capture handler already bailed (overlay was open) and the\n * template handler (e.g. `select.close()`) addressed it, or (b) no overlay was\n * open. In both cases we should transition to navigation mode.\n */\n #onEscapeKey(event: KeyboardEvent): boolean {\n // In grid mode: revert cell, blur input, enter navigation mode.\n if (this.#isGridMode && this.#gridModeInputFocused) {\n this.#enterGridModeNavigation();\n // Update focus styling\n this.requestAfterRender();\n return true;\n }\n\n // In row mode: cancel edit\n if (this.#activeEditRow !== -1 && !this.#isGridMode) {\n if (shouldPreventEditClose(this.config, event)) return true;\n this.#exitRowEdit(this.#activeEditRow, true);\n return true;\n }\n\n return false;\n }\n\n /**\n * Arrow keys — decides whether the grid navigates or the editor keeps the key.\n *\n * `#gridModeInputFocused` is a cached flag, so `event.target` is used as a\n * fallback: a stale flag (e.g. between a native `<select>` popup opening and\n * our focusout/focusin guard updating it) must not hijack ArrowUp/Down away\n * from a focused editor. Uses `closest()` (not `matches()`) so descendants of\n * an editor — most notably `<option>` children of an open native `<select>`\n * popup — also count as \"keydown originating inside an editor\".\n */\n #onArrowKey(event: KeyboardEvent): boolean {\n const arrowEditorAncestor = getEditorAncestor(event.target);\n const targetIsEditor = !!arrowEditorAncestor && arrowEditorAncestor !== this.gridElement;\n const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown';\n\n // Grid mode, no editor focused: let the grid's default navigation handle it.\n if (this.#isGridMode && !this.#gridModeInputFocused && !targetIsEditor) return false;\n\n // Grid mode with a focused editor: ArrowUp/Down belong to the editor\n // (e.g. ArrowDown opens autocomplete/datepicker overlays, ArrowUp/Down\n // navigates options). Handled: block grid navigation, let event reach editor.\n if (this.#isGridMode && (this.#gridModeInputFocused || targetIsEditor) && isVertical) return true;\n\n // Arrow Up/Down while a row is in edit mode (row mode): the editor owns\n // the key. The grid does NOT commit + jump to an adjacent row — the user\n // must explicitly leave edit mode (Enter / Escape / Tab / click) before\n // arrow keys resume cell navigation. Rationale:\n // - <input type=\"number\"> spinners must work natively\n // - <select> / native popups need ArrowUp/Down to traverse options\n // - <textarea> needs ArrowUp/Down to move the caret between lines\n // - Framework-adapter editors (Angular Material, MUI, etc.) commonly\n // bind ArrowUp/Down for autocomplete, datepicker, number-stepper,\n // and combobox interactions\n // Returning `true` blocks the core keyboard handler from running its\n // commit-and-navigate fallback (see `core/internal/keyboard.ts`'s\n // ArrowUp/Down switch case). The native event still reaches the focused\n // editor so it can consume it however it needs to.\n if (isVertical && this.#activeEditRow !== -1 && !this.#isGridMode) return true;\n\n return false;\n }\n\n /** Tab/Shift+Tab while editing: move to the next/previous editable cell. */\n #onTabKey(event: KeyboardEvent): boolean {\n if (this.#activeEditRow === -1 && !this.#isGridMode) return false;\n\n event.preventDefault();\n\n // In single-cell edit mode (F2), commit and close instead of navigating\n if (this.#singleCellEdit) {\n this.#exitRowEdit(this.#activeEditRow, false);\n return true;\n }\n\n this.#handleTabNavigation(!event.shiftKey);\n return true;\n }\n\n /** Space: toggle boolean cells (only when not editing — editors own their own space). */\n #onSpaceKey(event: KeyboardEvent): boolean {\n // Modified Space is a selection chord (Shift/Ctrl/⌘+Space) — never mutate data on it.\n if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) return false;\n // If we're in row edit mode, let the event pass through to the editor (e.g., checkbox)\n if (this.#activeEditRow !== -1) return false;\n\n const internalGrid = this.#internalGrid;\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n if (focusRow < 0 || focusCol < 0) return false;\n\n const column = internalGrid._visibleColumns[focusCol];\n const rowData = internalGrid._rows[focusRow];\n if (!column || !rowData || column.type !== 'boolean') return false;\n if (!this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return false;\n\n const field = column.field;\n // Space on a cell we cannot safely write - don't block keyboard navigation\n if (!isSafePropertyKey(field)) return false;\n\n this.#commitCellValue(focusRow, column, !readCellField(rowData, field), rowData);\n event.preventDefault();\n // Re-render to update the UI\n this.requestRender();\n return true;\n }\n\n /** Enter (unmodified): start row edit, commit, or enter edit mode in grid mode. */\n #onEnterKey(event: KeyboardEvent): boolean {\n // In grid mode when not editing: focus the current cell's input\n if (this.#isGridMode && !this.#gridModeInputFocused) {\n this.#focusCurrentCellEditor();\n return true;\n }\n\n if (this.#isGridMode && this.#gridModeInputFocused) return this.#onEnterWhileEditorFocused(event);\n\n if (this.#activeEditRow !== -1) {\n // ARIA-expanded fallback (#251): when the focused control declares\n // an open overlay via aria-expanded=\"true\" + aria-controls, defer\n // Enter to the overlay so combobox confirmation does not exit the\n // row. Mirrors the editor-injection.ts handler so the same guard\n // applies whether the editor host or the grid receives the event.\n const ariaTarget = event.target as HTMLElement | null;\n if (\n ariaTarget &&\n ariaTarget.getAttribute?.('aria-expanded') === 'true' &&\n ariaTarget.hasAttribute?.('aria-controls')\n ) {\n return false;\n }\n if (shouldPreventEditClose(this.config, event)) return true;\n // Already editing - let cell handlers deal with it\n return false;\n }\n\n return this.#beginRowEditFromEnter(event);\n }\n\n /**\n * Enter in grid mode while an editor (or its descendant — most notably an\n * `<option>` inside an open native `<select>` popup) has focus. Does NOT start\n * row-based editing or re-render. Two sub-cases:\n * - target is a **descendant** of an editor (popup option): let the browser's\n * native commit-and-close run untouched — calling `beginBulkEdit` /\n * re-rendering would destroy the open popup and abort the commit.\n * - target **is** the editor itself (focused SELECT after popup closed,\n * focused INPUT with no popup): commit the current value, blur the editor,\n * return focus to the grid so arrow keys resume cell navigation. Mirrors\n * Escape but accepts the value instead of reverting.\n */\n #onEnterWhileEditorFocused(event: KeyboardEvent): boolean {\n const enterEditorAncestor = getEditorAncestor(event.target);\n if (enterEditorAncestor && enterEditorAncestor !== event.target) {\n // Descendant — let native commit fire; do not interfere. Return true so\n // core's Enter handler (which would dispatch cell-activate) does not also\n // run. We don't call preventDefault, so the browser still commits the popup.\n return true;\n }\n // Editor itself — commit + blur + return to navigation mode.\n this.#gridModeCellSnapshot = null;\n const activeEl = document.activeElement as HTMLElement | null;\n if (activeEl && this.gridElement.contains(activeEl)) {\n activeEl.blur();\n this.gridElement.focus();\n }\n this.#gridModeInputFocused = false;\n this.#gridModeEditLocked = true;\n this.requestAfterRender();\n return true;\n }\n\n /**\n * Enter on a focused cell with no edit in progress: start row-based editing\n * (not just the focused cell), after giving consumers a cancelable\n * `cell-activate` event.\n */\n #beginRowEditFromEnter(event: KeyboardEvent): boolean {\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n if (editOn === false || editOn === 'manual') return false;\n\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n if (focusRow < 0) return false;\n\n // Check if ANY column in the row is potentially editable\n const hasEditableColumn = internalGrid._columns?.some((col) => this.#hasEditableConfig(col as ColumnConfig<T>));\n // Row-level gate. No editable columns - don't block keyboard navigation.\n const row = internalGrid._rows[focusRow];\n if (!hasEditableColumn || !this.#isRowEditable(row as T | undefined)) return false;\n\n // Emit cell-activate event BEFORE starting edit\n // This ensures consumers always get the activation event\n const column = internalGrid._visibleColumns[focusCol];\n const field = column?.field ?? '';\n const value = field && row ? readCellField(row, field) : undefined;\n const cellEl = this.gridElement.querySelector(`[data-row=\"${focusRow}\"][data-col=\"${focusCol}\"]`) as\n HTMLElement | undefined;\n\n const activateEvent = new CustomEvent('cell-activate', {\n cancelable: true,\n bubbles: true,\n detail: {\n rowIndex: focusRow,\n colIndex: focusCol,\n field,\n value,\n row,\n cellEl,\n trigger: 'keyboard' as const,\n originalEvent: event,\n },\n });\n this.gridElement.dispatchEvent(activateEvent);\n\n // If consumer canceled the activation, don't start editing\n if (activateEvent.defaultPrevented) {\n event.preventDefault();\n return true;\n }\n\n this.beginBulkEdit(focusRow);\n return true;\n }\n\n /** F2: begin single-cell edit on the focused cell. */\n #onF2Key(event: KeyboardEvent): boolean {\n if (this.#activeEditRow !== -1 || this.#isGridMode) return false;\n\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n if (editOn === false) return false;\n\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n if (focusRow < 0 || focusCol < 0) return false;\n\n const column = internalGrid._visibleColumns[focusCol];\n const rowData = internalGrid._rows[focusRow];\n if (!column || !rowData || !column.field) return false;\n if (!this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return false;\n\n event.preventDefault();\n this.beginCellEdit(focusRow, column.field);\n return true;\n }\n\n // #endregion\n\n // #region Render Hooks\n\n /**\n * Parse `data-*` attributes from a `<tbw-grid-type>` element into a camelCased map.\n */\n #readTypeParams(typeEl: HTMLElement): Record<string, unknown> {\n const params: Record<string, unknown> = {};\n for (const attr of Array.from(typeEl.attributes)) {\n if (!attr.name.startsWith('data-')) continue;\n const key = attr.name\n .slice(5)\n .replace(/-([a-z])/g, (_m, letter: string) => letter.toUpperCase())\n .trim();\n if (!key) continue;\n params[key] = attr.value;\n }\n return params;\n }\n\n /**\n * Seed type-level editor defaults from declarative `<tbw-grid-type>` light-DOM.\n *\n * This mirrors the column-level `<tbw-grid-column-editor>` behavior: editor\n * templates are editing-plugin-owned and only become active when the editing\n * plugin is imported.\n */\n #seedTypeEditorsFromLightDom(): void {\n const internalGrid = this.#internalGrid;\n const host = internalGrid._hostElement;\n if (!host) return;\n\n const effectiveConfig = internalGrid.effectiveConfig;\n if (!effectiveConfig) return;\n\n const typeElements = Array.from(host.children).filter((el) => el.tagName.toLowerCase() === 'tbw-grid-type');\n if (!typeElements.length) return;\n\n const adapter = internalGrid.__frameworkAdapter;\n const typeDefaults = (effectiveConfig.typeDefaults ??= {});\n\n for (let i = 0; i < typeElements.length; i++) {\n const typeEl = typeElements[i] as HTMLElement;\n const name = typeEl.getAttribute('name')?.trim();\n if (!name) continue;\n\n const existing = typeDefaults[name];\n // Programmatic (or already-seeded) editor always wins.\n if (existing?.editor) continue;\n\n const editorTpl = typeEl.querySelector('tbw-grid-column-editor') as HTMLElement | null;\n const editorTarget = (editorTpl ?? typeEl) as HTMLElement;\n\n let editor: TypeDefault<T>['editor'] | undefined = adapter?.canHandle?.(editorTarget)\n ? (adapter.createEditor?.(editorTarget) as TypeDefault<T>['editor'] | undefined)\n : undefined;\n\n if (!editor && editorTpl) {\n const params = this.#readTypeParams(typeEl);\n const compiled = compileTemplate(editorTpl.innerHTML);\n editor = ((ctx) =>\n compiled({\n row: ctx.row as T,\n value: ctx.value,\n field: ctx.field,\n column: ctx.column as ColumnInternal<T>,\n typeDefault: params,\n })) as TypeDefault<T>['editor'];\n }\n\n if (!editor) continue;\n\n typeDefaults[name] = { ...(existing ?? {}), editor };\n }\n }\n\n /**\n * Process columns to merge type-level editorParams with column-level and to\n * read editing-owned attributes from declarative `<tbw-grid-column>` markup.\n *\n * Column-level params take precedence over type-level. Editing-owned\n * attributes (`editable`, `<tbw-grid-column-editor>` template) are parsed\n * here from `col.__element` rather than in core, so the grid carries no\n * editing knowledge (issue #272).\n * @internal\n */\n override processColumns(columns: ColumnConfig<T>[]): ColumnConfig<T>[] {\n const internalGrid = this.#internalGrid;\n this.#seedTypeEditorsFromLightDom();\n const typeDefaults = (internalGrid as any).effectiveConfig?.typeDefaults;\n const adapter = internalGrid.__frameworkAdapter;\n const hasTypeDefaults = !!typeDefaults || !!adapter?.getTypeDefault;\n\n return columns.map((col) => {\n // Read editing-owned attributes from the originating light-DOM element.\n // Each element is seeded at most once: the attribute supplies the INITIAL\n // value only, so config and later runtime changes always win and are not\n // overridden by re-reading the attribute on subsequent passes (#272).\n const el = (col as ColumnInternal<T>).__element;\n if (el && !this.#seededFromAttr.has(el)) {\n this.#seededFromAttr.add(el);\n // `col.editable == null` lets a programmatic config value (including an\n // explicit `editable: false`) win over the attribute on this first pass.\n if (col.editable == null && el.hasAttribute('editable')) {\n col.editable = el.getAttribute('editable') !== 'false';\n }\n const internalCol = col as ColumnInternal<T>;\n if (!internalCol.__editorTemplate) {\n const editorTpl = el.querySelector('tbw-grid-column-editor');\n if (editorTpl) internalCol.__editorTemplate = editorTpl as HTMLElement;\n }\n }\n\n if (!col.type || !hasTypeDefaults) return col;\n\n // Get type-level editorParams\n let typeEditorParams: EditorParams | undefined;\n\n // Check grid-level typeDefaults first\n if (typeDefaults?.[col.type]?.editorParams) {\n typeEditorParams = typeDefaults[col.type].editorParams;\n }\n\n // Then check app-level (adapter) typeDefaults\n if (!typeEditorParams && adapter?.getTypeDefault) {\n const appDefault = adapter.getTypeDefault<T>(col.type, internalGrid._hostElement);\n if (appDefault?.editorParams) {\n typeEditorParams = appDefault.editorParams;\n }\n }\n\n // No type-level params to merge\n if (!typeEditorParams) return col;\n\n // Merge: type-level as base, column-level wins on conflicts\n return {\n ...col,\n editorParams: { ...typeEditorParams, ...col.editorParams },\n };\n });\n }\n\n /**\n * Stabilize the actively edited row across `rows` array replacements and\n * capture dirty tracking baselines.\n *\n * **Editing stability:** When the consumer reassigns `grid.rows` while\n * editing, the full pipeline (sort, filter, group) runs on the new data.\n * This hook finds the edited row in the new array by ID and swaps in the\n * in-progress row reference (`#activeEditRowRef`) so editors survive.\n *\n * **Dirty tracking baselines:** When `dirtyTracking` is enabled, captures\n * a `structuredClone` snapshot of each row on first appearance\n * (first-write-wins). This prevents Angular's feedback loop from\n * overwriting baselines.\n *\n * @internal Plugin API — part of the render pipeline\n */\n override processRows(rows: readonly T[]): T[] {\n // --- Dirty tracking: capture baselines (first-write-wins) ---\n if (this.config.dirtyTracking) this.#captureBaselines(rows);\n\n // --- Editing stability: swap in the in-progress row ---\n if (this.#activeEditRow === -1 || this.#isGridMode) return rows as T[];\n\n const editRowId = this.#activeEditRowId;\n const editRowRef = this.#activeEditRowRef;\n\n // Without a stable row ID we cannot match across array replacements\n if (!editRowId || !editRowRef) return rows as T[];\n\n const result = [...rows] as T[];\n\n // Find the edited row's new position by ID\n let newIndex = -1;\n for (let i = 0; i < result.length; i++) {\n if (this.#safeGetRowId(result[i]) === editRowId) {\n newIndex = i;\n break;\n }\n }\n\n if (newIndex === -1) {\n // Row was deleted server-side — close the editor.\n // Cannot close synchronously during the processRows pipeline;\n // schedule for after the current render cycle completes.\n setTimeout(() => this.cancelActiveRowEdit(), 0);\n return result;\n }\n\n // Swap in the in-progress row data to preserve editor state\n result[newIndex] = editRowRef;\n\n // Update index-keyed state if the position changed (due to sort/filter)\n if (this.#activeEditRow !== newIndex) {\n this.#migrateEditRowIndex(this.#activeEditRow, newIndex);\n }\n\n return result;\n }\n\n /**\n * After render, reapply editors to cells in edit mode.\n * This handles virtualization - when a row scrolls back into view,\n * we need to re-inject the editor.\n * @internal\n */\n override afterRender(): void {\n const internalGrid = this.#internalGrid;\n\n // --- Editing stability: verify active edit row index ---\n // After processRows, subsequent plugins (filtering, grouping) may have\n // shifted row indices. Verify the index is still correct and fix if needed\n // before re-injecting editors.\n if (this.#activeEditRow !== -1 && this.#activeEditRowRef && !this.#isGridMode) {\n if (internalGrid._rows[this.#activeEditRow] !== this.#activeEditRowRef) {\n const newIndex = (internalGrid._rows as T[]).indexOf(this.#activeEditRowRef);\n if (newIndex !== -1) {\n this.#migrateEditRowIndex(this.#activeEditRow, newIndex);\n } else {\n // Row no longer in rendered set (filtered out or deleted)\n setTimeout(() => this.cancelActiveRowEdit(), 0);\n return;\n }\n }\n }\n\n // Restore focus after exiting edit mode\n if (this.#pendingFocusRestore) {\n this.#pendingFocusRestore = false;\n this.#restoreCellFocus(internalGrid);\n }\n\n // Animate the row after render completes (so the row element exists)\n if (this.#pendingRowAnimation !== -1) {\n const rowIndex = this.#pendingRowAnimation;\n this.#pendingRowAnimation = -1;\n internalGrid.animateRow?.(rowIndex, 'change');\n }\n\n // Emit baselines-captured event when new baselines were captured this cycle.\n // Emitted post-render so consumers can safely read grid.rows, query the DOM,\n // or call getOriginalRow() in their handler.\n const capturedCount = this.#dirty.drainCapturedFlag();\n if (capturedCount != null) {\n this.emit<BaselinesCapturedDetail>('baselines-captured', {\n count: capturedCount,\n });\n }\n\n // In 'grid' mode, editors are injected via afterCellRender hook during render\n if (this.#isGridMode) return;\n\n if (this.#editingCells.size === 0) return;\n\n // Re-inject editors for any editing cells that are visible\n for (const cellKey of this.#editingCells) {\n const [rowStr, colStr] = cellKey.split(':');\n const rowIndex = parseInt(rowStr, 10);\n const colIndex = parseInt(colStr, 10);\n\n const cellEl = this.#getCell(rowIndex, colIndex);\n if (!cellEl || cellEl.classList.contains('editing')) continue;\n\n // Cell is visible but not in editing mode - reinject editor\n const rowData = internalGrid._rows[rowIndex];\n const column = internalGrid._visibleColumns[colIndex];\n if (rowData && column) {\n this.#injectEditor(rowData, rowIndex, column, colIndex, cellEl, true);\n }\n }\n }\n\n /**\n * Hook called after each cell is rendered.\n * In grid mode, injects editors into editable cells during render (no DOM queries needed).\n * @internal\n */\n override afterCellRender(context: AfterCellRenderContext): void {\n const { row, rowIndex, column, colIndex, cellElement } = context;\n\n const editable = this.#isCellEditable(column as ColumnConfig<T>, row as T);\n\n // Set aria-readonly on non-editable cells so screen readers can distinguish\n if (!editable) {\n cellElement.setAttribute('aria-readonly', 'true');\n // In grid mode, tear down stale editors on cells that are no longer editable\n // (e.g., after virtualization recycles a row element for different row data)\n if (this.#isGridMode && cellElement.classList.contains('editing')) {\n cellElement.classList.remove('editing');\n const value = readCellField(row, (column as ColumnConfig<T>).field);\n cellElement.textContent = value == null ? '' : String(value);\n }\n } else {\n cellElement.removeAttribute('aria-readonly');\n }\n\n // Only inject editors in grid mode\n if (!this.#isGridMode) return;\n\n // Skip non-editable cells\n if (!editable) return;\n\n // Skip if already has editor\n if (cellElement.classList.contains('editing')) return;\n\n // Inject editor (don't track in editingCells - we're always editing in grid mode)\n // Pass rowElement so incrementEditingCount works even when cell is in a DocumentFragment\n this.#injectEditor(row as T, rowIndex, column as ColumnConfig<T>, colIndex, cellElement, true, context.rowElement);\n }\n\n /**\n * Apply dirty-tracking CSS classes to each rendered row.\n *\n * - `tbw-cell-dirty` on individual cells whose value differs from baseline\n * (applied on cell-commit, visible during editing)\n * - `tbw-row-dirty` on the row element only after the row edit session is\n * committed (edit-close without cancel)\n * - `tbw-row-new` when a row was inserted via `insertRow()` with no baseline\n *\n * Only active when `dirtyTracking: true`.\n *\n * @internal Plugin API\n */\n override afterRowRender(context: AfterRowRenderContext): void {\n if (!this.config.dirtyTracking) return;\n\n const internalGrid = this.#internalGrid;\n const rowId = internalGrid.getRowId?.(context.row as T);\n if (!rowId) return;\n\n const { isNew, isCommittedDirty, hasBaseline } = this.#dirty.getRowDirtyState(rowId, context.row as T);\n\n const el = context.rowElement;\n\n // Row-level classes (tbw-row-dirty only after row-commit AND data differs)\n el.classList.toggle('tbw-row-dirty', isCommittedDirty);\n el.classList.toggle('tbw-row-new', isNew);\n\n // Cell-level classes (tbw-cell-dirty on individual cells with changed values)\n // Only run the per-cell loop when the row has a baseline — avoids\n // querySelectorAll on the hot path for rows without dirty tracking state.\n if (hasBaseline) {\n const cells = el.querySelectorAll('.cell[data-field]');\n for (let i = 0; i < cells.length; i++) {\n const cell = cells[i] as HTMLElement;\n const field = cell.getAttribute('data-field');\n if (field) {\n cell.classList.toggle('tbw-cell-dirty', this.#dirty.isCellDirty(rowId, context.row as T, field));\n }\n }\n } else {\n // Clean up stale tbw-cell-dirty classes on recycled row elements\n // that previously displayed a dirty row but now show a pristine one.\n const dirtyCells = el.querySelectorAll('.tbw-cell-dirty');\n for (let i = 0; i < dirtyCells.length; i++) {\n dirtyCells[i].classList.remove('tbw-cell-dirty');\n }\n }\n }\n\n /**\n * On scroll render, reapply editors to recycled cells.\n * @internal\n */\n override onScrollRender(): void {\n this.afterRender();\n }\n\n // #endregion\n\n // #region Public API\n\n /**\n * Get all rows that have been modified.\n * Uses ID-based lookup for stability when rows are reordered.\n */\n get changedRows(): T[] {\n return this.#dirty.getChangedRows((id) => this.grid.getRow(id) as T | undefined);\n }\n\n /**\n * Get IDs of all modified rows.\n */\n get changedRowIds(): string[] {\n return this.#dirty.getChangedRowIds();\n }\n\n /**\n * Get the currently active edit row index, or -1 if not editing.\n */\n get activeEditRow(): number {\n return this.#activeEditRow;\n }\n\n /**\n * Get the currently active edit column index, or -1 if not editing.\n */\n get activeEditCol(): number {\n return this.#activeEditCol;\n }\n\n /**\n * Check if a specific row is currently being edited.\n */\n isRowEditing(rowIndex: number): boolean {\n return this.#activeEditRow === rowIndex;\n }\n\n /**\n * Check if a specific cell is currently being edited.\n */\n isCellEditing(rowIndex: number, colIndex: number): boolean {\n return this.#editingCells.has(`${rowIndex}:${colIndex}`);\n }\n\n /**\n * Check if a specific row has been modified.\n * @param rowIndex - Row index to check (will be converted to ID internally)\n */\n isRowChanged(rowIndex: number): boolean {\n const internalGrid = this.#internalGrid;\n const row = internalGrid._rows[rowIndex];\n if (!row) return false;\n try {\n const rowId = internalGrid.getRowId?.(row);\n return rowId ? this.#dirty.isRowChanged(rowId) : false;\n } catch {\n return false;\n }\n }\n\n /**\n * Check if a row with the given ID has been modified.\n * @param rowId - Row ID to check\n */\n isRowChangedById(rowId: string): boolean {\n return this.#dirty.isRowChanged(rowId);\n }\n\n // #region Dirty Tracking API (delegated to DirtyTrackingManager)\n\n /** Check if a row differs from its baseline. Requires `dirtyTracking: true`. */\n isDirty(rowId: string): boolean {\n if (!this.config.dirtyTracking) return false;\n if (this.#dirty.newRowIds.has(rowId)) return true;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return false;\n return this.#dirty.isRowDirty(rowId, row);\n }\n\n /** Inverse of `isDirty`. */\n isPristine(rowId: string): boolean {\n return !this.isDirty(rowId);\n }\n\n /** Whether any row in the grid is dirty. */\n get dirty(): boolean {\n if (!this.config.dirtyTracking) return false;\n const internalGrid = this.#internalGrid;\n return this.#dirty.hasAnyDirty((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /** Whether all rows are pristine. */\n get pristine(): boolean {\n return !this.dirty;\n }\n\n /** Re-snapshot baseline from current data (call after a successful save). */\n markAsPristine(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return;\n this.#dirty.markPristine(rowId, row);\n // After mark-pristine, original === current — emit row as the canonical value.\n this.emit<DirtyChangeDetail<T>>('dirty-change', {\n rowId,\n row,\n original: row,\n type: 'pristine',\n });\n }\n\n /** Mark a row as new (auto-called by `insertRow()` when dirty tracking is on). */\n markAsNew(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n this.#dirty.markNew(rowId);\n const row = this.grid.getRow(rowId) as T | undefined;\n // `original` is intentionally undefined for new rows — emit directly\n // rather than via #emitDirtyChange (which always derives original from\n // the baseline map).\n this.emit<DirtyChangeDetail<T>>('dirty-change', {\n rowId,\n row: row as T,\n original: undefined,\n type: 'new',\n });\n }\n\n /** Mark a row as dirty after an external mutation that bypassed the editing pipeline. */\n markAsDirty(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return;\n this.#dirty.markDirty(rowId);\n this.#emitDirtyChange(rowId, row, 'modified');\n }\n\n /** Mark all tracked rows as pristine (call after a batch save). */\n markAllPristine(): void {\n if (!this.config.dirtyTracking) return;\n const internalGrid = this.#internalGrid;\n this.#dirty.markAllPristine((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /** Get a deep clone of the original (baseline) row data. Returns `undefined` for new rows. */\n getOriginalRow(rowId: string): T | undefined {\n if (!this.config.dirtyTracking) return undefined;\n return this.#dirty.getOriginalRow(rowId);\n }\n\n /** Lightweight check for whether a baseline exists (no cloning). */\n hasBaseline(rowId: string): boolean {\n if (!this.config.dirtyTracking) return false;\n return this.#dirty.hasBaseline(rowId);\n }\n\n /** Get all dirty rows with their original and current data. */\n getDirtyRows(): DirtyRowEntry<T>[] {\n if (!this.config.dirtyTracking) return [];\n const internalGrid = this.#internalGrid;\n return this.#dirty.getDirtyRows((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /**\n * Get IDs of all dirty rows.\n */\n get dirtyRowIds(): string[] {\n if (!this.config.dirtyTracking) return [];\n const internalGrid = this.#internalGrid;\n return this.#dirty.getDirtyRowIds((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n }\n\n /**\n * Revert a row to its baseline values (mutates the current row in-place).\n * Triggers a re-render.\n *\n * @param rowId - Row ID (from `getRowId`)\n */\n revertRow(rowId: string): void {\n if (!this.config.dirtyTracking) return;\n const row = this.grid.getRow(rowId) as T | undefined;\n if (!row) return;\n if (this.#dirty.revertRow(rowId, row)) {\n this.#emitDirtyChange(rowId, row, 'reverted');\n this.emitPluginEvent('cell-edit-reverted', { rowId });\n this.requestRender();\n }\n }\n\n /**\n * Revert all dirty rows to their baseline values and re-render.\n */\n revertAll(): void {\n if (!this.config.dirtyTracking) return;\n const internalGrid = this.#internalGrid;\n this.#dirty.revertAll((rowId) => internalGrid._getRowEntry(rowId)?.row as T | undefined);\n this.emitPluginEvent('cell-edit-reverted', {});\n this.requestRender();\n }\n\n // #endregion\n\n // #region Cell Validation (delegated to CellValidationManager)\n\n /**\n * Mark a cell as invalid with an optional validation message.\n * Invalid cells are marked with a `data-invalid` attribute for styling.\n *\n * @param rowId - The row ID (from getRowId)\n * @param field - The field name\n * @param message - Optional validation message (for tooltips or display)\n *\n * @example\n * ```typescript\n * // In cell-commit handler:\n * grid.on('cell-commit', (detail, e) => {\n * if (detail.field === 'email' && !isValidEmail(detail.value)) {\n * detail.setInvalid('Invalid email format');\n * }\n * });\n *\n * // Or programmatically:\n * editingPlugin.setInvalid('row-123', 'email', 'Invalid email format');\n * ```\n */\n setInvalid(rowId: string, field: string, message = ''): void {\n this.#validation.setInvalid(rowId, field, message);\n }\n\n /** Clear the invalid state for a specific cell. */\n clearInvalid(rowId: string, field: string): void {\n this.#validation.clearInvalid(rowId, field);\n }\n\n /** Clear all invalid cells for a specific row. */\n clearRowInvalid(rowId: string): void {\n this.#validation.clearRowInvalid(rowId);\n }\n\n /** Clear all invalid cell states across all rows. */\n clearAllInvalid(): void {\n this.#validation.clearAllInvalid();\n }\n\n /** Check if a specific cell is marked as invalid. */\n isCellInvalid(rowId: string, field: string): boolean {\n return this.#validation.isCellInvalid(rowId, field);\n }\n\n /** Get the validation message for an invalid cell. */\n getInvalidMessage(rowId: string, field: string): string | undefined {\n return this.#validation.getInvalidMessage(rowId, field);\n }\n\n /** Check if a row has any invalid cells. */\n hasInvalidCells(rowId: string): boolean {\n return this.#validation.hasInvalidCells(rowId);\n }\n\n /** Get all invalid fields for a row. */\n getInvalidFields(rowId: string): Map<string, string> {\n return this.#validation.getInvalidFields(rowId);\n }\n\n // #endregion\n\n /**\n * Reset all change tracking.\n * @param silent - If true, suppresses the `changed-rows-reset` event\n * @fires changed-rows-reset - Emitted when tracking is reset (unless silent)\n */\n resetChangedRows(silent?: boolean): void {\n const rows = this.changedRows;\n const ids = this.changedRowIds;\n this.#dirty.changedRowIds.clear();\n this.#dirty.committedDirtyRowIds.clear();\n this.#syncGridEditState();\n\n if (!silent) {\n this.emit<ChangedRowsResetDetail<T>>('changed-rows-reset', { rows, ids });\n }\n\n // Clear visual indicators\n const internalGrid = this.#internalGrid;\n internalGrid._rowPool?.forEach((r) => r.classList.remove('changed'));\n }\n\n /**\n * Programmatically begin editing a cell.\n * @param rowIndex - Index of the row to edit\n * @param field - Field name of the column to edit\n * @fires cell-commit - Emitted when the cell value is committed (on blur or Enter)\n */\n beginCellEdit(rowIndex: number, field: string): void {\n const internalGrid = this.#internalGrid;\n const colIndex = internalGrid._visibleColumns.findIndex((c) => c.field === field);\n if (colIndex === -1) return;\n\n const column = internalGrid._visibleColumns[colIndex];\n const rowData = internalGrid._rows[rowIndex];\n if (!column || !rowData || !this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return;\n\n const cellEl = this.#getCell(rowIndex, colIndex);\n if (!cellEl) return;\n\n this.#singleCellEdit = true;\n this.#beginCellEdit(rowIndex, colIndex, cellEl);\n }\n\n /**\n * Programmatically begin editing all editable cells in a row.\n * @param rowIndex - Index of the row to edit\n * @fires cell-commit - Emitted for each cell value that is committed\n * @fires row-commit - Emitted when focus leaves the row\n */\n beginBulkEdit(rowIndex: number): void {\n const internalGrid = this.#internalGrid;\n const editOn = this.config.editOn ?? internalGrid.effectiveConfig?.editOn;\n if (editOn === false) return;\n\n const hasEditableColumn = internalGrid._columns?.some((col) => this.#hasEditableConfig(col as ColumnConfig<T>));\n if (!hasEditableColumn) return;\n\n const rowEl = internalGrid.findRenderedRowElement?.(rowIndex);\n if (!rowEl) return;\n\n // Row-level gate\n const rowData = internalGrid._rows[rowIndex];\n if (!this.#isRowEditable(rowData as T | undefined)) return;\n\n // Bulk edit clears single-cell mode\n this.#singleCellEdit = false;\n\n // Start row edit\n this.#startRowEdit(rowIndex, rowData);\n\n // Enter edit mode on all editable cells\n Array.from(rowEl.children).forEach((cell, i) => {\n const col = internalGrid._visibleColumns[i];\n if (col && this.#isCellEditable(col as ColumnConfig<T>, rowData as T)) {\n const cellEl = cell as HTMLElement;\n if (!cellEl.classList.contains('editing')) {\n this.#injectEditor(rowData, rowIndex, col, i, cellEl, true);\n }\n }\n });\n\n // Focus the first editable cell\n setTimeout(() => {\n let targetCell = rowEl.querySelector(`.cell[data-col=\"${internalGrid._focusCol}\"]`);\n if (!targetCell?.classList.contains('editing')) {\n targetCell = rowEl.querySelector('.cell.editing');\n }\n if (targetCell?.classList.contains('editing')) {\n const editor = (targetCell as HTMLElement).querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n try {\n editor?.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n }\n }, 0);\n }\n\n /**\n * Commit the currently active row edit.\n * @fires row-commit - Emitted after the row edit is committed\n */\n commitActiveRowEdit(): void {\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, false);\n }\n }\n\n /**\n * Cancel the currently active row edit.\n */\n cancelActiveRowEdit(): void {\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, true);\n }\n }\n\n // #endregion\n\n // #region Internal Methods\n\n // --- Small private helpers (centralised to keep call sites uniform) ---\n\n /**\n * Safely resolve a row's ID via the configured `getRowId`. Returns\n * `undefined` when no ID can be derived (no `getRowId` callback, or it\n * threw — e.g. row has no key).\n */\n #safeGetRowId(row: T | undefined | null): string | undefined {\n if (!row) return undefined;\n try {\n return this.#internalGrid.getRowId?.(row);\n } catch {\n return undefined;\n }\n }\n\n /**\n * Resolve a rendered cell element by row + column index. Returns `null`\n * when the row is not currently rendered or the cell is missing.\n */\n #getCell(rowIndex: number, colIndex: number): HTMLElement | null {\n const rowEl = this.#internalGrid.findRenderedRowElement?.(rowIndex);\n return (rowEl?.querySelector(`.cell[data-col=\"${colIndex}\"]`) as HTMLElement | null) ?? null;\n }\n\n /**\n * Row-level editability gate. Returns `false` only when\n * `gridConfig.rowEditable` is configured AND vetoes the row. Rows without\n * data return `true` (defer to the column-level check).\n */\n #isRowEditable(rowData: T | undefined): boolean {\n if (!rowData) return true;\n const rowEditable = this.#internalGrid.effectiveConfig?.rowEditable;\n if (!rowEditable) return true;\n return rowEditable(rowData as any) !== false;\n }\n\n /**\n * Emit a `dirty-change` event with the canonical `{ rowId, row, original, type }`\n * shape. Centralises baseline lookup so every emit site uses the same\n * source of truth.\n */\n #emitDirtyChange(rowId: string, row: T | undefined, type: DirtyChangeDetail<T>['type']): void {\n this.emit<DirtyChangeDetail<T>>('dirty-change', {\n rowId,\n row: row as T,\n original: this.#dirty.getOriginalRow(rowId),\n type,\n });\n }\n\n /**\n * Grid-mode \"Escape → navigation mode\" transition: revert the cell from\n * the focus snapshot, blur the active editor, return focus to the grid,\n * and flip the navigation/lock flags. Used by the capture-phase Escape\n * handler (both deferred and sync branches) and the bubble-phase Escape\n * handler in `onKeyDown`.\n */\n #enterGridModeNavigation(): void {\n this.#revertGridModeCellEdit();\n const activeEl = document.activeElement as HTMLElement | null;\n if (activeEl && this.gridElement.contains(activeEl)) {\n activeEl.blur();\n this.gridElement.focus();\n }\n this.#gridModeInputFocused = false;\n this.#gridModeEditLocked = true;\n }\n\n /**\n * Restore a row to its snapshot values and clear all per-row dirty\n * bookkeeping. Called both from the explicit-revert path and from the\n * `row-commit` cancellation path in `#exitRowEdit`.\n */\n #revertRowFromSnapshot(rowId: string | undefined, current: T, snapshot: T | undefined): void {\n if (!snapshot) return;\n Object.keys(snapshot as object).forEach((k) => {\n (current as Record<string, unknown>)[k] = (snapshot as Record<string, unknown>)[k];\n });\n if (rowId) {\n this.#dirty.changedRowIds.delete(rowId);\n this.#dirty.committedDirtyRowIds.delete(rowId);\n this.clearRowInvalid(rowId);\n }\n this.emitPluginEvent('cell-edit-reverted', { rowId });\n }\n\n /**\n * Sync the data-invalid attribute on a cell element.\n * Used as the DOM callback for CellValidationManager.\n */\n #syncInvalidCellAttribute(rowId: string, field: string, invalid: boolean): void {\n const internalGrid = this.#internalGrid;\n const colIndex = internalGrid._visibleColumns?.findIndex((c) => c.field === field);\n if (colIndex === -1 || colIndex === undefined) return;\n\n // Find the row element by rowId\n const rows = internalGrid._rows;\n const rowIndex = rows?.findIndex((r) => this.#safeGetRowId(r as T) === rowId);\n if (rowIndex === -1 || rowIndex === undefined) return;\n\n const cellEl = this.#getCell(rowIndex, colIndex);\n if (!cellEl) return;\n\n if (invalid) {\n cellEl.setAttribute('data-invalid', 'true');\n // `data-invalid` is styling only; `aria-invalid` is what actually reaches a\n // screen reader (SC 3.3.1 Error Identification).\n cellEl.setAttribute('aria-invalid', 'true');\n const message = this.#validation.getInvalidMessage(rowId, field);\n if (message) {\n cellEl.setAttribute('title', message);\n // Hand the title over: core would otherwise reclaim it as its own\n // truncation tooltip on the next hover and overwrite the message.\n cellEl.removeAttribute(GridDataAttrs.TRUNCATED);\n }\n } else {\n cellEl.removeAttribute('data-invalid');\n cellEl.removeAttribute('aria-invalid');\n cellEl.removeAttribute('title');\n }\n }\n\n /**\n * Migrate all index-keyed editing state when the active edit row moves to\n * a different position in `_rows` (e.g. after sort, filter, or new data push).\n *\n * Updates: `#activeEditRow`, `#editingCells`, `#rowEditSnapshots`,\n * `#editorValueCallbacks`, and syncs `_activeEditRows` on the grid.\n */\n #migrateEditRowIndex(oldIndex: number, newIndex: number): void {\n this.#activeEditRow = newIndex;\n\n // Migrate #editingCells keys (\"rowIndex:colIndex\")\n const migratedCells = new Set<string>();\n const prefix = `${oldIndex}:`;\n for (const cellKey of this.#editingCells) {\n if (cellKey.startsWith(prefix)) {\n migratedCells.add(`${newIndex}:${cellKey.substring(prefix.length)}`);\n } else {\n migratedCells.add(cellKey);\n }\n }\n this.#editingCells.clear();\n for (const key of migratedCells) {\n this.#editingCells.add(key);\n }\n\n // Migrate #rowEditSnapshots key\n const snapshot = this.#rowEditSnapshots.get(oldIndex);\n if (snapshot !== undefined) {\n this.#rowEditSnapshots.delete(oldIndex);\n this.#rowEditSnapshots.set(newIndex, snapshot);\n }\n\n // Migrate #editorValueCallbacks keys (\"rowIndex:field\")\n const updates: [string, (newValue: unknown) => void][] = [];\n for (const [key, cb] of this.#editorValueCallbacks) {\n if (key.startsWith(prefix)) {\n updates.push([`${newIndex}:${key.substring(prefix.length)}`, cb]);\n this.#editorValueCallbacks.delete(key);\n }\n }\n for (const [key, cb] of updates) {\n this.#editorValueCallbacks.set(key, cb);\n }\n\n // Sync the grid's rendering state so rows.ts checks the correct index\n this.#syncGridEditState();\n }\n\n /**\n * Begin editing a single cell.\n */\n #beginCellEdit(rowIndex: number, colIndex: number, cellEl: HTMLElement): void {\n const internalGrid = this.#internalGrid;\n const rowData = internalGrid._rows[rowIndex];\n const column = internalGrid._visibleColumns[colIndex];\n\n if (!rowData || !column || !this.#isCellEditable(column as ColumnConfig<T>, rowData as T)) return;\n if (cellEl.classList.contains('editing')) return;\n\n // Start row edit if not already\n if (this.#activeEditRow !== rowIndex) {\n this.#startRowEdit(rowIndex, rowData);\n }\n\n this.#activeEditCol = colIndex;\n this.#injectEditor(rowData, rowIndex, column, colIndex, cellEl, false);\n }\n\n /**\n * Revert the focused cell's value from the snapshot taken when the editor\n * first received focus, then emit a `cell-cancel` event so framework\n * adapters (e.g., GridFormArray) can revert their FormControls.\n *\n * Called by the grid-mode Escape handlers (both capture and bubble phase).\n */\n #revertGridModeCellEdit(): void {\n const snapshot = this.#gridModeCellSnapshot;\n if (!snapshot) return;\n\n const internalGrid = this.#internalGrid;\n const rowData = internalGrid._rows?.[snapshot.rowIndex];\n if (rowData) {\n writeCellField(rowData, snapshot.field, snapshot.value);\n this.emitPluginEvent('cell-edit-reverted', { rowId: this.#safeGetRowId(rowData as T) });\n }\n\n // Push the reverted value to the editor's input element so that the\n // subsequent blur-commit (triggered by activeEl.blur()) reads the\n // reverted value instead of the user's typed text. This makes the\n // blur-commit a no-op (oldValue === newValue) and prevents it from\n // overwriting the revert.\n const callbackKey = `${snapshot.rowIndex}:${snapshot.field}`;\n const cb = this.#editorValueCallbacks.get(callbackKey);\n if (cb) cb(snapshot.value);\n\n // Notify framework adapters to revert FormControls\n this.emit<CellCancelDetail>('cell-cancel', {\n rowIndex: snapshot.rowIndex,\n colIndex: snapshot.colIndex,\n field: snapshot.field,\n previousValue: snapshot.value,\n });\n\n // After reverting, clean up changedRowIds so the built-in `.changed` class\n // toggle in rows.ts doesn't fight with consumer's rowClass callback.\n // changedRowIds.add() is unconditional during commit, so cleanup must also\n // be unconditional — otherwise the Set retains stale IDs when dirtyTracking\n // is disabled and the built-in toggle re-adds `.changed` on every render.\n if (rowData) {\n const rowId = this.#safeGetRowId(rowData as T);\n if (rowId) {\n if (this.config.dirtyTracking) {\n // With dirty tracking, only remove if the row has no other dirty cells\n if (!this.#dirty.isRowDirty(rowId, rowData as T)) {\n this.#dirty.changedRowIds.delete(rowId);\n this.#emitDirtyChange(rowId, rowData as T, 'reverted');\n }\n } else {\n // Without dirty tracking there's no baseline to compare, so always\n // remove — the consumer manages their own change state via rowClass.\n this.#dirty.changedRowIds.delete(rowId);\n }\n }\n }\n\n // Always re-render after revert so rowClass callbacks re-evaluate\n // (e.g., consumer may toggle a 'changed' class based on local state\n // updated in response to the cell-cancel event above).\n this.requestRender();\n\n this.#gridModeCellSnapshot = null;\n }\n\n /**\n * Focus the editor input in the currently focused cell (grid mode only).\n * Used when pressing Enter to enter edit mode from navigation mode.\n */\n #focusCurrentCellEditor(): void {\n const internalGrid = this.#internalGrid;\n const focusRow = internalGrid._focusRow;\n const focusCol = internalGrid._focusCol;\n\n if (focusRow < 0 || focusCol < 0) return;\n\n const cellEl = this.#getCell(focusRow, focusCol);\n\n if (cellEl?.classList.contains('editing')) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (editor) {\n this.#gridModeEditLocked = false; // Unlock edit mode - user pressed Enter\n editor.focus();\n this.#gridModeInputFocused = true;\n // Select all text in text inputs for quick replacement\n if (editor instanceof HTMLInputElement && (editor.type === 'text' || editor.type === 'number')) {\n editor.select();\n }\n }\n }\n }\n\n /**\n * Handle Tab/Shift+Tab navigation while editing.\n * Moves to next/previous editable cell, staying in edit mode.\n * Wraps to next/previous row when reaching row boundaries.\n */\n #handleTabNavigation(forward: boolean): void {\n const internalGrid = this.#internalGrid;\n const rows = internalGrid._rows;\n // In grid mode, use focusRow since there's no active edit row\n const currentRow = this.#isGridMode ? internalGrid._focusRow : this.#activeEditRow;\n const currentRowData = rows[currentRow] as T;\n\n // Get editable column indices for the CURRENT row\n const editableCols = internalGrid._visibleColumns\n .map((c, i) => (currentRowData && this.#isCellEditable(c as ColumnConfig<T>, currentRowData) ? i : -1))\n .filter((i) => i >= 0);\n if (editableCols.length === 0) return;\n\n const currentIdx = editableCols.indexOf(internalGrid._focusCol);\n const nextIdx = currentIdx + (forward ? 1 : -1);\n\n // Can move within same row?\n if (nextIdx >= 0 && nextIdx < editableCols.length) {\n internalGrid._focusCol = editableCols[nextIdx];\n const cellEl = this.#getCell(currentRow, editableCols[nextIdx]);\n if (cellEl?.classList.contains('editing')) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n ensureCellVisible(internalGrid, { forceHorizontalScroll: true });\n return;\n }\n\n // Can move to adjacent row?\n const nextRow = currentRow + (forward ? 1 : -1);\n if (nextRow >= 0 && nextRow < rows.length) {\n const nextRowData = rows[nextRow] as T;\n // Compute editable columns for the next row\n const nextEditableCols = internalGrid._visibleColumns\n .map((c, i) => (nextRowData && this.#isCellEditable(c as ColumnConfig<T>, nextRowData) ? i : -1))\n .filter((i) => i >= 0);\n if (nextEditableCols.length === 0) return; // Next row has no editable cells\n\n // In grid mode, just move focus (all rows are always editable)\n if (this.#isGridMode) {\n internalGrid._focusRow = nextRow;\n internalGrid._focusCol = forward ? nextEditableCols[0] : nextEditableCols[nextEditableCols.length - 1];\n ensureCellVisible(internalGrid, { forceHorizontalScroll: true });\n // Focus the editor in the new cell after render\n this.requestAfterRender();\n setTimeout(() => {\n const cellEl = this.#getCell(nextRow, internalGrid._focusCol);\n if (cellEl?.classList.contains('editing')) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n }, 0);\n } else {\n // In row mode, commit current row and enter next row\n this.#exitRowEdit(currentRow, false);\n internalGrid._focusRow = nextRow;\n internalGrid._focusCol = forward ? nextEditableCols[0] : nextEditableCols[nextEditableCols.length - 1];\n this.beginBulkEdit(nextRow);\n ensureCellVisible(internalGrid, { forceHorizontalScroll: true });\n }\n }\n // else: at boundary - stay put\n }\n\n /**\n * Sync the internal grid state with the plugin's editing state.\n */\n #syncGridEditState(): void {\n const internalGrid = this.#internalGrid;\n internalGrid._activeEditRows = this.#activeEditRow;\n internalGrid._rowEditSnapshots = this.#rowEditSnapshots;\n }\n\n /**\n * Snapshot original row data and mark as editing.\n */\n #startRowEdit(rowIndex: number, rowData: T): void {\n if (this.#activeEditRow !== rowIndex) {\n // Commit the previous row before starting a new one\n if (this.#activeEditRow !== -1) {\n this.#exitRowEdit(this.#activeEditRow, false);\n }\n this.#rowEditSnapshots.set(rowIndex, { ...rowData });\n this.#activeEditRow = rowIndex;\n this.#activeEditRowRef = rowData;\n\n // Store stable row ID for resilience against _rows replacement during editing\n this.#activeEditRowId = this.#safeGetRowId(rowData);\n\n this.#syncGridEditState();\n\n // Broadcast edit-open so consumers (DOM listeners) AND other plugins\n // (notably SelectionPlugin via the plugin event bus) stay in sync with\n // the row currently being edited. Issue #284.\n // Row mode only — in grid mode every row is perpetually editable.\n if (!this.#isGridMode) {\n this.broadcast<EditOpenDetail<T>>('edit-open', {\n rowIndex,\n rowId: this.#activeEditRowId ?? '',\n row: rowData,\n });\n announce(this.gridElement, getA11yMessage(this.gridElement, 'editingStarted', rowIndex));\n }\n }\n }\n\n /**\n * Exit editing for a row.\n */\n #exitRowEdit(rowIndex: number, revert: boolean): void {\n if (this.#activeEditRow !== rowIndex) return;\n\n const internalGrid = this.#internalGrid;\n const snapshot = this.#rowEditSnapshots.get(rowIndex);\n const rowEl = internalGrid.findRenderedRowElement?.(rowIndex);\n const { rowId, current } = this.#resolveEditedRow(rowIndex);\n\n // Collect and commit values from active editors before re-rendering\n if (!revert && rowEl && current) {\n this.#commitActiveEditors(rowEl, rowIndex, current);\n }\n\n // Flush managed editors (framework adapters) before clearing state.\n // At this point the commit() callback is still active, so editors can\n // synchronously commit their pending values in response to this event.\n if (!revert && !this.#isGridMode && current) {\n this.emit<BeforeEditCloseDetail<T>>('before-edit-close', {\n rowIndex,\n rowId: rowId ?? '',\n row: current,\n });\n }\n\n // Revert if requested\n if (revert && current) {\n this.#revertRowFromSnapshot(rowId, current, snapshot);\n } else if (!revert && current) {\n this.#finalizeRowCommit(rowIndex, rowId, current, snapshot);\n }\n\n this.#clearRowEditState(rowIndex);\n\n // Mark that focus should be restored after the upcoming render completes.\n // This must be set BEFORE refreshVirtualWindow because it calls afterRender()\n // synchronously, which reads this flag.\n this.#pendingFocusRestore = true;\n\n // Re-render the row to remove editors\n if (rowEl) {\n this.#teardownRowEditors(rowEl, internalGrid);\n } else {\n // Row not visible - restore focus immediately (no render will happen)\n this.#restoreCellFocus(internalGrid);\n this.#pendingFocusRestore = false;\n }\n\n // Broadcast edit-close (row mode only, fires for both commit and revert)\n // so consumers AND other plugins receive it. Issue #284.\n if (!this.#isGridMode && current) {\n this.broadcast<EditCloseDetail<T>>('edit-close', {\n rowIndex,\n rowId: rowId ?? '',\n row: current,\n reverted: revert,\n });\n if (!revert) {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'editingCommitted', rowIndex));\n }\n }\n }\n\n /**\n * Resolve the row being edited using the stored row ID.\n *\n * The `_rows` array may have been replaced (e.g. Angular pushing new rows via\n * directive effect) since editing started, so `_rows[rowIndex]` could point to\n * a completely different row — the ID map is always up to date. Without an ID\n * we fall back to the row reference stored at edit-open (`#activeEditRowRef`),\n * which is safer than the possibly-stale `_rows[rowIndex]`.\n */\n #resolveEditedRow(rowIndex: number): { rowId: string | undefined; current: T | undefined } {\n const internalGrid = this.#internalGrid;\n let rowId = this.#activeEditRowId;\n const entry = rowId ? internalGrid._getRowEntry(rowId) : undefined;\n const current = entry?.row ?? this.#activeEditRowRef ?? internalGrid._rows[rowIndex];\n\n if (!rowId && current) {\n rowId = this.#safeGetRowId(current);\n }\n return { rowId, current };\n }\n\n /**\n * Read pending values out of every native editor in the row and commit the\n * ones that actually changed.\n */\n #commitActiveEditors(rowEl: HTMLElement, rowIndex: number, current: T): void {\n const internalGrid = this.#internalGrid;\n rowEl.querySelectorAll('.cell.editing').forEach((cell) => {\n const colIndex = Number((cell as HTMLElement).getAttribute('data-col'));\n if (isNaN(colIndex)) return;\n const col = internalGrid._visibleColumns[colIndex];\n if (!col) return;\n\n // Skip cells with externally-managed editors (framework adapters like Angular/React/Vue).\n // These editors handle their own commits via the commit() callback - we should NOT\n // try to read values from their DOM inputs (which may contain formatted display values).\n if ((cell as HTMLElement).hasAttribute('data-editor-managed')) return;\n\n const input = cell.querySelector('input,textarea,select') as\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null;\n if (!input) return;\n\n const field = col.field;\n const originalValue = readCellField(current, field) as T[keyof T];\n const val = getInputValue(input, col, originalValue);\n if (!isSameEditorValue(originalValue, val)) {\n this.#commitCellValue(rowIndex, col, val, current);\n }\n });\n }\n\n /**\n * Emit the cancelable `row-commit` event and apply its outcome — revert on\n * `preventDefault()`, otherwise refresh committed-dirty tracking and queue the\n * change animation.\n */\n #finalizeRowCommit(rowIndex: number, rowId: string | undefined, current: T, snapshot: T | undefined): void {\n // Compare snapshot vs current to detect if changes were made during THIS edit session\n const changedThisSession = hasRowChanged(snapshot, current);\n\n // Check if this row has any cumulative changes (via ID tracking)\n // Fall back to session-based detection when no row ID is available\n const changed = rowId ? this.#dirty.changedRowIds.has(rowId) : changedThisSession;\n\n const cancelled = this.emitCancelable<RowCommitDetail<T>>('row-commit', {\n rowIndex,\n rowId: rowId ?? '',\n row: current,\n oldValue: snapshot,\n newValue: current,\n changed,\n changedRows: this.changedRows,\n changedRowIds: this.changedRowIds,\n });\n\n // If consumer called preventDefault(), revert the row\n if (cancelled) {\n if (snapshot) this.#revertRowFromSnapshot(rowId, current, snapshot);\n return;\n }\n\n // Mark row as committed-dirty if it has actual changes vs baseline\n if (rowId && this.config.dirtyTracking) {\n if (this.#dirty.isRowDirty(rowId, current)) {\n this.#dirty.committedDirtyRowIds.add(rowId);\n } else {\n this.#dirty.committedDirtyRowIds.delete(rowId);\n }\n }\n\n if (changedThisSession && this.isAnimationEnabled) {\n // Animate the row only if changes were made during this edit session\n // (deferred to afterRender so the row element exists after re-render)\n this.#pendingRowAnimation = rowIndex;\n }\n }\n\n /**\n * Clear all per-row editing bookkeeping.\n *\n * The `#editingCells` / `#editorValueCallbacks` keys use the rowIndex captured\n * at edit-open time. Even if `_rows` was replaced and the row moved to a\n * different index, the keys still match what was inserted during this session.\n */\n #clearRowEditState(rowIndex: number): void {\n this.#rowEditSnapshots.delete(rowIndex);\n this.#activeEditRow = -1;\n this.#activeEditRowId = undefined;\n this.#activeEditRowRef = undefined;\n this.#activeEditCol = -1;\n this.#singleCellEdit = false;\n this.#syncGridEditState();\n\n for (const cellKey of this.#editingCells) {\n if (cellKey.startsWith(`${rowIndex}:`)) {\n this.#editingCells.delete(cellKey);\n }\n }\n for (const callbackKey of this.#editorValueCallbacks.keys()) {\n if (callbackKey.startsWith(`${rowIndex}:`)) {\n this.#editorValueCallbacks.delete(callbackKey);\n }\n }\n }\n\n /**\n * Release framework editor components, drop the editing state from the cells\n * and re-render the row so the display values come back.\n */\n #teardownRowEditors(rowEl: HTMLElement, internalGrid: InternalGrid<T>): void {\n // Release framework editor components (Angular/React/Vue) BEFORE clearing\n // editing state. This ensures releaseCell runs while the editor DOM is\n // still inside each cell, so the adapter can find and destroy ComponentRefs.\n // Without this, overlay editors (BaseOverlayEditor) leak panels on <body>\n // with active MutationObservers that react to cell-focus class changes.\n // An active cell loader is parked outside the cell meanwhile, as in\n // editor injection: React's releaseCell would otherwise unmount it, and the\n // loading replay never refills an overlay that is still present.\n const adapter = internalGrid.__frameworkAdapter;\n const editingCells = rowEl.querySelectorAll('.cell.editing');\n if (adapter?.releaseCell) {\n editingCells.forEach((cell) => {\n const loader = parkCellLoadingOverlay(cell as HTMLElement);\n adapter.releaseCell!(cell as HTMLElement);\n if (loader) cell.appendChild(loader);\n });\n }\n\n // Remove editing class and re-render cells\n editingCells.forEach((cell) => {\n cell.classList.remove('editing');\n clearEditingState(cell.parentElement as RowElementInternal);\n });\n\n // Refresh the virtual window to restore cell content WITHOUT rebuilding\n // the row model. requestRender() would trigger processRows (ROWS phase)\n // which re-sorts — causing the edited row to jump to a new position and\n // disappear from view. refreshVirtualWindow re-renders visible cells from\n // the current _rows order, keeping the row in place until the user\n // explicitly sorts again or new data arrives.\n internalGrid.refreshVirtualWindow(true);\n }\n\n /**\n * Commit a single cell value change.\n * Uses ID-based change tracking for stability when rows are reordered.\n */\n #commitCellValue(\n rowIndex: number,\n column: ColumnConfig<T>,\n newValue: unknown,\n rowData: T,\n source: UpdateSource = 'user',\n ): void {\n const field = column.field;\n if (!isSafePropertyKey(field)) return;\n const oldValue = readCellField(rowData, field);\n if (oldValue === newValue) return;\n\n const internalGrid = this.#internalGrid;\n\n // Get row ID for change tracking (may not exist if getRowId not configured)\n const rowId = this.#safeGetRowId(rowData);\n\n const firstTime = rowId ? !this.#dirty.changedRowIds.has(rowId) : true;\n\n // Create updateRow helper for cascade updates (noop if row has no ID)\n const updateRow: (changes: Partial<T>) => void = rowId\n ? (changes) => this.grid.updateRow(rowId!, changes as Record<string, unknown>, 'cascade')\n : noopUpdateRow;\n\n // Track whether setInvalid was called during event handling\n let invalidWasSet = false;\n\n // Create setInvalid callback for validation (noop if row has no ID)\n const setInvalid = rowId\n ? (message?: string) => {\n invalidWasSet = true;\n this.setInvalid(rowId!, field, message ?? '');\n }\n : () => {}; // eslint-disable-line @typescript-eslint/no-empty-function\n\n // Emit cancelable event BEFORE applying the value.\n //\n // Re-entrancy guard: a `cell-commit` listener commonly cascades by calling\n // `detail.updateRow()` / `grid.updateRow()` (which route back through this\n // commit pipeline) or forces a row re-render the same way. If that nested\n // update targets the SAME cell that is still mid-commit, re-emitting\n // `cell-commit` would recurse until the call stack overflows\n // (`RangeError: Maximum call stack size exceeded`).\n //\n // When we detect a re-entrant commit of a cell already in flight we RETURN\n // EARLY rather than re-applying: the OUTER commit still owns this cell and\n // will apply the value + emit `cell-edit-committed` exactly once when its\n // own emit returns. Re-applying here would instead (a) fire a duplicate\n // `cell-edit-committed` → a phantom UndoRedo history entry, and (b) sneak\n // the value in even when the outer listener called `preventDefault()`,\n // bypassing the cancelable-commit contract. Cascades to OTHER cells still\n // emit normally, and the set is keyed per (row, field) so distinct fields\n // can never form an unbounded chain.\n const commitKey = `${rowId ?? rowIndex}\\u0000${field}`;\n if (this.#committingCells.has(commitKey)) return;\n\n let cancelled = false;\n this.#committingCells.add(commitKey);\n try {\n cancelled = this.emitCancelable<CellCommitDetail<T>>('cell-commit', {\n row: rowData,\n rowId: rowId ?? '',\n field,\n oldValue,\n value: newValue,\n source,\n rowIndex,\n changedRows: this.changedRows,\n changedRowIds: this.changedRowIds,\n firstTimeForRow: firstTime,\n updateRow,\n setInvalid,\n });\n } finally {\n this.#committingCells.delete(commitKey);\n }\n\n // If consumer called preventDefault(), abort the commit\n if (cancelled) return;\n\n // Clear any previous invalid state for this cell ONLY if setInvalid wasn't called\n // (if setInvalid was called, the handler wants it to remain invalid)\n if (rowId && !invalidWasSet && this.isCellInvalid(rowId, field)) {\n this.clearInvalid(rowId, field);\n }\n\n // Apply the value and mark row as changed.\n // Whole-row invalidation (not per-`field`): `field` here is this column's\n // own key, but OTHER columns' `valueAccessor`s may derive from it while\n // being cached under their own `column.field`. Per-field invalidation would\n // refresh only this column and leave those dependent columns stale.\n writeCellField(rowData, field, newValue);\n invalidateAccessorCache(rowData as object);\n if (rowId) {\n this.#dirty.changedRowIds.add(rowId);\n }\n this.#syncGridEditState();\n\n // Emit dirty-change event if dirty tracking is enabled\n if (this.config.dirtyTracking && rowId) {\n const dirty = this.#dirty.isRowDirty(rowId, rowData);\n this.#emitDirtyChange(rowId, rowData, dirty ? 'modified' : 'pristine');\n }\n\n // Notify other plugins (e.g., UndoRedoPlugin) about the committed edit\n this.emitPluginEvent('cell-edit-committed', {\n rowIndex,\n rowId,\n field,\n oldValue,\n newValue,\n });\n\n // Mark the row visually as changed (animation happens when row edit closes)\n const rowEl = internalGrid.findRenderedRowElement?.(rowIndex);\n if (rowEl) {\n rowEl.classList.add('changed');\n }\n }\n\n /**\n * Inject an editor into a cell.\n * Delegates to the extracted `editor-injection` module.\n */\n #injectEditor(\n rowData: T,\n rowIndex: number,\n column: ColumnConfig<T>,\n colIndex: number,\n cell: HTMLElement,\n skipFocus: boolean,\n parentRowEl?: HTMLElement,\n ): void {\n injectEditorImpl(this.#editorDeps, rowData, rowIndex, column, colIndex, cell, skipFocus, parentRowEl);\n }\n\n /**\n * Restore focus to cell after exiting edit mode.\n */\n #restoreCellFocus(internalGrid: InternalGrid<T>): void {\n queueMicrotask(() => {\n try {\n const rowIdx = internalGrid._focusRow;\n const colIdx = internalGrid._focusCol;\n const rowEl = internalGrid.findRenderedRowElement?.(rowIdx);\n if (rowEl) {\n Array.from(internalGrid._bodyEl.querySelectorAll('.cell-focus')).forEach((el) =>\n el.classList.remove('cell-focus'),\n );\n const cell = rowEl.querySelector(`.cell[data-row=\"${rowIdx}\"][data-col=\"${colIdx}\"]`) as HTMLElement | null;\n if (cell) {\n cell.classList.add('cell-focus');\n cell.setAttribute('aria-selected', 'true');\n if (!cell.hasAttribute('tabindex')) cell.setAttribute('tabindex', '-1');\n cell.focus({ preventScroll: true });\n }\n }\n } catch {\n /* empty */\n }\n });\n }\n\n // #endregion\n}\n"],"mappings":"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"}