apex-grid 3.4.0 → 3.6.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 (217) hide show
  1. package/CHANGELOG.md +439 -0
  2. package/LICENSE.md +125 -0
  3. package/README.md +107 -14
  4. package/components/cell.d.ts +30 -4
  5. package/components/cell.js +199 -53
  6. package/components/cell.js.map +1 -1
  7. package/components/filter-row.d.ts +2 -0
  8. package/components/filter-row.js +82 -28
  9. package/components/filter-row.js.map +1 -1
  10. package/components/grid.d.ts +257 -20
  11. package/components/grid.js +463 -99
  12. package/components/grid.js.map +1 -1
  13. package/components/group-header-row.js +28 -25
  14. package/components/group-header-row.js.map +1 -1
  15. package/components/header-row.d.ts +7 -5
  16. package/components/header-row.js +19 -9
  17. package/components/header-row.js.map +1 -1
  18. package/components/header.js +36 -22
  19. package/components/header.js.map +1 -1
  20. package/components/row.d.ts +19 -3
  21. package/components/row.js +96 -22
  22. package/components/row.js.map +1 -1
  23. package/components/toolbar.js +2 -1
  24. package/components/toolbar.js.map +1 -1
  25. package/components/virtualizer.d.ts +45 -1
  26. package/components/virtualizer.js +184 -3
  27. package/components/virtualizer.js.map +1 -1
  28. package/controllers/dom.d.ts +4 -4
  29. package/controllers/dom.js +40 -22
  30. package/controllers/dom.js.map +1 -1
  31. package/controllers/editing.d.ts +104 -37
  32. package/controllers/editing.js +230 -78
  33. package/controllers/editing.js.map +1 -1
  34. package/controllers/expansion.d.ts +14 -5
  35. package/controllers/expansion.js +24 -1
  36. package/controllers/expansion.js.map +1 -1
  37. package/controllers/filter.d.ts +10 -2
  38. package/controllers/filter.js +62 -17
  39. package/controllers/filter.js.map +1 -1
  40. package/controllers/history.d.ts +45 -8
  41. package/controllers/history.js +58 -20
  42. package/controllers/history.js.map +1 -1
  43. package/controllers/navigation.d.ts +68 -13
  44. package/controllers/navigation.js +196 -51
  45. package/controllers/navigation.js.map +1 -1
  46. package/controllers/pagination.d.ts +6 -0
  47. package/controllers/pagination.js +10 -2
  48. package/controllers/pagination.js.map +1 -1
  49. package/controllers/reorder.js +42 -24
  50. package/controllers/reorder.js.map +1 -1
  51. package/controllers/resize.d.ts +17 -2
  52. package/controllers/resize.js +16 -6
  53. package/controllers/resize.js.map +1 -1
  54. package/controllers/row-pin.d.ts +20 -2
  55. package/controllers/row-pin.js +16 -9
  56. package/controllers/row-pin.js.map +1 -1
  57. package/controllers/row-reorder.d.ts +31 -11
  58. package/controllers/row-reorder.js +117 -37
  59. package/controllers/row-reorder.js.map +1 -1
  60. package/controllers/selection.d.ts +11 -4
  61. package/controllers/selection.js +26 -11
  62. package/controllers/selection.js.map +1 -1
  63. package/controllers/sort.d.ts +22 -2
  64. package/controllers/sort.js +29 -23
  65. package/controllers/sort.js.map +1 -1
  66. package/controllers/state.d.ts +65 -10
  67. package/controllers/state.js +86 -6
  68. package/controllers/state.js.map +1 -1
  69. package/controllers/tree.d.ts +15 -10
  70. package/controllers/tree.js +0 -0
  71. package/controllers/tree.js.map +1 -1
  72. package/custom-elements.json +3589 -452
  73. package/docs/assets/hierarchy.js +1 -1
  74. package/docs/assets/highlight.css +15 -8
  75. package/docs/assets/navigation.js +1 -1
  76. package/docs/assets/search.js +1 -1
  77. package/docs/classes/ApexGrid.html +660 -0
  78. package/docs/classes/ApexGridPaginator.html +36 -0
  79. package/docs/classes/ApexGridToolbar.html +67 -0
  80. package/docs/functions/localize.html +2 -2
  81. package/docs/functions/setup.html +1 -2
  82. package/docs/hierarchy.html +1 -1
  83. package/docs/index.html +105 -15
  84. package/docs/interfaces/ApexCellValidationFailedEvent.html +3 -2
  85. package/docs/interfaces/ApexCellValueChangedEvent.html +4 -2
  86. package/docs/interfaces/ApexCellValueChangingEvent.html +3 -2
  87. package/docs/interfaces/ApexColumnMovedEvent.html +2 -2
  88. package/docs/interfaces/ApexColumnMovingEvent.html +1 -1
  89. package/docs/interfaces/ApexDetailContext.html +2 -2
  90. package/docs/interfaces/ApexGridEventMap.html +9 -6
  91. package/docs/interfaces/ApexRowEditEndedEvent.html +2 -1
  92. package/docs/interfaces/ApexRowEditStartedEvent.html +2 -1
  93. package/docs/interfaces/ApexRowMovingEvent.html +9 -3
  94. package/docs/interfaces/ApexRowPinningEvent.html +1 -1
  95. package/docs/interfaces/ApexTreeRowExpandedEvent.html +4 -1
  96. package/docs/interfaces/ApexTreeRowExpandingEvent.html +4 -1
  97. package/docs/interfaces/BaseApexCellContext.html +1 -1
  98. package/docs/interfaces/BaseApexEditorContext.html +1 -1
  99. package/docs/interfaces/BaseColumnConfiguration.html +3 -3
  100. package/docs/interfaces/BaseFilterExpression.html +2 -2
  101. package/docs/interfaces/CSVExportOptions.html +13 -5
  102. package/docs/interfaces/DataPipelineConfiguration.html +2 -2
  103. package/docs/interfaces/EditingHistoryConfiguration.html +1 -1
  104. package/docs/interfaces/ExportFormat.html +2 -2
  105. package/docs/interfaces/ExportOptions.html +5 -3
  106. package/docs/interfaces/GridExpansionConfiguration.html +6 -2
  107. package/docs/interfaces/GridRowPinningConfiguration.html +1 -1
  108. package/docs/interfaces/GridRowReorderingConfiguration.html +2 -2
  109. package/docs/interfaces/PaginationConfiguration.html +1 -1
  110. package/docs/interfaces/PaginationState.html +1 -1
  111. package/docs/interfaces/SetStateResult.html +3 -2
  112. package/docs/interfaces/ValidatorContext.html +7 -2
  113. package/docs/media/_token-values.scss +118 -0
  114. package/docs/media/package.json +5 -5
  115. package/docs/media/state-persistence.html +0 -10
  116. package/docs/modules.html +1 -1
  117. package/docs/types/DataPipelineParams.html +2 -2
  118. package/docs/types/EditMode.html +2 -2
  119. package/docs/types/ExportSource.html +6 -4
  120. package/docs/types/GridLocaleText.html +1 -1
  121. package/docs/types/OperandKeys.html +9 -0
  122. package/docs/types/PaginationMode.html +1 -1
  123. package/docs/variables/EN_LOCALE.html +3 -3
  124. package/docs/variables/esLocale.html +1 -1
  125. package/i18n/en.d.ts +19 -0
  126. package/i18n/en.js +19 -0
  127. package/i18n/en.js.map +1 -1
  128. package/i18n/es.js +19 -0
  129. package/i18n/es.js.map +1 -1
  130. package/index.d.ts +2 -0
  131. package/index.js +2 -0
  132. package/index.js.map +1 -1
  133. package/internal/column-types.d.ts +27 -1
  134. package/internal/column-types.js +75 -49
  135. package/internal/column-types.js.map +1 -1
  136. package/internal/constants.d.ts +1 -2
  137. package/internal/constants.js +0 -4
  138. package/internal/constants.js.map +1 -1
  139. package/internal/document-styles.d.ts +30 -0
  140. package/internal/document-styles.js +48 -0
  141. package/internal/document-styles.js.map +1 -0
  142. package/internal/export.d.ts +39 -7
  143. package/internal/export.js +35 -34
  144. package/internal/export.js.map +1 -1
  145. package/internal/feature-module.d.ts +82 -3
  146. package/internal/feature-module.js +11 -0
  147. package/internal/feature-module.js.map +1 -1
  148. package/internal/index.d.ts +6 -4
  149. package/internal/index.js +5 -3
  150. package/internal/index.js.map +1 -1
  151. package/internal/normalize-case.d.ts +8 -1
  152. package/internal/normalize-case.js +3 -2
  153. package/internal/normalize-case.js.map +1 -1
  154. package/internal/state-snapshot.d.ts +60 -8
  155. package/internal/state-snapshot.js +87 -19
  156. package/internal/state-snapshot.js.map +1 -1
  157. package/internal/types.d.ts +17 -2
  158. package/internal/types.js.map +1 -1
  159. package/internal/utils.d.ts +48 -6
  160. package/internal/utils.js +35 -21
  161. package/internal/utils.js.map +1 -1
  162. package/internal/validators.js +1 -0
  163. package/internal/validators.js.map +1 -1
  164. package/operations/filter/operands/number.js +9 -6
  165. package/operations/filter/operands/number.js.map +1 -1
  166. package/operations/filter/operands/string.js +2 -3
  167. package/operations/filter/operands/string.js.map +1 -1
  168. package/operations/filter/types.d.ts +13 -1
  169. package/operations/filter/types.js.map +1 -1
  170. package/operations/filter.d.ts +6 -0
  171. package/operations/filter.js +23 -7
  172. package/operations/filter.js.map +1 -1
  173. package/operations/quick-filter.js +2 -5
  174. package/operations/quick-filter.js.map +1 -1
  175. package/operations/sort.d.ts +21 -2
  176. package/operations/sort.js +41 -14
  177. package/operations/sort.js.map +1 -1
  178. package/package.json +8 -4
  179. package/setup.d.ts +23 -11
  180. package/setup.js +19 -21
  181. package/setup.js.map +1 -1
  182. package/styles/_common.css.js +1 -1
  183. package/styles/_common.css.js.map +1 -1
  184. package/styles/_token-values.css.d.ts +1 -0
  185. package/styles/_token-values.css.js +3 -0
  186. package/styles/_token-values.css.js.map +1 -0
  187. package/styles/_tokens.css.js +1 -1
  188. package/styles/_tokens.css.js.map +1 -1
  189. package/styles/body-cell/body-cell.css.js +1 -1
  190. package/styles/body-cell/body-cell.css.js.map +1 -1
  191. package/styles/body-cell/cell-renderers.css.js +1 -1
  192. package/styles/body-cell/cell-renderers.css.js.map +1 -1
  193. package/styles/body-row/body-row.css.js +1 -1
  194. package/styles/body-row/body-row.css.js.map +1 -1
  195. package/styles/document-tokens.css.d.ts +1 -0
  196. package/styles/document-tokens.css.js +3 -0
  197. package/styles/document-tokens.css.js.map +1 -0
  198. package/styles/filter-row/filter-row.css.js +1 -1
  199. package/styles/filter-row/filter-row.css.js.map +1 -1
  200. package/styles/grid/_grid.base.css.js +1 -1
  201. package/styles/grid/_grid.base.css.js.map +1 -1
  202. package/styles/grid/grid.css.js +1 -1
  203. package/styles/grid/grid.css.js.map +1 -1
  204. package/styles/group-header-row/group-header-row.css.js +1 -1
  205. package/styles/group-header-row/group-header-row.css.js.map +1 -1
  206. package/styles/header-cell/header-cell.css.js +1 -1
  207. package/styles/header-cell/header-cell.css.js.map +1 -1
  208. package/styles/header-row/header-row.base.css.js +1 -1
  209. package/styles/header-row/header-row.base.css.js.map +1 -1
  210. package/styles/paginator/paginator.css.js +1 -1
  211. package/styles/paginator/paginator.css.js.map +1 -1
  212. package/styles/toolbar/toolbar.css.js +1 -1
  213. package/styles/toolbar/toolbar.css.js.map +1 -1
  214. package/tokens.css +78 -0
  215. package/LICENSE +0 -80
  216. package/docs/media/LICENSE +0 -80
  217. package/docs/media/_tokens.scss +0 -193
@@ -4,23 +4,30 @@ import type { GridHost } from '../internal/types.js';
4
4
  * The default expansion configuration used when the grid has none set.
5
5
  *
6
6
  * @remarks
7
- * `detailTemplate` defaults to `null` here — the controller will refuse to
7
+ * `detailTemplate` defaults to `null` here; the controller will refuse to
8
8
  * toggle rows when the configuration is missing or no template is provided.
9
9
  */
10
10
  export declare const DEFAULT_EXPANSION_CONFIG: Readonly<{
11
11
  enabled: false;
12
12
  showToggleColumn: true;
13
13
  }>;
14
+ /**
15
+ * Returns `rows` without the entries that are no longer in `data`, for an
16
+ * expansion set that follows a data refresh. The survivors are counted first,
17
+ * so the usual refresh (nothing left) allocates nothing and returns `rows`
18
+ * itself. A row listed twice in `data` counts twice, which can keep one stale
19
+ * entry until a later refresh.
20
+ */
21
+ export declare function retainPresent<T>(rows: Set<T>, data: ReadonlyArray<T>): Set<T>;
14
22
  /**
15
23
  * Reactive controller backing row expansion (master-detail).
16
24
  *
17
25
  * @remarks
18
- * Expansion state is reference-based — the controller holds a `Set` of row
26
+ * Expansion state is reference-based: the controller holds a `Set` of row
19
27
  * data references so the expansion set survives sort / filter / pagination as
20
28
  * long as those operations preserve row identity (the default in-place
21
- * pipeline does). Replacing {@link ApexGrid.data} wholesale invalidates any
22
- * expanded rows that are no longer present; consumers should call
23
- * {@link collapseAll} or re-apply expansion afterwards.
29
+ * pipeline does). Replacing {@link ApexGrid.data} keeps the rows that are
30
+ * still present and drops the rest, without events.
24
31
  *
25
32
  * Mutations go through the cancellable `rowExpanding` event and emit a
26
33
  * follow-up `rowExpanded` event, mirroring the pattern used by selection
@@ -33,6 +40,8 @@ export declare class ExpansionController<T extends object> implements ReactiveCo
33
40
  expanded: Set<T>;
34
41
  constructor(host: GridHost<T>);
35
42
  hostConnected(): void;
43
+ /** Drops expanded rows that left {@link ApexGrid.data} once it is replaced. */
44
+ hostUpdate(): void;
36
45
  /** Whether expansion is enabled at the grid level. */
37
46
  get enabled(): boolean;
38
47
  /** Whether the built-in chevron toggle column should be rendered. */
@@ -2,13 +2,36 @@ export const DEFAULT_EXPANSION_CONFIG = Object.freeze({
2
2
  enabled: false,
3
3
  showToggleColumn: true,
4
4
  });
5
+ export function retainPresent(rows, data) {
6
+ if (rows.size === 0)
7
+ return rows;
8
+ let present = 0;
9
+ for (const row of data)
10
+ if (rows.has(row))
11
+ present++;
12
+ if (present === rows.size)
13
+ return rows;
14
+ const next = new Set();
15
+ for (const row of data)
16
+ if (rows.has(row))
17
+ next.add(row);
18
+ return next;
19
+ }
5
20
  export class ExpansionController {
21
+ #data;
6
22
  constructor(host) {
7
23
  this.host = host;
8
24
  this.expanded = new Set();
9
25
  this.host.addController(this);
10
26
  }
11
27
  hostConnected() { }
28
+ hostUpdate() {
29
+ const data = this.host.data;
30
+ if (!data || data === this.#data)
31
+ return;
32
+ this.#data = data;
33
+ this.expanded = retainPresent(this.expanded, data);
34
+ }
12
35
  get enabled() {
13
36
  return Boolean(this.host.expansion?.enabled);
14
37
  }
@@ -111,7 +134,7 @@ export class ExpansionController {
111
134
  else if (added.length === 0 && removed.length === 1) {
112
135
  this.host.announce(this.host.localize('announce.rowCollapsed'));
113
136
  }
114
- else if (added.length > 0 && next.size === added.length + this.expanded.size - added.length) {
137
+ else if (added.length > 0 && removed.length === 0) {
115
138
  this.host.announce(this.host.localize('announce.rowsExpanded', { count: added.length }));
116
139
  }
117
140
  else if (removed.length > 0 && next.size === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"expansion.js","sourceRoot":"","sources":["../../src/controllers/expansion.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController } from 'lit';\nimport type { GridHost } from '../internal/types.js';\n\n/**\n * The default expansion configuration used when the grid has none set.\n *\n * @remarks\n * `detailTemplate` defaults to `null` here — the controller will refuse to\n * toggle rows when the configuration is missing or no template is provided.\n */\nexport const DEFAULT_EXPANSION_CONFIG = Object.freeze({\n enabled: false,\n showToggleColumn: true,\n});\n\n/**\n * Reactive controller backing row expansion (master-detail).\n *\n * @remarks\n * Expansion state is reference-based — the controller holds a `Set` of row\n * data references so the expansion set survives sort / filter / pagination as\n * long as those operations preserve row identity (the default in-place\n * pipeline does). Replacing {@link ApexGrid.data} wholesale invalidates any\n * expanded rows that are no longer present; consumers should call\n * {@link collapseAll} or re-apply expansion afterwards.\n *\n * Mutations go through the cancellable `rowExpanding` event and emit a\n * follow-up `rowExpanded` event, mirroring the pattern used by selection\n * and editing.\n */\nexport class ExpansionController<T extends object> implements ReactiveController {\n /** Set of currently expanded row data references. */\n public expanded: Set<T> = new Set();\n\n constructor(protected host: GridHost<T>) {\n this.host.addController(this);\n }\n\n public hostConnected() {}\n\n /** Whether expansion is enabled at the grid level. */\n public get enabled(): boolean {\n return Boolean(this.host.expansion?.enabled);\n }\n\n /** Whether the built-in chevron toggle column should be rendered. */\n public get showToggleColumn(): boolean {\n return (\n this.enabled &&\n (this.host.expansion?.showToggleColumn ?? DEFAULT_EXPANSION_CONFIG.showToggleColumn)\n );\n }\n\n /** Whether `row` is currently expanded. */\n public isExpanded(row: T): boolean {\n return this.expanded.has(row);\n }\n\n /**\n * Whether `row` is permitted to expand. Combines the grid-wide `enabled`\n * flag, the presence of a `detailTemplate`, and the optional per-row\n * `isExpandable` predicate.\n */\n public canExpand(row: T): boolean {\n if (!this.enabled) return false;\n if (typeof this.host.expansion?.detailTemplate !== 'function') return false;\n const predicate = this.host.expansion?.isExpandable;\n return predicate ? Boolean(predicate(row)) : true;\n }\n\n /** Snapshot of currently expanded rows in insertion order. */\n public expandedRows(): T[] {\n return Array.from(this.expanded);\n }\n\n /** Toggles the expansion of `row`. */\n public async toggleRow(row: T): Promise<boolean> {\n if (!this.canExpand(row) && !this.expanded.has(row)) return false;\n const next = new Set(this.expanded);\n if (next.has(row)) next.delete(row);\n else next.add(row);\n return this.#commit(next);\n }\n\n /** Expands `row`. No-op when the row is already expanded or not expandable. */\n public async expandRow(row: T): Promise<boolean> {\n if (this.expanded.has(row)) return true;\n if (!this.canExpand(row)) return false;\n const next = new Set(this.expanded);\n next.add(row);\n return this.#commit(next);\n }\n\n /** Collapses `row`. No-op when the row is not expanded. */\n public async collapseRow(row: T): Promise<boolean> {\n if (!this.expanded.has(row)) return true;\n const next = new Set(this.expanded);\n next.delete(row);\n return this.#commit(next);\n }\n\n /**\n * Expands every expandable row in {@link ApexGrid.dataView}. Skips rows\n * the optional `isExpandable` predicate rejects.\n */\n public async expandAll(): Promise<boolean> {\n if (!this.enabled || typeof this.host.expansion?.detailTemplate !== 'function') return false;\n const predicate = this.host.expansion?.isExpandable;\n const next = new Set(this.expanded);\n for (const row of this.host.dataView as ReadonlyArray<T>) {\n if (!predicate || predicate(row)) next.add(row);\n }\n return this.#commit(next);\n }\n\n /** Collapses every currently expanded row. */\n public async collapseAll(): Promise<boolean> {\n if (this.expanded.size === 0) return true;\n return this.#commit(new Set());\n }\n\n /**\n * Replaces the expansion set with `rows`. Used by the public\n * {@link ApexGrid.expandedRows} setter for programmatic control.\n */\n public async replaceExpansion(rows: ReadonlyArray<T>): Promise<boolean> {\n if (!this.enabled) return false;\n const predicate = this.host.expansion?.isExpandable;\n const next = new Set(predicate ? rows.filter((row) => predicate(row)) : rows);\n return this.#commit(next);\n }\n\n async #commit(next: Set<T>): Promise<boolean> {\n const added: T[] = [];\n const removed: T[] = [];\n for (const row of next) {\n if (!this.expanded.has(row)) added.push(row);\n }\n for (const row of this.expanded) {\n if (!next.has(row)) removed.push(row);\n }\n if (added.length === 0 && removed.length === 0) return true;\n\n const proceed = this.host.emitEvent('rowExpanding', {\n detail: {\n added,\n removed,\n current: Array.from(this.expanded),\n next: Array.from(next),\n },\n cancelable: true,\n });\n if (!proceed) return false;\n\n this.expanded = next;\n this.host.requestUpdate();\n\n if (added.length === 1 && removed.length === 0) {\n this.host.announce(this.host.localize('announce.rowExpanded'));\n } else if (added.length === 0 && removed.length === 1) {\n this.host.announce(this.host.localize('announce.rowCollapsed'));\n } else if (added.length > 0 && next.size === added.length + this.expanded.size - added.length) {\n this.host.announce(this.host.localize('announce.rowsExpanded', { count: added.length }));\n } else if (removed.length > 0 && next.size === 0) {\n this.host.announce(this.host.localize('announce.allRowsCollapsed'));\n }\n\n this.host.emitEvent('rowExpanded', {\n detail: { added, removed, expanded: Array.from(this.expanded) },\n });\n return true;\n }\n}\n"]}
1
+ {"version":3,"file":"expansion.js","sourceRoot":"","sources":["../../src/controllers/expansion.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController } from 'lit';\nimport type { GridHost } from '../internal/types.js';\n\n/**\n * The default expansion configuration used when the grid has none set.\n *\n * @remarks\n * `detailTemplate` defaults to `null` here; the controller will refuse to\n * toggle rows when the configuration is missing or no template is provided.\n */\nexport const DEFAULT_EXPANSION_CONFIG = Object.freeze({\n enabled: false,\n showToggleColumn: true,\n});\n\n/**\n * Returns `rows` without the entries that are no longer in `data`, for an\n * expansion set that follows a data refresh. The survivors are counted first,\n * so the usual refresh (nothing left) allocates nothing and returns `rows`\n * itself. A row listed twice in `data` counts twice, which can keep one stale\n * entry until a later refresh.\n */\nexport function retainPresent<T>(rows: Set<T>, data: ReadonlyArray<T>): Set<T> {\n if (rows.size === 0) return rows;\n let present = 0;\n for (const row of data) if (rows.has(row)) present++;\n if (present === rows.size) return rows;\n const next = new Set<T>();\n for (const row of data) if (rows.has(row)) next.add(row);\n return next;\n}\n\n/**\n * Reactive controller backing row expansion (master-detail).\n *\n * @remarks\n * Expansion state is reference-based: the controller holds a `Set` of row\n * data references so the expansion set survives sort / filter / pagination as\n * long as those operations preserve row identity (the default in-place\n * pipeline does). Replacing {@link ApexGrid.data} keeps the rows that are\n * still present and drops the rest, without events.\n *\n * Mutations go through the cancellable `rowExpanding` event and emit a\n * follow-up `rowExpanded` event, mirroring the pattern used by selection\n * and editing.\n */\nexport class ExpansionController<T extends object> implements ReactiveController {\n /** Set of currently expanded row data references. */\n public expanded: Set<T> = new Set();\n /** The data array the expansion set was last reconciled with. */\n #data?: ReadonlyArray<T>;\n\n constructor(protected host: GridHost<T>) {\n this.host.addController(this);\n }\n\n public hostConnected() {}\n\n /** Drops expanded rows that left {@link ApexGrid.data} once it is replaced. */\n public hostUpdate() {\n const data = this.host.data;\n if (!data || data === this.#data) return;\n this.#data = data;\n this.expanded = retainPresent(this.expanded, data);\n }\n\n /** Whether expansion is enabled at the grid level. */\n public get enabled(): boolean {\n return Boolean(this.host.expansion?.enabled);\n }\n\n /** Whether the built-in chevron toggle column should be rendered. */\n public get showToggleColumn(): boolean {\n return (\n this.enabled &&\n (this.host.expansion?.showToggleColumn ?? DEFAULT_EXPANSION_CONFIG.showToggleColumn)\n );\n }\n\n /** Whether `row` is currently expanded. */\n public isExpanded(row: T): boolean {\n return this.expanded.has(row);\n }\n\n /**\n * Whether `row` is permitted to expand. Combines the grid-wide `enabled`\n * flag, the presence of a `detailTemplate`, and the optional per-row\n * `isExpandable` predicate.\n */\n public canExpand(row: T): boolean {\n if (!this.enabled) return false;\n if (typeof this.host.expansion?.detailTemplate !== 'function') return false;\n const predicate = this.host.expansion?.isExpandable;\n return predicate ? Boolean(predicate(row)) : true;\n }\n\n /** Snapshot of currently expanded rows in insertion order. */\n public expandedRows(): T[] {\n return Array.from(this.expanded);\n }\n\n /** Toggles the expansion of `row`. */\n public async toggleRow(row: T): Promise<boolean> {\n if (!this.canExpand(row) && !this.expanded.has(row)) return false;\n const next = new Set(this.expanded);\n if (next.has(row)) next.delete(row);\n else next.add(row);\n return this.#commit(next);\n }\n\n /** Expands `row`. No-op when the row is already expanded or not expandable. */\n public async expandRow(row: T): Promise<boolean> {\n if (this.expanded.has(row)) return true;\n if (!this.canExpand(row)) return false;\n const next = new Set(this.expanded);\n next.add(row);\n return this.#commit(next);\n }\n\n /** Collapses `row`. No-op when the row is not expanded. */\n public async collapseRow(row: T): Promise<boolean> {\n if (!this.expanded.has(row)) return true;\n const next = new Set(this.expanded);\n next.delete(row);\n return this.#commit(next);\n }\n\n /**\n * Expands every expandable row in {@link ApexGrid.dataView}. Skips rows\n * the optional `isExpandable` predicate rejects.\n */\n public async expandAll(): Promise<boolean> {\n if (!this.enabled || typeof this.host.expansion?.detailTemplate !== 'function') return false;\n const predicate = this.host.expansion?.isExpandable;\n const next = new Set(this.expanded);\n for (const row of this.host.dataView as ReadonlyArray<T>) {\n if (!predicate || predicate(row)) next.add(row);\n }\n return this.#commit(next);\n }\n\n /** Collapses every currently expanded row. */\n public async collapseAll(): Promise<boolean> {\n if (this.expanded.size === 0) return true;\n return this.#commit(new Set());\n }\n\n /**\n * Replaces the expansion set with `rows`. Used by the public\n * {@link ApexGrid.expandedRows} setter for programmatic control.\n */\n public async replaceExpansion(rows: ReadonlyArray<T>): Promise<boolean> {\n if (!this.enabled) return false;\n const predicate = this.host.expansion?.isExpandable;\n const next = new Set(predicate ? rows.filter((row) => predicate(row)) : rows);\n return this.#commit(next);\n }\n\n async #commit(next: Set<T>): Promise<boolean> {\n const added: T[] = [];\n const removed: T[] = [];\n for (const row of next) {\n if (!this.expanded.has(row)) added.push(row);\n }\n for (const row of this.expanded) {\n if (!next.has(row)) removed.push(row);\n }\n if (added.length === 0 && removed.length === 0) return true;\n\n const proceed = this.host.emitEvent('rowExpanding', {\n detail: {\n added,\n removed,\n current: Array.from(this.expanded),\n next: Array.from(next),\n },\n cancelable: true,\n });\n if (!proceed) return false;\n\n this.expanded = next;\n this.host.requestUpdate();\n\n if (added.length === 1 && removed.length === 0) {\n this.host.announce(this.host.localize('announce.rowExpanded'));\n } else if (added.length === 0 && removed.length === 1) {\n this.host.announce(this.host.localize('announce.rowCollapsed'));\n } else if (added.length > 0 && removed.length === 0) {\n // Only a pure expansion has a count to report; a swap that also\n // collapses rows would be misread as \"N rows expanded\".\n this.host.announce(this.host.localize('announce.rowsExpanded', { count: added.length }));\n } else if (removed.length > 0 && next.size === 0) {\n this.host.announce(this.host.localize('announce.allRowsCollapsed'));\n }\n\n this.host.emitEvent('rowExpanded', {\n detail: { added, removed, expanded: Array.from(this.expanded) },\n });\n return true;\n }\n}\n"]}
@@ -15,7 +15,15 @@ export declare class FilterController<T extends object> implements ReactiveContr
15
15
  setActiveColumn(column?: ColumnConfiguration<T>, triggerRect?: DOMRect): void;
16
16
  getDefaultExpression(column: ColumnConfiguration<T>): FilterExpression<T>;
17
17
  removeAllExpressions(key: Keys<T>): Promise<void>;
18
- removeExpression(expression: FilterExpression<T>): Promise<void>;
19
- filterWithEvent(expression: FilterExpression<T>, type: 'add' | 'modify' | 'remove'): Promise<void>;
18
+ /** Resolves to false when nothing was removed (not applied, or cancelled). */
19
+ removeExpression(expression: FilterExpression<T>): Promise<boolean>;
20
+ /**
21
+ * Emits the cancelable `filtering` event for `expression` and applies it
22
+ * when the event passes. With a `target` (the panel's draft or an applied
23
+ * expression), `expression` is the proposed copy: it is written into
24
+ * `target` only after the event passes, so a cancelled change leaves the
25
+ * applied filter untouched. Resolves to false when cancelled.
26
+ */
27
+ filterWithEvent(expression: FilterExpression<T>, type: 'add' | 'modify' | 'remove', target?: FilterExpression<T>): Promise<boolean>;
20
28
  filter(expression: FilterExpression<T> | FilterExpression<T>[]): void;
21
29
  }
@@ -5,14 +5,13 @@ export class FilterController {
5
5
  constructor(host) {
6
6
  this.host = host;
7
7
  this.state = new FilterState();
8
+ this.#applied = [];
9
+ this.#touched = false;
8
10
  this.host.addController(this);
9
11
  }
10
12
  get filterRow() {
11
13
  return this.host.filterRow;
12
14
  }
13
- get #virtualizer() {
14
- return this.host.scrollContainer;
15
- }
16
15
  #emitFilteringEvent(expression, type) {
17
16
  return this.host.emitEvent('filtering', {
18
17
  detail: {
@@ -30,11 +29,20 @@ export class FilterController {
30
29
  for (const expr of asArray(expression)) {
31
30
  this.state.set(expr);
32
31
  }
33
- this.#virtualizer?.scrollTo({ top: 0 });
32
+ this.#touched = true;
34
33
  this.host.requestUpdate(PIPELINE);
35
34
  }
35
+ #applied;
36
+ #touched;
36
37
  hostConnected() { }
37
38
  hostUpdate() {
39
+ if (this.#touched) {
40
+ this.#touched = false;
41
+ const next = snapshot(this.state);
42
+ if (!sameFilter(this.#applied, next))
43
+ this.host.scrollTop = 0;
44
+ this.#applied = next;
45
+ }
38
46
  this.filterRow?.requestUpdate();
39
47
  }
40
48
  get(key) {
@@ -42,6 +50,7 @@ export class FilterController {
42
50
  }
43
51
  reset(key) {
44
52
  key ? this.state.delete(key) : this.state.clear();
53
+ this.#touched = true;
45
54
  }
46
55
  setActiveColumn(column, triggerRect) {
47
56
  const filterRow = this.filterRow;
@@ -72,6 +81,8 @@ export class FilterController {
72
81
  };
73
82
  }
74
83
  async removeAllExpressions(key) {
84
+ if (!this.state.has(key))
85
+ return;
75
86
  const state = this.get(key)?.all ?? [];
76
87
  if (!this.host.emitEvent('filtering', {
77
88
  detail: {
@@ -85,33 +96,37 @@ export class FilterController {
85
96
  }
86
97
  this.reset(key);
87
98
  this.#filter([]);
88
- await this.host.updateComplete;
99
+ await this.host.pipelineSettled();
89
100
  this.#announce(key, 'cleared');
90
101
  this.#emitFilteredEvent({ key, state: this.get(key)?.all ?? [] });
91
102
  }
92
103
  async removeExpression(expression) {
93
104
  const state = this.get(expression.key);
105
+ if (!state?.has(expression))
106
+ return false;
94
107
  if (!this.#emitFilteringEvent(expression, 'remove')) {
95
- return;
108
+ return false;
96
109
  }
97
- state?.remove(expression);
98
- if (state?.empty) {
110
+ state.remove(expression);
111
+ if (state.empty) {
99
112
  this.reset(state.key);
100
113
  }
101
114
  this.#filter([]);
102
- await this.host.updateComplete;
115
+ await this.host.pipelineSettled();
103
116
  this.#announce(expression.key, 'cleared');
104
- this.#emitFilteredEvent({ key: expression.key, state: state?.all ?? [] });
117
+ this.#emitFilteredEvent({ key: expression.key, state: state.all });
118
+ return true;
105
119
  }
106
- async filterWithEvent(expression, type) {
120
+ async filterWithEvent(expression, type, target = expression) {
107
121
  if (!this.#emitFilteringEvent(expression, type)) {
108
- return;
122
+ return false;
109
123
  }
110
- this.#filter(expression);
111
- await this.host.updateComplete;
124
+ this.#filter(Object.assign(target, expression));
125
+ await this.host.pipelineSettled();
112
126
  if (type === 'add')
113
- this.#announce(expression.key, 'applied');
114
- this.#emitFilteredEvent({ key: expression.key, state: this.get(expression.key)?.all ?? [] });
127
+ this.#announce(target.key, 'applied');
128
+ this.#emitFilteredEvent({ key: target.key, state: this.get(target.key)?.all ?? [] });
129
+ return true;
115
130
  }
116
131
  #announce(key, kind) {
117
132
  const column = this.host.getColumn(key);
@@ -121,7 +136,37 @@ export class FilterController {
121
136
  }));
122
137
  }
123
138
  filter(expression) {
124
- this.#filter(asArray(expression).map((expr) => Object.assign(this.getDefaultExpression(this.host.getColumn(expr.key)), expr)));
139
+ this.#filter(asArray(expression).map((expr) => {
140
+ const defaults = this.getDefaultExpression(this.host.getColumn(expr.key));
141
+ return Object.assign(defaults, expr, {
142
+ condition: expr.condition ?? defaults.condition,
143
+ caseSensitive: expr.caseSensitive ?? defaults.caseSensitive,
144
+ });
145
+ }));
146
+ }
147
+ }
148
+ function snapshot(state) {
149
+ const out = [];
150
+ for (const tree of state.values) {
151
+ for (const e of tree)
152
+ out.push(e.key, e.condition, e.searchTerm, e.criteria, e.caseSensitive);
125
153
  }
154
+ return out;
155
+ }
156
+ function sameFilter(a, b) {
157
+ if (a.length !== b.length)
158
+ return false;
159
+ for (let i = 0; i < a.length; i++) {
160
+ const x = a[i];
161
+ const y = b[i];
162
+ if (Object.is(x, y))
163
+ continue;
164
+ if (x instanceof Date && y instanceof Date && x.getTime() === y.getTime())
165
+ continue;
166
+ if (Array.isArray(x) && Array.isArray(y) && sameFilter(x, y))
167
+ continue;
168
+ return false;
169
+ }
170
+ return true;
126
171
  }
127
172
  //# sourceMappingURL=filter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"filter.js","sourceRoot":"","sources":["../../src/controllers/filter.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController } from 'lit';\nimport type { ApexFilteredEvent } from '../components/grid.js';\nimport { PIPELINE } from '../internal/constants.js';\nimport type { ColumnConfiguration, GridHost, Keys } from '../internal/types.js';\nimport { asArray, getFilterOperandsFor } from '../internal/utils.js';\nimport { FilterState } from '../operations/filter/state.js';\nimport type { FilterExpression } from '../operations/filter/types.js';\n\nexport class FilterController<T extends object> implements ReactiveController {\n constructor(protected host: GridHost<T>) {\n this.host.addController(this);\n }\n\n public state: FilterState<T> = new FilterState();\n\n public get filterRow() {\n // @ts-expect-error - protected access\n return this.host.filterRow;\n }\n\n get #virtualizer() {\n // @ts-expect-error - protected access\n return this.host.scrollContainer;\n }\n\n #emitFilteringEvent(expression: FilterExpression<T>, type: 'add' | 'modify' | 'remove') {\n return this.host.emitEvent('filtering', {\n detail: {\n key: expression.key,\n expressions: [expression],\n type,\n },\n cancelable: true,\n });\n }\n\n #emitFilteredEvent(detail?: ApexFilteredEvent<T>) {\n return this.host.emitEvent('filtered', { detail });\n }\n\n #filter(expression: FilterExpression<T> | FilterExpression<T>[]) {\n for (const expr of asArray(expression)) {\n this.state.set(expr);\n }\n\n // HACK: In the case where the scrollTop is a large and amount and a big chunk of data is filtered out\n // HACK: the virtualizer can't recalculate its scroll position correctly. Thus, we reset the scrollTop state.\n this.#virtualizer?.scrollTo({ top: 0 });\n this.host.requestUpdate(PIPELINE);\n }\n\n public hostConnected() {}\n\n public hostUpdate(): void {\n this.filterRow?.requestUpdate();\n }\n\n public get(key: Keys<T>) {\n return this.state.get(key);\n }\n\n public reset(key?: Keys<T>) {\n key ? this.state.delete(key) : this.state.clear();\n }\n\n public setActiveColumn(column?: ColumnConfiguration<T>, triggerRect?: DOMRect) {\n const filterRow = this.filterRow;\n if (!filterRow) return;\n\n if (!column?.filter) {\n filterRow.active = false;\n return;\n }\n\n // Toggle: clicking the same column's filter while the panel is open closes it.\n if (filterRow.active && filterRow.column?.key === column.key) {\n filterRow.active = false;\n return;\n }\n\n filterRow.triggerRect = triggerRect ?? null;\n filterRow.column = column;\n filterRow.expression = this.getDefaultExpression(column);\n filterRow.active = true;\n this.host.requestUpdate();\n }\n\n public getDefaultExpression(column: ColumnConfiguration<T>) {\n const caseSensitive =\n typeof column.filter === 'boolean' ? false : Boolean(column.filter?.caseSensitive);\n const operands = getFilterOperandsFor(column);\n const keys = Object.keys(operands) as Keys<typeof operands>[];\n\n // XXX: Types\n return {\n key: column.key,\n condition: operands[keys[0]],\n caseSensitive,\n } as unknown as FilterExpression<T>;\n }\n\n public async removeAllExpressions(key: Keys<T>) {\n const state = this.get(key)?.all ?? [];\n\n if (\n !this.host.emitEvent('filtering', {\n detail: {\n key,\n expressions: state,\n type: 'remove',\n },\n cancelable: true,\n })\n ) {\n return;\n }\n\n this.reset(key);\n this.#filter([]);\n\n await this.host.updateComplete;\n this.#announce(key, 'cleared');\n this.#emitFilteredEvent({ key, state: this.get(key)?.all ?? [] });\n }\n\n public async removeExpression(expression: FilterExpression<T>) {\n const state = this.get(expression.key);\n\n if (!this.#emitFilteringEvent(expression, 'remove')) {\n return;\n }\n\n state?.remove(expression);\n\n if (state?.empty) {\n this.reset(state.key);\n }\n\n this.#filter([]);\n\n await this.host.updateComplete;\n this.#announce(expression.key, 'cleared');\n this.#emitFilteredEvent({ key: expression.key, state: state?.all ?? [] });\n }\n\n public async filterWithEvent(expression: FilterExpression<T>, type: 'add' | 'modify' | 'remove') {\n if (!this.#emitFilteringEvent(expression, type)) {\n return;\n }\n\n this.#filter(expression);\n\n await this.host.updateComplete;\n // Announce only on `add`: `modify` fires per keystroke while typing a\n // filter value and would flood the live region.\n if (type === 'add') this.#announce(expression.key, 'applied');\n this.#emitFilteredEvent({ key: expression.key, state: this.get(expression.key)?.all ?? [] });\n }\n\n #announce(key: Keys<T>, kind: 'applied' | 'cleared') {\n const column = this.host.getColumn(key);\n const label = column?.headerText ?? String(key);\n this.host.announce(\n this.host.localize(kind === 'applied' ? 'announce.filtered' : 'announce.filterCleared', {\n label,\n })\n );\n }\n\n public filter(expression: FilterExpression<T> | FilterExpression<T>[]) {\n this.#filter(\n asArray(expression).map((expr) =>\n Object.assign(this.getDefaultExpression(this.host.getColumn(expr.key)!), expr)\n )\n );\n }\n}\n"]}
1
+ {"version":3,"file":"filter.js","sourceRoot":"","sources":["../../src/controllers/filter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAG5D,MAAM,OAAO,gBAAgB;IAC3B,YAAsB,IAAiB;QAAjB,SAAI,GAAJ,IAAI,CAAa;QAIhC,UAAK,GAAmB,IAAI,WAAW,EAAE,CAAC;QAgCjD,aAAQ,GAAc,EAAE,CAAC;QAEzB,aAAQ,GAAG,KAAK,CAAC;QArCf,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAID,IAAW,SAAS;QAElB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;IAC7B,CAAC;IAED,mBAAmB,CAAC,UAA+B,EAAE,IAAiC;QACpF,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;YACtC,MAAM,EAAE;gBACN,GAAG,EAAE,UAAU,CAAC,GAAG;gBACnB,WAAW,EAAE,CAAC,UAAU,CAAC;gBACzB,IAAI;aACL;YACD,UAAU,EAAE,IAAI;SACjB,CAAC,CAAC;IACL,CAAC;IAED,kBAAkB,CAAC,MAA6B;QAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,CAAC,UAAuD;QAC7D,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YACvC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;QAED,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IAGD,QAAQ,CAAiB;IAEzB,QAAQ,CAAS;IAEV,aAAa,KAAI,CAAC;IAElB,UAAU;QACf,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;YACtB,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAOlC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;YAC9D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC;IAClC,CAAC;IAEM,GAAG,CAAC,GAAY;QACrB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAEM,KAAK,CAAC,GAAa;QACxB,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;IACvB,CAAC;IAEM,eAAe,CAAC,MAA+B,EAAE,WAAqB;QAC3E,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QACjC,IAAI,CAAC,SAAS;YAAE,OAAO;QAEvB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;YACpB,SAAS,CAAC,MAAM,GAAG,KAAK,CAAC;YACzB,OAAO;QACT,CAAC;QAGD,IAAI,SAAS,CAAC,MAAM,IAAI,SAAS,CAAC,MAAM,EAAE,GAAG,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC;YAC7D,SAAS,CAAC,MAAM,GAAG,KAAK,CAAC;YACzB,OAAO;QACT,CAAC;QAED,SAAS,CAAC,WAAW,GAAG,WAAW,IAAI,IAAI,CAAC;QAC5C,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC;QAC1B,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;QACzD,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;IAC5B,CAAC;IAEM,oBAAoB,CAAC,MAA8B;QACxD,MAAM,aAAa,GACjB,OAAO,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QACrF,MAAM,QAAQ,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAA4B,CAAC;QAG9D,OAAO;YACL,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,SAAS,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC5B,aAAa;SACoB,CAAC;IACtC,CAAC;IAEM,KAAK,CAAC,oBAAoB,CAAC,GAAY;QAE5C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC;QAEvC,IACE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;YAChC,MAAM,EAAE;gBACN,GAAG;gBACH,WAAW,EAAE,KAAK;gBAClB,IAAI,EAAE,QAAQ;aACf;YACD,UAAU,EAAE,IAAI;SACjB,CAAC,EACF,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAGjB,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;QAClC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC/B,IAAI,CAAC,kBAAkB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;IACpE,CAAC;IAGM,KAAK,CAAC,gBAAgB,CAAC,UAA+B;QAC3D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAEvC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,UAAU,CAAC;YAAE,OAAO,KAAK,CAAC;QAE1C,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;YACpD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAEzB,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAGjB,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;QAClC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC1C,IAAI,CAAC,kBAAkB,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QACnE,OAAO,IAAI,CAAC;IACd,CAAC;IASM,KAAK,CAAC,eAAe,CAC1B,UAA+B,EAC/B,IAAiC,EACjC,MAAM,GAAG,UAAU;QAEnB,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,EAAE,IAAI,CAAC,EAAE,CAAC;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;QAGhD,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;QAGlC,IAAI,IAAI,KAAK,KAAK;YAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC1D,IAAI,CAAC,kBAAkB,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC,CAAC;QACrF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,SAAS,CAAC,GAAY,EAAE,IAA2B;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,EAAE,UAAU,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAChB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,wBAAwB,EAAE;YACtF,KAAK;SACN,CAAC,CACH,CAAC;IACJ,CAAC;IAEM,MAAM,CAAC,UAAuD;QACnE,IAAI,CAAC,OAAO,CACV,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAE,CAAC,CAAC;YAI3E,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE;gBACnC,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS;gBAC/C,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,QAAQ,CAAC,aAAa;aAC5D,CAAC,CAAC;QACL,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AAOD,SAAS,QAAQ,CAAI,KAAqB;IACxC,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC;IAChG,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,UAAU,CAAC,CAAY,EAAE,CAAY;IAC5C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACf,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACf,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;YAAE,SAAS;QAC9B,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE;YAAE,SAAS;QAEpF,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC;YAAE,SAAS;QACvE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import type { ReactiveController } from 'lit';\nimport type { ApexFilteredEvent } from '../components/grid.js';\nimport { PIPELINE } from '../internal/constants.js';\nimport type { ColumnConfiguration, GridHost, Keys } from '../internal/types.js';\nimport { asArray, getFilterOperandsFor } from '../internal/utils.js';\nimport { FilterState } from '../operations/filter/state.js';\nimport type { FilterExpression } from '../operations/filter/types.js';\n\nexport class FilterController<T extends object> implements ReactiveController {\n constructor(protected host: GridHost<T>) {\n this.host.addController(this);\n }\n\n public state: FilterState<T> = new FilterState();\n\n public get filterRow() {\n // @ts-expect-error - protected access\n return this.host.filterRow;\n }\n\n #emitFilteringEvent(expression: FilterExpression<T>, type: 'add' | 'modify' | 'remove') {\n return this.host.emitEvent('filtering', {\n detail: {\n key: expression.key,\n expressions: [expression],\n type,\n },\n cancelable: true,\n });\n }\n\n #emitFilteredEvent(detail?: ApexFilteredEvent<T>) {\n return this.host.emitEvent('filtered', { detail });\n }\n\n #filter(expression: FilterExpression<T> | FilterExpression<T>[]) {\n for (const expr of asArray(expression)) {\n this.state.set(expr);\n }\n\n this.#touched = true;\n this.host.requestUpdate(PIPELINE);\n }\n\n /** The filter as of the last host update, to tell a change from a reapply. */\n #applied: unknown[] = [];\n /** Set by every write to the filter state; checked at the next update. */\n #touched = false;\n\n public hostConnected() {}\n\n public hostUpdate(): void {\n if (this.#touched) {\n this.#touched = false;\n const next = snapshot(this.state);\n // A changed match set starts at its first row. The grid host owns\n // vertical scrolling (the virtualizer is not a scroller), so reset it\n // there; left alone, a deep offset is clamped to the end of a shorter\n // result. Reapplying the same filter (setState(getState()), or a\n // wrapper that assigns filterExpressions on every render) keeps the\n // position.\n if (!sameFilter(this.#applied, next)) this.host.scrollTop = 0;\n this.#applied = next;\n }\n this.filterRow?.requestUpdate();\n }\n\n public get(key: Keys<T>) {\n return this.state.get(key);\n }\n\n public reset(key?: Keys<T>) {\n key ? this.state.delete(key) : this.state.clear();\n this.#touched = true;\n }\n\n public setActiveColumn(column?: ColumnConfiguration<T>, triggerRect?: DOMRect) {\n const filterRow = this.filterRow;\n if (!filterRow) return;\n\n if (!column?.filter) {\n filterRow.active = false;\n return;\n }\n\n // Toggle: clicking the same column's filter while the panel is open closes it.\n if (filterRow.active && filterRow.column?.key === column.key) {\n filterRow.active = false;\n return;\n }\n\n filterRow.triggerRect = triggerRect ?? null;\n filterRow.column = column;\n filterRow.expression = this.getDefaultExpression(column);\n filterRow.active = true;\n this.host.requestUpdate();\n }\n\n public getDefaultExpression(column: ColumnConfiguration<T>) {\n const caseSensitive =\n typeof column.filter === 'boolean' ? false : Boolean(column.filter?.caseSensitive);\n const operands = getFilterOperandsFor(column);\n const keys = Object.keys(operands) as Keys<typeof operands>[];\n\n // XXX: Types\n return {\n key: column.key,\n condition: operands[keys[0]],\n caseSensitive,\n } as unknown as FilterExpression<T>;\n }\n\n public async removeAllExpressions(key: Keys<T>) {\n // Nothing to clear: no events and no announcement.\n if (!this.state.has(key)) return;\n const state = this.get(key)?.all ?? [];\n\n if (\n !this.host.emitEvent('filtering', {\n detail: {\n key,\n expressions: state,\n type: 'remove',\n },\n cancelable: true,\n })\n ) {\n return;\n }\n\n this.reset(key);\n this.#filter([]);\n\n // @ts-expect-error - protected member access\n await this.host.pipelineSettled();\n this.#announce(key, 'cleared');\n this.#emitFilteredEvent({ key, state: this.get(key)?.all ?? [] });\n }\n\n /** Resolves to false when nothing was removed (not applied, or cancelled). */\n public async removeExpression(expression: FilterExpression<T>) {\n const state = this.get(expression.key);\n // An expression that was never applied (the panel's draft) is a no-op.\n if (!state?.has(expression)) return false;\n\n if (!this.#emitFilteringEvent(expression, 'remove')) {\n return false;\n }\n\n state.remove(expression);\n\n if (state.empty) {\n this.reset(state.key);\n }\n\n this.#filter([]);\n\n // @ts-expect-error - protected member access\n await this.host.pipelineSettled();\n this.#announce(expression.key, 'cleared');\n this.#emitFilteredEvent({ key: expression.key, state: state.all });\n return true;\n }\n\n /**\n * Emits the cancelable `filtering` event for `expression` and applies it\n * when the event passes. With a `target` (the panel's draft or an applied\n * expression), `expression` is the proposed copy: it is written into\n * `target` only after the event passes, so a cancelled change leaves the\n * applied filter untouched. Resolves to false when cancelled.\n */\n public async filterWithEvent(\n expression: FilterExpression<T>,\n type: 'add' | 'modify' | 'remove',\n target = expression\n ) {\n if (!this.#emitFilteringEvent(expression, type)) {\n return false;\n }\n\n this.#filter(Object.assign(target, expression));\n\n // @ts-expect-error - protected member access\n await this.host.pipelineSettled();\n // Announce only on `add`: `modify` fires per keystroke while typing a\n // filter value and would flood the live region.\n if (type === 'add') this.#announce(target.key, 'applied');\n this.#emitFilteredEvent({ key: target.key, state: this.get(target.key)?.all ?? [] });\n return true;\n }\n\n #announce(key: Keys<T>, kind: 'applied' | 'cleared') {\n const column = this.host.getColumn(key);\n const label = column?.headerText ?? String(key);\n this.host.announce(\n this.host.localize(kind === 'applied' ? 'announce.filtered' : 'announce.filterCleared', {\n label,\n })\n );\n }\n\n public filter(expression: FilterExpression<T> | FilterExpression<T>[]) {\n this.#filter(\n asArray(expression).map((expr) => {\n const defaults = this.getDefaultExpression(this.host.getColumn(expr.key)!);\n // An omitted or undefined field keeps the column default, so a\n // caseSensitive: undefined from setState or an AI patch inherits the\n // column's filter.caseSensitive instead of overriding it.\n return Object.assign(defaults, expr, {\n condition: expr.condition ?? defaults.condition,\n caseSensitive: expr.caseSensitive ?? defaults.caseSensitive,\n });\n })\n );\n }\n}\n\n/**\n * The parts of every applied expression that decide the match set, flattened\n * in state order. Values are captured, so a later in-place edit of an\n * expression (the filter panel's draft) still reads as a change.\n */\nfunction snapshot<T>(state: FilterState<T>): unknown[] {\n const out: unknown[] = [];\n for (const tree of state.values) {\n for (const e of tree) out.push(e.key, e.condition, e.searchTerm, e.criteria, e.caseSensitive);\n }\n return out;\n}\n\nfunction sameFilter(a: unknown[], b: unknown[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n const x = a[i];\n const y = b[i];\n if (Object.is(x, y)) continue;\n if (x instanceof Date && y instanceof Date && x.getTime() === y.getTime()) continue;\n // Array terms (a set filter's values) are copied by getState and setState.\n if (Array.isArray(x) && Array.isArray(y) && sameFilter(x, y)) continue;\n return false;\n }\n return true;\n}\n"]}
@@ -2,8 +2,8 @@ import type { ReactiveController } from 'lit';
2
2
  import type { GridHost, Keys } from '../internal/types.js';
3
3
  /**
4
4
  * A single recorded cell mutation: the record reference, its column, and the
5
- * before / after values. `rowIndex` is the view-relative index at record time
6
- * and is used only for the replayed `cellValueChanged` event (the live record
5
+ * before / after values. `rowIndex` is the view-relative index at record time;
6
+ * replay reports the record's index at replay time instead (the live record
7
7
  * reference is authoritative).
8
8
  */
9
9
  export interface CellChange<T extends object> {
@@ -24,20 +24,38 @@ export interface CellChange<T extends object> {
24
24
  export interface EditCommand {
25
25
  apply(): void;
26
26
  revert(): void;
27
+ /** The record the command acts on: history is cleared once a reload drops it. */
28
+ readonly record?: object;
29
+ /**
30
+ * For a command staged with a row edit: the value to stage for its cell,
31
+ * re-read from the row's staged values just before the row commits, so
32
+ * validation judges what the command will leave there.
33
+ */
34
+ stagedValue?(staged: ReadonlyMap<PropertyKey, unknown>): unknown;
27
35
  }
28
36
  /**
29
37
  * One or more cell mutations that undo / redo as a single step. A single-cell
30
38
  * edit holds one change; a row-mode commit or a bulk paste / fill holds many so
31
- * one Ctrl+Z reverts the whole batch.
39
+ * one Ctrl+Z reverts the whole batch. `commands` are other commands pushed in
40
+ * the same batch (a feature's state riding with the edit, such as a formula):
41
+ * they replay after the cell values in both directions, so a value write can
42
+ * never undo the state they restore.
32
43
  */
33
44
  export declare class CellEditCommand<T extends object> implements EditCommand {
34
45
  #private;
35
46
  private host;
36
47
  readonly changes: ReadonlyArray<CellChange<T>>;
37
- constructor(host: GridHost<T>, changes: ReadonlyArray<CellChange<T>>);
48
+ readonly commands: ReadonlyArray<EditCommand>;
49
+ constructor(host: GridHost<T>, changes: ReadonlyArray<CellChange<T>>, commands?: ReadonlyArray<EditCommand>);
38
50
  apply(): void;
39
51
  revert(): void;
40
52
  }
53
+ /**
54
+ * Returns a lookup of a record's index in `items` (by reference, `-1` when
55
+ * absent). For more than one lookup it indexes `items` once, so a large batch
56
+ * costs one pass instead of one scan per record.
57
+ */
58
+ export declare function viewIndexer<T>(items: ReadonlyArray<T>, lookups: number): (record: T) => number;
41
59
  /**
42
60
  * Reactive controller owning the undo / redo stacks for cell-data edits.
43
61
  *
@@ -67,20 +85,39 @@ export declare class HistoryController<T extends object> implements ReactiveCont
67
85
  * disabled.
68
86
  */
69
87
  record(change: CellChange<T>): void;
88
+ /**
89
+ * Records a command that is not a plain cell write (its first `apply` has
90
+ * already run). Joins the open batch, replaying after the batch's cell
91
+ * changes, otherwise pushed as its own step. No-op when history is disabled.
92
+ */
93
+ push(command: EditCommand): void;
70
94
  /**
71
95
  * Opens a batch so subsequent {@link record} calls coalesce into one command.
72
- * Nestable calls are flattened (the outermost {@link endBatch} commits).
96
+ * Nested calls are flattened: a batch opened while another is open (a
97
+ * `cellValueChanged` listener that calls `applyEdits`, say) joins it, and
98
+ * only the outermost {@link endBatch} commits. Counted even while history
99
+ * is disabled, so every begin pairs with its end.
73
100
  */
74
101
  beginBatch(): void;
75
102
  /**
76
- * Closes the open batch, pushing it as a single command when it captured any
77
- * changes.
103
+ * Closes the innermost open batch. Closing the outermost pushes everything
104
+ * it captured as a single command.
78
105
  */
79
106
  endBatch(): void;
80
107
  /** Reverts the most recent command. No-op when the undo stack is empty. */
81
108
  undo(): void;
82
109
  /** Re-applies the most recently undone command. No-op when nothing to redo. */
83
110
  redo(): void;
84
- /** Clears both stacks (e.g. after a data reset). */
111
+ /** Clears both stacks, and the changes an open batch has captured so far. */
85
112
  clear(): void;
113
+ /**
114
+ * Clears both stacks when the grid's data was replaced and a recorded change
115
+ * (or a command's {@link EditCommand.record}) names a record that `records`
116
+ * no longer yields (a reload with new objects).
117
+ * Replaying onto such a detached record would change nothing on screen while
118
+ * still reporting the edit. New data that keeps the edited records (an
119
+ * append, a re-sorted copy) keeps its history. `records` is read only while
120
+ * a stack has entries, so the caller may pass an expensive source.
121
+ */
122
+ reconcile(records: () => Iterable<T>): void;
86
123
  }
@@ -1,37 +1,53 @@
1
1
  import { PIPELINE } from '../internal/constants.js';
2
2
  const DEFAULT_STACK_SIZE = 100;
3
3
  export class CellEditCommand {
4
- constructor(host, changes) {
4
+ constructor(host, changes, commands = []) {
5
5
  this.host = host;
6
6
  this.changes = changes;
7
+ this.commands = commands;
7
8
  }
8
9
  apply() {
10
+ const indexOf = viewIndexer(this.host.pageItems, this.changes.length);
9
11
  for (const change of this.changes) {
10
- this.#write(change, change.newValue);
12
+ this.#write(change, change.newValue, indexOf);
11
13
  }
14
+ for (const command of this.commands)
15
+ command.apply();
12
16
  }
13
17
  revert() {
18
+ const indexOf = viewIndexer(this.host.pageItems, this.changes.length);
14
19
  for (let i = this.changes.length - 1; i >= 0; i -= 1) {
15
- this.#write(this.changes[i], this.changes[i].oldValue);
20
+ this.#write(this.changes[i], this.changes[i].oldValue, indexOf);
16
21
  }
22
+ for (let i = this.commands.length - 1; i >= 0; i -= 1)
23
+ this.commands[i].revert();
17
24
  }
18
- #write(change, value) {
25
+ #write(change, value, indexOf) {
19
26
  change.record[change.key] = value;
20
- const liveIndex = this.host.pageItems.indexOf(change.record);
21
27
  this.host.emitEvent('cellValueChanged', {
22
- detail: {
23
- key: change.key,
24
- rowIndex: liveIndex >= 0 ? liveIndex : change.rowIndex,
25
- data: change.record,
26
- value,
27
- },
28
+ detail: { key: change.key, rowIndex: indexOf(change.record), data: change.record, value },
28
29
  });
29
30
  }
30
31
  }
32
+ export function viewIndexer(items, lookups) {
33
+ if (lookups < 2)
34
+ return (record) => items.indexOf(record);
35
+ let index;
36
+ return (record) => {
37
+ if (!index) {
38
+ index = new Map();
39
+ for (let i = items.length - 1; i >= 0; i -= 1)
40
+ index.set(items[i], i);
41
+ }
42
+ return index.get(record) ?? -1;
43
+ };
44
+ }
31
45
  export class HistoryController {
32
46
  #undo = [];
33
47
  #redo = [];
34
- #batch = null;
48
+ #batch = [];
49
+ #commands = [];
50
+ #depth = 0;
35
51
  constructor(host) {
36
52
  this.host = host;
37
53
  this.host.addController(this);
@@ -53,22 +69,32 @@ export class HistoryController {
53
69
  record(change) {
54
70
  if (!this.enabled)
55
71
  return;
56
- if (this.#batch) {
72
+ if (this.#depth > 0) {
57
73
  this.#batch.push(change);
58
74
  return;
59
75
  }
60
76
  this.#push(new CellEditCommand(this.host, [change]));
61
77
  }
62
- beginBatch() {
78
+ push(command) {
63
79
  if (!this.enabled)
64
80
  return;
65
- this.#batch ??= [];
81
+ if (this.#depth > 0)
82
+ this.#commands.push(command);
83
+ else
84
+ this.#push(command);
85
+ }
86
+ beginBatch() {
87
+ this.#depth += 1;
66
88
  }
67
89
  endBatch() {
90
+ if (this.#depth === 0 || --this.#depth > 0)
91
+ return;
68
92
  const batch = this.#batch;
69
- this.#batch = null;
70
- if (batch && batch.length > 0) {
71
- this.#push(new CellEditCommand(this.host, batch));
93
+ const commands = this.#commands;
94
+ this.#batch = [];
95
+ this.#commands = [];
96
+ if (batch.length > 0 || commands.length > 0) {
97
+ this.#push(new CellEditCommand(this.host, batch, commands));
72
98
  }
73
99
  }
74
100
  undo() {
@@ -88,13 +114,25 @@ export class HistoryController {
88
114
  this.#afterReplay(command, 'redo');
89
115
  }
90
116
  clear() {
117
+ this.#batch = [];
118
+ this.#commands = [];
91
119
  if (this.#undo.length === 0 && this.#redo.length === 0)
92
120
  return;
93
121
  this.#undo = [];
94
122
  this.#redo = [];
95
- this.#batch = null;
96
123
  this.#emitChanged();
97
124
  }
125
+ reconcile(records) {
126
+ if (this.#undo.length === 0 && this.#redo.length === 0)
127
+ return;
128
+ const live = new Set(records());
129
+ const stale = (command) => (command.record !== undefined && !live.has(command.record)) ||
130
+ (command instanceof CellEditCommand &&
131
+ (command.changes.some((change) => !live.has(change.record)) ||
132
+ command.commands.some(stale)));
133
+ if (this.#undo.some(stale) || this.#redo.some(stale))
134
+ this.clear();
135
+ }
98
136
  #push(command) {
99
137
  this.#undo.push(command);
100
138
  this.#redo = [];
@@ -105,7 +143,7 @@ export class HistoryController {
105
143
  }
106
144
  #afterReplay(command, kind) {
107
145
  this.host.requestUpdate(PIPELINE);
108
- const count = command instanceof CellEditCommand ? command.changes.length : 1;
146
+ const count = command instanceof CellEditCommand ? command.changes.length || 1 : 1;
109
147
  const key = kind === 'undo'
110
148
  ? count === 1
111
149
  ? 'announce.undoOne'