@toolbox-web/grid 3.9.1 → 3.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all.js +1 -1
- package/all.js.map +1 -1
- package/custom-elements.json +2 -2
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/grid.d.ts +8 -4
- package/lib/core/plugin/base-plugin.d.ts +7 -0
- package/lib/core/types.d.ts +2 -0
- package/lib/features/registry.js +1 -1
- package/lib/plugins/context-menu/context-menu-plugin.d.ts +9 -4
- package/lib/plugins/context-menu/index.js +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/context-menu/types.d.ts +8 -4
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/editing/internal/dirty-tracking-manager.d.ts +2 -1
- package/lib/plugins/filtering/filter-model.d.ts +2 -1
- package/lib/plugins/filtering/filtering-plugin.d.ts +5 -2
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-rows/grouping-rows-plugin.d.ts +7 -14
- package/lib/plugins/grouping-rows/grouping-rows.d.ts +48 -0
- package/lib/plugins/grouping-rows/index.d.ts +1 -1
- package/lib/plugins/grouping-rows/index.js +1 -1
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/grouping-rows/types.d.ts +21 -0
- package/lib/plugins/master-detail/index.d.ts +1 -1
- package/lib/plugins/master-detail/index.js +1 -1
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/master-detail/master-detail-plugin.d.ts +28 -6
- package/lib/plugins/master-detail/master-detail.d.ts +0 -8
- package/lib/plugins/master-detail/types.d.ts +25 -1
- package/lib/plugins/multi-sort/index.js +1 -1
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/selection/index.js +1 -1
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/selection/selection-plugin.d.ts +8 -1
- package/lib/plugins/server-side/datasource-types.d.ts +28 -0
- package/lib/plugins/server-side/datasource.d.ts +7 -0
- package/lib/plugins/server-side/index.js +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/server-side/server-side-plugin.d.ts +61 -8
- package/lib/plugins/server-side/types.d.ts +9 -1
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/tree/tree-plugin.d.ts +5 -11
- package/lib/plugins/tree/types.d.ts +22 -6
- package/lib/plugins/undo-redo/history.d.ts +2 -1
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/undo-redo/types.d.ts +13 -1
- package/lib/plugins/undo-redo/undo-redo-plugin.d.ts +15 -2
- package/package.json +1 -1
- package/umd/grid.all.umd.js +1 -1
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +1 -1
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/context-menu.umd.js +1 -1
- package/umd/plugins/context-menu.umd.js.map +1 -1
- package/umd/plugins/editing.umd.js +1 -1
- package/umd/plugins/editing.umd.js.map +1 -1
- package/umd/plugins/filtering.umd.js +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/grouping-rows.umd.js +1 -1
- package/umd/plugins/grouping-rows.umd.js.map +1 -1
- package/umd/plugins/master-detail.umd.js +1 -1
- package/umd/plugins/master-detail.umd.js.map +1 -1
- package/umd/plugins/multi-sort.umd.js +1 -1
- package/umd/plugins/multi-sort.umd.js.map +1 -1
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/server-side.umd.js +1 -1
- package/umd/plugins/server-side.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
- package/umd/plugins/undo-redo.umd.js +1 -1
- package/umd/plugins/undo-redo.umd.js.map +1 -1
|
@@ -100,15 +100,6 @@ export declare class TreePlugin extends BaseGridPlugin<TreeConfig> implements Hi
|
|
|
100
100
|
private previousVisibleKeys;
|
|
101
101
|
private keysToAnimate;
|
|
102
102
|
private sortState;
|
|
103
|
-
/** Keys of nodes that are currently loading lazy children. */
|
|
104
|
-
private loadingKeys;
|
|
105
|
-
/**
|
|
106
|
-
* Keys whose children have already been fetched. Prevents a re-fetch on
|
|
107
|
-
* collapse → re-expand, and prevents an infinite retry loop when a node
|
|
108
|
-
* legitimately resolves to zero children but a custom `hasChildren`
|
|
109
|
-
* predicate keeps reporting `true`.
|
|
110
|
-
*/
|
|
111
|
-
private loadedKeys;
|
|
112
103
|
/** Cached original (unwrapped) renderer to prevent re-wrapping on repeated processColumns calls. */
|
|
113
104
|
private originalTreeColumnRenderer;
|
|
114
105
|
/** Property used by the original renderer, matching core's renderer precedence. */
|
|
@@ -154,8 +145,11 @@ export declare class TreePlugin extends BaseGridPlugin<TreeConfig> implements Hi
|
|
|
154
145
|
* without `ServerSidePlugin` for grids that only need lazy tree children
|
|
155
146
|
* and not server-side root pagination/sorting/filtering.
|
|
156
147
|
*
|
|
157
|
-
* No-op when neither route is configured,
|
|
158
|
-
* its children are already embedded.
|
|
148
|
+
* No-op when neither route is configured, this row is already loading, or
|
|
149
|
+
* its children are already embedded. A pending load for a DIFFERENT row
|
|
150
|
+
* object under the same key (replaced by a refetch) is cancelled first.
|
|
151
|
+
*
|
|
152
|
+
* @returns `true` when a fetch was started.
|
|
159
153
|
*/
|
|
160
154
|
private requestLazyChildren;
|
|
161
155
|
/**
|
|
@@ -73,9 +73,11 @@ export interface TreeConfig {
|
|
|
73
73
|
/**
|
|
74
74
|
* Lazy-load child rows when a node is expanded.
|
|
75
75
|
*
|
|
76
|
-
* Called once per
|
|
77
|
-
* not yet loaded
|
|
78
|
-
*
|
|
76
|
+
* Called once per row object the first time it is expanded while its children
|
|
77
|
+
* are not yet loaded — and again for a still-expanded node when the rows are
|
|
78
|
+
* replaced with new objects (e.g. a new `grid.rows` array). The resolved rows
|
|
79
|
+
* are written to `row[childrenField]` and the grid re-renders. While the
|
|
80
|
+
* request is in flight the node's toggle is
|
|
79
81
|
* replaced by the grid's small spinner and the row carries `aria-busy`.
|
|
80
82
|
*
|
|
81
83
|
* Returns a `Promise` or a {@link Subscribable} (e.g. an Angular
|
|
@@ -185,8 +187,18 @@ export interface TreeLoadStartDetail<T = TreeRow> {
|
|
|
185
187
|
* @since 3.4.0
|
|
186
188
|
*/
|
|
187
189
|
export interface TreeLoadEndDetail<T = TreeRow> extends TreeLoadStartDetail<T> {
|
|
188
|
-
/** Number of child rows received */
|
|
190
|
+
/** Number of child rows received (`0` when {@link TreeLoadEndDetail.cancelled | cancelled}) */
|
|
189
191
|
childCount: number;
|
|
192
|
+
/**
|
|
193
|
+
* `true` when the load was superseded before it settled: the node's row was
|
|
194
|
+
* replaced by a new object for the same key (e.g. a ServerSide refetch after a
|
|
195
|
+
* sort/filter change or `refresh()`) and the replacement rendered, so the
|
|
196
|
+
* request was aborted and nothing was merged. If the node is still expanded and
|
|
197
|
+
* the new row's children are lazy, they are requested again (a fresh
|
|
198
|
+
* `tree-load-start`).
|
|
199
|
+
* @since 3.9.2
|
|
200
|
+
*/
|
|
201
|
+
cancelled?: boolean;
|
|
190
202
|
}
|
|
191
203
|
/**
|
|
192
204
|
* Event detail emitted when lazy child loading fails.
|
|
@@ -203,12 +215,16 @@ declare module '../../core/types' {
|
|
|
203
215
|
/**
|
|
204
216
|
* Fired when lazy child loading starts for a node — i.e. a node with
|
|
205
217
|
* unloaded children was expanded and `loadChildren` (or the ServerSide
|
|
206
|
-
* data source) was invoked.
|
|
218
|
+
* data source) was invoked. Every `tree-load-start` is followed by exactly
|
|
219
|
+
* one `tree-load-end` or `tree-load-error` (unless the grid is torn down).
|
|
220
|
+
* @group Tree Events
|
|
207
221
|
*/
|
|
208
222
|
'tree-load-start': TreeLoadStartDetail;
|
|
209
223
|
/**
|
|
210
224
|
* Fired when lazy child loading completes and the children have been
|
|
211
|
-
* merged into the parent row
|
|
225
|
+
* merged into the parent row — or, with `cancelled: true`, when the load
|
|
226
|
+
* was superseded because the row was replaced before its children arrived.
|
|
227
|
+
* @group Tree Events
|
|
212
228
|
*/
|
|
213
229
|
'tree-load-end': TreeLoadEndDetail;
|
|
214
230
|
/**
|
|
@@ -58,9 +58,10 @@ export declare function clearHistory(): UndoRedoState;
|
|
|
58
58
|
* @param field - The field (column key) that was edited
|
|
59
59
|
* @param oldValue - The value before the edit
|
|
60
60
|
* @param newValue - The value after the edit
|
|
61
|
+
* @param rowId - Stable ID of the edited row, when resolvable
|
|
61
62
|
* @returns A new EditAction object
|
|
62
63
|
*/
|
|
63
|
-
export declare function createEditAction(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): EditAction;
|
|
64
|
+
export declare function createEditAction(rowIndex: number, field: string, oldValue: unknown, newValue: unknown, rowId?: string): EditAction;
|
|
64
65
|
/**
|
|
65
66
|
* Create a compound action grouping multiple edits into a single undo/redo unit.
|
|
66
67
|
*
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{BaseGridPlugin as t,writeCellField as e}from"@toolbox-web/grid";var o="editing";function n(t,e,o){const n=[...t.undoStack,e];for(;n.length>o;)n.shift();return{undoStack:n,redoStack:[]}}function i(t){if(0===t.undoStack.length)return{newState:t,action:null};const e=[...t.undoStack],o=e.pop();return o?{newState:{undoStack:e,redoStack:[...t.redoStack,o]},action:o}:{newState:t,action:null}}function
|
|
1
|
+
import{BaseGridPlugin as t,writeCellField as e}from"@toolbox-web/grid";var o="editing";function n(t,e,o){const n=[...t.undoStack,e];for(;n.length>o;)n.shift();return{undoStack:n,redoStack:[]}}function i(t){if(0===t.undoStack.length)return{newState:t,action:null};const e=[...t.undoStack],o=e.pop();return o?{newState:{undoStack:e,redoStack:[...t.redoStack,o]},action:o}:{newState:t,action:null}}function r(t){if(0===t.redoStack.length)return{newState:t,action:null};const e=[...t.redoStack],o=e.pop();return o?{newState:{undoStack:[...t.undoStack,o],redoStack:e},action:o}:{newState:t,action:null}}var a=class extends t{static dependencies=[{name:"editing",required:!0,reason:"UndoRedoPlugin tracks cell edit history"}];name="undoRedo";get defaultConfig(){return{maxHistorySize:100}}undoStack=[];redoStack=[];#t=!1;#e=null;#o=0;#n=null;#i(t){const e=this.rows[t];if(e)try{return this.grid.getRowId(e)||void 0}catch{return}}#r(t,o){if(void 0!==t.rowId)return void this.grid.updateRow(t.rowId,{[t.field]:o},"history");const n=this.rows[t.rowIndex];n&&e(n,t.field,o)}#a(t){const e=this.grid,n=e._visibleColumns?.findIndex(e=>e.field===t.field)??-1;if(n<0)return;const i=void 0!==t.rowId?e._getRowEntry(t.rowId)?.index??-1:t.rowIndex;if(i<0)return;e._focusRow=i,e._focusCol=n;const r=e.findRenderedRowElement?.(i);if(!r)return;const a=r.querySelector(`.cell[data-col="${n}"]`);a?.classList.contains(o)&&a.querySelector('input,select,textarea,[contenteditable="true"],[contenteditable=""],[tabindex]:not([tabindex="-1"])')?.focus({preventScroll:!0})}#c(t,e){if(this.#t=!0,"compound"===t.type){const o="undo"===e?[...t.actions].reverse():t.actions;for(const t of o)this.#r(t,"undo"===e?t.oldValue:t.newValue)}else this.#r(t,"undo"===e?t.oldValue:t.newValue);this.#t=!1}#s(t){const e="compound"===t.type?t.actions[t.actions.length-1]:t;this.#n=e??null}afterRender(){const t=this.#n;t&&(this.#n=null,this.#a(t))}attach(t){super.attach(t),this.on("cell-edit-committed",t=>{this.#t||this.recordEdit(t.rowIndex,t.field,t.oldValue,t.newValue,t.rowId)})}detach(){this.undoStack=[],this.redoStack=[],this.#e=null,this.#o=0,this.#n=null}onKeyDown(t){const e=(t.ctrlKey||t.metaKey)&&"z"===t.key&&!t.shiftKey,o=(t.ctrlKey||t.metaKey)&&("y"===t.key||"z"===t.key&&t.shiftKey);if(!e&&!o)return!1;const n=e?"undo":"redo";return this.#d(t.target)?(this.#u(t.target,n),!0):(t.preventDefault(),this.#h(n),!0)}#h(t){const e={undoStack:this.undoStack,redoStack:this.redoStack},o="undo"===t?i(e):r(e);o.action&&(this.#c(o.action,t),this.undoStack=o.newState.undoStack,this.redoStack=o.newState.redoStack,this.emit(t,{action:o.action,type:t}),this.#s(o.action),this.requestRenderWithFocus())}#d(t){return t instanceof Element&&!!t.closest('input, textarea, [contenteditable=""], [contenteditable="true"]')}#u(t,e){const o="undo"===e?"historyUndo":"historyRedo";let n=!1;const i=t=>{t.inputType===o&&(n=!0)};t.addEventListener("beforeinput",i,{capture:!0}),queueMicrotask(()=>{t.removeEventListener("beforeinput",i,{capture:!0}),n||this.#h(e)})}recordEdit(t,e,o,i,r){const a=function(t,e,o,n,i){const r={type:"cell-edit",rowIndex:t,field:e,oldValue:o,newValue:n,timestamp:Date.now()};return void 0!==i&&(r.rowId=i),r}(t,e,o,i,r||this.#i(t));if(this.#e)return void this.#e.push(a);const c=n({undoStack:this.undoStack,redoStack:this.redoStack},a,this.config.maxHistorySize??100);this.undoStack=c.undoStack,this.redoStack=c.redoStack}beginTransaction(){0===this.#o&&(this.#e=[]),this.#o++}endTransaction(){if(0===this.#o&&function(t,e,o,n){throw new Error(function(t,e,o,n){return`${function(t,e){return`[tbw-grid${t?`#${t}`:""}${e?`:${e}`:""}]`}(o,n)} ${t}: ${e}\n\n → More info: ${function(t){return`https://toolboxjs.com/grid/errors#${t.toLowerCase()}`}(t)}`}(t,e,o,n))}("TBW112","No transaction in progress. Call beginTransaction() first."),this.#o--,this.#o>0)return;const t=this.#e;if(this.#e=null,!t||0===t.length)return;const e=1===t.length?t[0]:{type:"compound",actions:t,timestamp:Date.now()};const o=n({undoStack:this.undoStack,redoStack:this.redoStack},e,this.config.maxHistorySize??100);this.undoStack=o.undoStack,this.redoStack=o.redoStack}undo(){const t=i({undoStack:this.undoStack,redoStack:this.redoStack});return t.action&&(this.#c(t.action,"undo"),this.undoStack=t.newState.undoStack,this.redoStack=t.newState.redoStack,this.#s(t.action),this.requestRenderWithFocus()),t.action}redo(){const t=r({undoStack:this.undoStack,redoStack:this.redoStack});return t.action&&(this.#c(t.action,"redo"),this.undoStack=t.newState.undoStack,this.redoStack=t.newState.redoStack,this.#s(t.action),this.requestRenderWithFocus()),t.action}canUndo(){return{undoStack:this.undoStack,redoStack:this.redoStack}.undoStack.length>0}canRedo(){return{undoStack:this.undoStack,redoStack:this.redoStack}.redoStack.length>0}clearHistory(){const t={undoStack:[],redoStack:[]};this.undoStack=t.undoStack,this.redoStack=t.redoStack,this.#e=null,this.#o=0}getUndoStack(){return[...this.undoStack]}getRedoStack(){return[...this.redoStack]}};export{a as UndoRedoPlugin};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/plugins/undo-redo/history.ts","../../../../../../libs/grid/src/lib/plugins/undo-redo/undo-redo-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 * 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 * Undo/Redo History Management\n *\n * Pure functions for managing the undo/redo stacks.\n * These functions are stateless and return new state objects.\n */\n\nimport type { CompoundEditAction, EditAction, UndoRedoAction, UndoRedoState } from './types';\n\n/**\n * Push a new action onto the undo stack.\n * Clears the redo stack since new actions invalidate redo history.\n *\n * @param state - Current undo/redo state\n * @param action - The action to add\n * @param maxSize - Maximum history size\n * @returns New state with the action added\n */\nexport function pushAction(state: UndoRedoState, action: UndoRedoAction, maxSize: number): UndoRedoState {\n const undoStack = [...state.undoStack, action];\n\n // Trim oldest actions if over max size\n while (undoStack.length > maxSize) {\n undoStack.shift();\n }\n\n return {\n undoStack,\n redoStack: [], // Clear redo on new action\n };\n}\n\n/**\n * Undo the most recent action.\n * Moves the action from undo stack to redo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was undone (or null if nothing to undo)\n */\nexport function undo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.undoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const undoStack = [...state.undoStack];\n const action = undoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack,\n redoStack: [...state.redoStack, action],\n },\n action,\n };\n}\n\n/**\n * Redo the most recently undone action.\n * Moves the action from redo stack back to undo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was redone (or null if nothing to redo)\n */\nexport function redo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.redoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const redoStack = [...state.redoStack];\n const action = redoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack: [...state.undoStack, action],\n redoStack,\n },\n action,\n };\n}\n\n/**\n * Check if there are any actions that can be undone.\n *\n * @param state - Current undo/redo state\n * @returns True if undo is available\n */\nexport function canUndo(state: UndoRedoState): boolean {\n return state.undoStack.length > 0;\n}\n\n/**\n * Check if there are any actions that can be redone.\n *\n * @param state - Current undo/redo state\n * @returns True if redo is available\n */\nexport function canRedo(state: UndoRedoState): boolean {\n return state.redoStack.length > 0;\n}\n\n/**\n * Clear all history, returning an empty state.\n *\n * @returns Fresh empty state\n */\nexport function clearHistory(): UndoRedoState {\n return { undoStack: [], redoStack: [] };\n}\n\n/**\n * Create a new edit action with the current timestamp.\n *\n * @param rowIndex - The row index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n * @returns A new EditAction object\n */\nexport function createEditAction(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): EditAction {\n return {\n type: 'cell-edit',\n rowIndex,\n field,\n oldValue,\n newValue,\n timestamp: Date.now(),\n };\n}\n\n/**\n * Create a compound action grouping multiple edits into a single undo/redo unit.\n *\n * @param actions - The individual edit actions to group (in chronological order)\n * @returns A CompoundEditAction wrapping all provided edits\n */\nexport function createCompoundAction(actions: EditAction[]): CompoundEditAction {\n return {\n type: 'compound',\n actions,\n timestamp: Date.now(),\n };\n}\n","/**\n * Undo/Redo Plugin (Class-based)\n *\n * Provides undo/redo functionality for cell edits in tbw-grid.\n * Supports Ctrl+Z/Cmd+Z for undo and Ctrl+Y/Cmd+Y (or Ctrl+Shift+Z) for redo.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { NO_TRANSACTION, throwDiagnostic } from '../../core/internal/diagnostics';\nimport { FOCUSABLE_EDITOR_SELECTOR } from '../../core/internal/rows';\nimport { writeCellField } from '../../core/internal/value-accessor';\nimport { BaseGridPlugin, type GridElement, type PluginDependency } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport {\n canRedo,\n canUndo,\n clearHistory,\n createCompoundAction,\n createEditAction,\n pushAction,\n redo,\n undo,\n} from './history';\nimport type { EditAction, UndoRedoAction, UndoRedoConfig, UndoRedoDetail } from './types';\n\n/**\n * Undo/Redo Plugin for tbw-grid\n *\n * Tracks all cell edits and lets users revert or replay changes with familiar keyboard\n * shortcuts (Ctrl+Z / Ctrl+Y). Maintains an in-memory history stack with configurable\n * depth—perfect for data entry workflows where mistakes happen.\n *\n * > **Required Dependency:** This plugin requires EditingPlugin to be loaded first.\n * > UndoRedo tracks the edit history that EditingPlugin creates.\n *\n * ## Installation\n *\n * ```ts\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Ctrl+Z` / `Cmd+Z` | Undo last edit |\n * | `Ctrl+Y` / `Cmd+Shift+Z` | Redo last undone edit |\n *\n * @example Basic Usage with EditingPlugin\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', editable: true },\n * { field: 'price', header: 'Price', type: 'number', editable: true },\n * ],\n * plugins: [\n * new EditingPlugin({ editOn: 'dblclick' }), // Required - must be first\n * new UndoRedoPlugin({ maxHistorySize: 50 }),\n * ],\n * };\n * ```\n *\n * @see {@link UndoRedoConfig} for configuration options\n * @see EditingPlugin for the required dependency\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> {\n /**\n * Plugin dependencies - UndoRedoPlugin requires EditingPlugin to track edits.\n *\n * The EditingPlugin must be loaded BEFORE this plugin in the plugins array.\n * @internal\n */\n static override readonly dependencies: PluginDependency[] = [\n { name: 'editing', required: true, reason: 'UndoRedoPlugin tracks cell edit history' },\n ];\n\n /** @internal */\n readonly name = 'undoRedo';\n\n /** @internal */\n protected override get defaultConfig(): Partial<UndoRedoConfig> {\n return {\n maxHistorySize: 100,\n };\n }\n\n // State as class properties\n private undoStack: UndoRedoAction[] = [];\n private redoStack: UndoRedoAction[] = [];\n\n /** Suppresses recording during undo/redo to prevent feedback loops. */\n #suppressRecording = false;\n\n /** Accumulates edits during a transaction; `null` when no transaction is active. */\n #transactionBuffer: EditAction[] | null = null;\n\n /**\n * Open-transaction nesting depth. `beginTransaction` increments, `endTransaction`\n * decrements; the buffer is only finalized when it returns to 0. Nesting lets a\n * synchronous burst of commits (e.g. a multi-cell paste routed through\n * `updateRows`, where each cell fires `cell-commit` and consumers bracket it\n * with `beginTransaction()`/`queueMicrotask(endTransaction)`) coalesce into a\n * single compound undo entry instead of throwing \"already in progress\".\n */\n #transactionDepth = 0;\n\n /**\n * Apply a value to a row cell, using `updateRow()` when possible so that\n * active editors (during row-edit mode) are notified via the `cell-change`\n * → `onValueChange` pipeline. Falls back to direct mutation when the row\n * has no ID.\n */\n #applyValue(action: EditAction, value: unknown): void {\n const rows = this.rows as Record<string, unknown>[];\n const row = rows[action.rowIndex];\n if (!row) return;\n\n // Prefer updateRow() — it emits `cell-change` events which notify active\n // editors via their `onValueChange` callbacks. Without this, undo/redo\n // during row-edit mode is invisible because the render pipeline skips\n // cells that have active editors.\n //\n // The `'history'` source tells data plugins (editing) to apply the value\n // and recompute dirty state WITHOUT recording a fresh history entry or\n // re-marking the row changed — the undo stack owns this change. This is\n // what breaks the updateRow→commit→cell-edit-committed→record feedback loop.\n try {\n const rowId = this.grid.getRowId(row);\n if (rowId) {\n this.grid.updateRow(rowId, { [action.field]: value }, 'history');\n return;\n }\n } catch {\n // No row ID configured — fall back to direct mutation\n }\n\n // Fallback: direct mutation (editors won't see the change during editing)\n writeCellField(row, action.field, value);\n }\n\n /**\n * Move keyboard focus to the cell targeted by an undo/redo action.\n * If the grid is in row-edit mode and the cell has an active editor,\n * the editor input is focused so the user can continue editing.\n */\n #focusActionCell(action: EditAction): void {\n const internalGrid: GridHost = this.grid;\n\n // Map field name → visible column index\n const colIdx = internalGrid._visibleColumns?.findIndex((c) => c.field === action.field) ?? -1;\n if (colIdx < 0) return;\n\n internalGrid._focusRow = action.rowIndex;\n internalGrid._focusCol = colIdx;\n\n // If we're in row-edit mode, focus the editor input in the target cell\n const rowEl = internalGrid.findRenderedRowElement?.(action.rowIndex);\n if (!rowEl) return;\n\n const cellEl = rowEl.querySelector(`.cell[data-col=\"${colIdx}\"]`) as HTMLElement | null;\n if (cellEl?.classList.contains(GridClasses.EDITING)) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n }\n\n /**\n * Apply value changes for a single or compound action.\n * Wraps `#applyValue` calls with `#suppressRecording` to prevent feedback loops.\n */\n #applyUndoRedoAction(action: UndoRedoAction, direction: 'undo' | 'redo'): void {\n this.#suppressRecording = true;\n if (action.type === 'compound') {\n const subActions = direction === 'undo' ? [...action.actions].reverse() : action.actions;\n for (const sub of subActions) {\n this.#applyValue(sub, direction === 'undo' ? sub.oldValue : sub.newValue);\n }\n } else {\n this.#applyValue(action, direction === 'undo' ? action.oldValue : action.newValue);\n }\n this.#suppressRecording = false;\n }\n\n /**\n * Focus the cell associated with an undo/redo action.\n * For compound actions, focuses the **last** action's cell. When consumers\n * use `beginTransaction()` + `recordEdit()` (cascaded fields) followed by\n * `queueMicrotask(() => endTransaction())`, the grid's auto-recorded\n * primary field edit is appended last. Focusing it ensures the cursor\n * lands on the field the user originally edited, not on a cascaded field\n * whose column may not even be visible.\n */\n #focusUndoRedoAction(action: UndoRedoAction): void {\n const target = action.type === 'compound' ? action.actions[action.actions.length - 1] : action;\n if (target) this.#focusActionCell(target);\n }\n\n /**\n * Subscribe to cell-edit-committed events from EditingPlugin.\n * @internal\n */\n override attach(grid: GridElement): void {\n super.attach(grid);\n // Auto-record edits via Event Bus\n this.on(\n 'cell-edit-committed',\n (detail: { rowIndex: number; field: string; oldValue: unknown; newValue: unknown }) => {\n // Skip recording during undo/redo operations. When undo/redo applies a\n // value via updateRow, two things can cause re-entry:\n // 1. updateRow → cell-change → onValueChange → editor triggers commit\n // 2. Browser native undo (if not fully suppressed) fires input event → commit\n // The suppress flag prevents these from corrupting the history stacks.\n if (this.#suppressRecording) return;\n this.recordEdit(detail.rowIndex, detail.field, detail.oldValue, detail.newValue);\n },\n );\n }\n\n /**\n * Clean up state when plugin is detached.\n * @internal\n */\n override detach(): void {\n this.undoStack = [];\n this.redoStack = [];\n this.#transactionBuffer = null;\n this.#transactionDepth = 0;\n }\n\n /**\n * Handle keyboard shortcuts for undo/redo.\n * - Ctrl+Z / Cmd+Z: Undo\n * - Ctrl+Y / Cmd+Y / Ctrl+Shift+Z / Cmd+Shift+Z: Redo\n *\n * When the keystroke originates from a focusable form control (e.g. an\n * active cell editor input, a filter text box) we defer to the browser's\n * native undo/redo first. We watch for a `beforeinput` event with\n * `inputType === 'historyUndo'` / `'historyRedo'` on the target — if the\n * browser had something to undo, it consumes the keystroke and we do\n * nothing; if it didn't (history depleted), we fall back to the grid's\n * own undo/redo. This lets users undo their typing/paste inside an open\n * editor, then continue undoing committed cell edits with the same key.\n * @internal\n */\n override onKeyDown(event: KeyboardEvent): boolean {\n const isUndo = (event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey;\n const isRedo = (event.ctrlKey || event.metaKey) && (event.key === 'y' || (event.key === 'z' && event.shiftKey));\n\n if (!isUndo && !isRedo) return false;\n\n const kind: 'undo' | 'redo' = isUndo ? 'undo' : 'redo';\n\n if (this.#isNativeHistoryTarget(event.target)) {\n // Let the browser try its native undo/redo first. Only run ours if\n // the browser had nothing left in its own history.\n this.#deferToNativeHistory(event.target as HTMLElement, kind);\n return true;\n }\n\n // Prevent browser native undo on text inputs — it would conflict with\n // the grid's undo by mutating the input text independently, triggering\n // re-commits that cancel the grid undo.\n event.preventDefault();\n this.#performHistory(kind);\n return true;\n }\n\n /**\n * Apply an undo or redo against the grid's own history stack.\n */\n #performHistory(kind: 'undo' | 'redo'): void {\n const state = { undoStack: this.undoStack, redoStack: this.redoStack };\n const result = kind === 'undo' ? undo(state) : redo(state);\n if (!result.action) return;\n\n this.#applyUndoRedoAction(result.action, kind);\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n\n this.emit<UndoRedoDetail>(kind, { action: result.action, type: kind });\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n\n /**\n * True when the keystroke target is a form control with its own native\n * undo history (text inputs, textareas, contenteditable). `<select>` is\n * intentionally excluded — it has no undo history.\n */\n #isNativeHistoryTarget(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n return !!target.closest('input, textarea, [contenteditable=\"\"], [contenteditable=\"true\"]');\n }\n\n /**\n * Watch for the browser's native undo/redo on `target`. The browser fires\n * `beforeinput` with `inputType: 'historyUndo' | 'historyRedo'` when it\n * actually has something to undo/redo. If we don't observe that within a\n * microtask of the keydown returning, the native history is depleted and\n * we run the grid's own undo/redo to extend the chain.\n */\n #deferToNativeHistory(target: HTMLElement, kind: 'undo' | 'redo'): void {\n const wantedInputType = kind === 'undo' ? 'historyUndo' : 'historyRedo';\n let nativeHandled = false;\n\n const onBeforeInput = (e: Event) => {\n if ((e as InputEvent).inputType === wantedInputType) nativeHandled = true;\n };\n target.addEventListener('beforeinput', onBeforeInput, { capture: true });\n\n queueMicrotask(() => {\n target.removeEventListener('beforeinput', onBeforeInput, { capture: true } as EventListenerOptions);\n if (!nativeHandled) this.#performHistory(kind);\n });\n }\n\n // #region Public API Methods\n\n /**\n * Record a cell edit for undo/redo tracking.\n * Call this when a cell value changes.\n *\n * @param rowIndex - The row index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n */\n recordEdit(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): void {\n const action = createEditAction(rowIndex, field, oldValue, newValue);\n\n // Buffer during transactions instead of pushing to undo stack\n if (this.#transactionBuffer) {\n this.#transactionBuffer.push(action);\n return;\n }\n\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Begin grouping subsequent edits into a single compound action.\n *\n * While a transaction is active, all `recordEdit()` calls (both manual\n * and auto-recorded from `cell-edit-committed`) are buffered instead of\n * pushed to the undo stack. Call `endTransaction()` to finalize the group.\n *\n * **Nesting:** transactions are re-entrant. Calling `beginTransaction()` while\n * one is already open increments a depth counter rather than throwing; the\n * buffer is finalized only when a matching `endTransaction()` returns the depth\n * to 0. This lets a synchronous burst of commits (e.g. a multi-cell paste, where\n * each `cell-commit` handler brackets itself with `beginTransaction()` +\n * `queueMicrotask(endTransaction)`) coalesce into a single compound undo entry.\n *\n * **Typical usage** — group a user edit with its cascaded side-effects:\n *\n * ```ts\n * grid.on('cell-commit', () => {\n * const undoRedo = grid.getPluginByName('undoRedo');\n * undoRedo.beginTransaction();\n *\n * // Record cascaded updates (these won't auto-record)\n * const oldB = row.fieldB;\n * undoRedo.recordEdit(rowIndex, 'fieldB', oldB, computedB);\n * grid.updateRow(rowId, { fieldB: computedB });\n *\n * // End after the auto-recorded original edit is captured\n * queueMicrotask(() => undoRedo.endTransaction());\n * });\n * ```\n */\n beginTransaction(): void {\n if (this.#transactionDepth === 0) {\n this.#transactionBuffer = [];\n }\n this.#transactionDepth++;\n }\n\n /**\n * Finalize the current transaction, wrapping all buffered edits into a\n * single compound action on the undo stack.\n *\n * - If the buffer contains multiple edits, they are wrapped in a `CompoundEditAction`.\n * - If the buffer contains a single edit, it is pushed as a regular `EditAction`.\n * - If the buffer is empty, this is a no-op.\n *\n * Undoing a compound action reverts all edits in reverse order; redoing\n * replays them in forward order.\n *\n * When transactions are nested, only the outermost `endTransaction()` (the one\n * that returns the depth to 0) finalizes the buffer; inner calls just decrement.\n *\n * @throws Error if no transaction is in progress\n */\n endTransaction(): void {\n if (this.#transactionDepth === 0) {\n throwDiagnostic(NO_TRANSACTION, 'No transaction in progress. Call beginTransaction() first.');\n }\n\n this.#transactionDepth--;\n if (this.#transactionDepth > 0) return; // still inside an outer transaction\n\n const buffer = this.#transactionBuffer;\n this.#transactionBuffer = null;\n if (!buffer || buffer.length === 0) return;\n\n const action: UndoRedoAction = buffer.length === 1 ? buffer[0] : createCompoundAction(buffer);\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Programmatically undo the last action.\n *\n * @returns The undone action, or null if nothing to undo\n */\n undo(): UndoRedoAction | null {\n const result = undo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'undo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Programmatically redo the last undone action.\n *\n * @returns The redone action, or null if nothing to redo\n */\n redo(): UndoRedoAction | null {\n const result = redo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'redo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Check if there are any actions that can be undone.\n */\n canUndo(): boolean {\n return canUndo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Check if there are any actions that can be redone.\n */\n canRedo(): boolean {\n return canRedo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Clear all undo/redo history.\n */\n clearHistory(): void {\n const newState = clearHistory();\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n this.#transactionBuffer = null;\n this.#transactionDepth = 0;\n }\n\n /**\n * Get a copy of the current undo stack.\n */\n getUndoStack(): UndoRedoAction[] {\n return [...this.undoStack];\n }\n\n /**\n * Get a copy of the current redo stack.\n */\n getRedoStack(): UndoRedoAction[] {\n return [...this.redoStack];\n }\n // #endregion\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/plugins/undo-redo/history.ts","../../../../../../libs/grid/src/lib/plugins/undo-redo/undo-redo-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 * 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 * Undo/Redo History Management\n *\n * Pure functions for managing the undo/redo stacks.\n * These functions are stateless and return new state objects.\n */\n\nimport type { CompoundEditAction, EditAction, UndoRedoAction, UndoRedoState } from './types';\n\n/**\n * Push a new action onto the undo stack.\n * Clears the redo stack since new actions invalidate redo history.\n *\n * @param state - Current undo/redo state\n * @param action - The action to add\n * @param maxSize - Maximum history size\n * @returns New state with the action added\n */\nexport function pushAction(state: UndoRedoState, action: UndoRedoAction, maxSize: number): UndoRedoState {\n const undoStack = [...state.undoStack, action];\n\n // Trim oldest actions if over max size\n while (undoStack.length > maxSize) {\n undoStack.shift();\n }\n\n return {\n undoStack,\n redoStack: [], // Clear redo on new action\n };\n}\n\n/**\n * Undo the most recent action.\n * Moves the action from undo stack to redo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was undone (or null if nothing to undo)\n */\nexport function undo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.undoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const undoStack = [...state.undoStack];\n const action = undoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack,\n redoStack: [...state.redoStack, action],\n },\n action,\n };\n}\n\n/**\n * Redo the most recently undone action.\n * Moves the action from redo stack back to undo stack.\n *\n * @param state - Current undo/redo state\n * @returns New state and the action that was redone (or null if nothing to redo)\n */\nexport function redo(state: UndoRedoState): {\n newState: UndoRedoState;\n action: UndoRedoAction | null;\n} {\n if (state.redoStack.length === 0) {\n return { newState: state, action: null };\n }\n\n const redoStack = [...state.redoStack];\n const action = redoStack.pop();\n\n // This should never happen due to the length check above,\n // but TypeScript needs the explicit check\n if (!action) {\n return { newState: state, action: null };\n }\n\n return {\n newState: {\n undoStack: [...state.undoStack, action],\n redoStack,\n },\n action,\n };\n}\n\n/**\n * Check if there are any actions that can be undone.\n *\n * @param state - Current undo/redo state\n * @returns True if undo is available\n */\nexport function canUndo(state: UndoRedoState): boolean {\n return state.undoStack.length > 0;\n}\n\n/**\n * Check if there are any actions that can be redone.\n *\n * @param state - Current undo/redo state\n * @returns True if redo is available\n */\nexport function canRedo(state: UndoRedoState): boolean {\n return state.redoStack.length > 0;\n}\n\n/**\n * Clear all history, returning an empty state.\n *\n * @returns Fresh empty state\n */\nexport function clearHistory(): UndoRedoState {\n return { undoStack: [], redoStack: [] };\n}\n\n/**\n * Create a new edit action with the current timestamp.\n *\n * @param rowIndex - The row index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n * @param rowId - Stable ID of the edited row, when resolvable\n * @returns A new EditAction object\n */\nexport function createEditAction(\n rowIndex: number,\n field: string,\n oldValue: unknown,\n newValue: unknown,\n rowId?: string,\n): EditAction {\n const action: EditAction = {\n type: 'cell-edit',\n rowIndex,\n field,\n oldValue,\n newValue,\n timestamp: Date.now(),\n };\n if (rowId !== undefined) action.rowId = rowId;\n return action;\n}\n\n/**\n * Create a compound action grouping multiple edits into a single undo/redo unit.\n *\n * @param actions - The individual edit actions to group (in chronological order)\n * @returns A CompoundEditAction wrapping all provided edits\n */\nexport function createCompoundAction(actions: EditAction[]): CompoundEditAction {\n return {\n type: 'compound',\n actions,\n timestamp: Date.now(),\n };\n}\n","/**\n * Undo/Redo Plugin (Class-based)\n *\n * Provides undo/redo functionality for cell edits in tbw-grid.\n * Supports Ctrl+Z/Cmd+Z for undo and Ctrl+Y/Cmd+Y (or Ctrl+Shift+Z) for redo.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { NO_TRANSACTION, throwDiagnostic } from '../../core/internal/diagnostics';\nimport { FOCUSABLE_EDITOR_SELECTOR } from '../../core/internal/rows';\nimport { writeCellField } from '../../core/internal/value-accessor';\nimport { BaseGridPlugin, type GridElement, type PluginDependency } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport {\n canRedo,\n canUndo,\n clearHistory,\n createCompoundAction,\n createEditAction,\n pushAction,\n redo,\n undo,\n} from './history';\nimport type { EditAction, UndoRedoAction, UndoRedoConfig, UndoRedoDetail } from './types';\n\n/**\n * Undo/Redo Plugin for tbw-grid\n *\n * Tracks all cell edits and lets users revert or replay changes with familiar keyboard\n * shortcuts (Ctrl+Z / Ctrl+Y). Maintains an in-memory history stack with configurable\n * depth—perfect for data entry workflows where mistakes happen.\n *\n * > **Required Dependency:** This plugin requires EditingPlugin to be loaded first.\n * > UndoRedo tracks the edit history that EditingPlugin creates.\n *\n * ## Installation\n *\n * ```ts\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n * ```\n *\n * ## Keyboard Shortcuts\n *\n * | Shortcut | Action |\n * |----------|--------|\n * | `Ctrl+Z` / `Cmd+Z` | Undo last edit |\n * | `Ctrl+Y` / `Cmd+Shift+Z` | Redo last undone edit |\n *\n * @example Basic Usage with EditingPlugin\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\n * import { UndoRedoPlugin } from '@toolbox-web/grid/plugins/undo-redo';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', editable: true },\n * { field: 'price', header: 'Price', type: 'number', editable: true },\n * ],\n * plugins: [\n * new EditingPlugin({ editOn: 'dblclick' }), // Required - must be first\n * new UndoRedoPlugin({ maxHistorySize: 50 }),\n * ],\n * };\n * ```\n *\n * @see {@link UndoRedoConfig} for configuration options\n * @see EditingPlugin for the required dependency\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> {\n /**\n * Plugin dependencies - UndoRedoPlugin requires EditingPlugin to track edits.\n *\n * The EditingPlugin must be loaded BEFORE this plugin in the plugins array.\n * @internal\n */\n static override readonly dependencies: PluginDependency[] = [\n { name: 'editing', required: true, reason: 'UndoRedoPlugin tracks cell edit history' },\n ];\n\n /** @internal */\n readonly name = 'undoRedo';\n\n /** @internal */\n protected override get defaultConfig(): Partial<UndoRedoConfig> {\n return {\n maxHistorySize: 100,\n };\n }\n\n // State as class properties\n private undoStack: UndoRedoAction[] = [];\n private redoStack: UndoRedoAction[] = [];\n\n /** Suppresses recording during undo/redo to prevent feedback loops. */\n #suppressRecording = false;\n\n /** Accumulates edits during a transaction; `null` when no transaction is active. */\n #transactionBuffer: EditAction[] | null = null;\n\n /**\n * Open-transaction nesting depth. `beginTransaction` increments, `endTransaction`\n * decrements; the buffer is only finalized when it returns to 0. Nesting lets a\n * synchronous burst of commits (e.g. a multi-cell paste routed through\n * `updateRows`, where each cell fires `cell-commit` and consumers bracket it\n * with `beginTransaction()`/`queueMicrotask(endTransaction)`) coalesce into a\n * single compound undo entry instead of throwing \"already in progress\".\n */\n #transactionDepth = 0;\n\n /** Cell to focus after the render scheduled by the last undo/redo (see `afterRender`). */\n #pendingFocus: EditAction | null = null;\n\n /**\n * Resolve the stable ID of the row currently displayed at `rowIndex`.\n * Returns `undefined` when there is no such row or it has no resolvable ID.\n */\n #resolveRowId(rowIndex: number): string | undefined {\n const row = this.rows[rowIndex];\n if (!row) return undefined;\n try {\n return this.grid.getRowId(row) || undefined;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Apply a value to a row cell, using `updateRow()` when possible so that\n * active editors (during row-edit mode) are notified via the `cell-change`\n * → `onValueChange` pipeline. Falls back to direct mutation when the row\n * has no ID.\n *\n * The row is resolved by `action.rowId` (searching the full source data, so\n * filtered-out rows are reachable) — never by `action.rowIndex` when an ID\n * exists, because the display index goes stale once the view is sorted or\n * filtered (#526).\n */\n #applyValue(action: EditAction, value: unknown): void {\n // Prefer updateRow() — it emits `cell-change` events which notify active\n // editors via their `onValueChange` callbacks. Without this, undo/redo\n // during row-edit mode is invisible because the render pipeline skips\n // cells that have active editors.\n //\n // The `'history'` source tells data plugins (editing) to apply the value\n // and recompute dirty state WITHOUT recording a fresh history entry or\n // re-marking the row changed — the undo stack owns this change. This is\n // what breaks the updateRow→commit→cell-edit-committed→record feedback loop.\n if (action.rowId !== undefined) {\n // updateRow resolves the id against the full source data (filtered-out rows\n // included); a row deleted since the edit is a silent no-op for 'history'.\n this.grid.updateRow(action.rowId, { [action.field]: value }, 'history');\n return;\n }\n\n // No row identity: fall back to the display index captured at edit time.\n const rows = this.rows as Record<string, unknown>[];\n const row = rows[action.rowIndex];\n if (!row) return;\n writeCellField(row, action.field, value);\n }\n\n /**\n * Move keyboard focus to the cell targeted by an undo/redo action.\n * If the grid is in row-edit mode and the cell has an active editor,\n * the editor input is focused so the user can continue editing.\n */\n #focusActionCell(action: EditAction): void {\n const internalGrid: GridHost = this.grid;\n\n // Map field name → visible column index\n const colIdx = internalGrid._visibleColumns?.findIndex((c) => c.field === action.field) ?? -1;\n if (colIdx < 0) return;\n\n // Resolve the row's CURRENT display index; skip focus when it is not in view.\n const rowIndex =\n action.rowId !== undefined ? (internalGrid._getRowEntry(action.rowId)?.index ?? -1) : action.rowIndex;\n if (rowIndex < 0) return;\n\n internalGrid._focusRow = rowIndex;\n internalGrid._focusCol = colIdx;\n\n // If we're in row-edit mode, focus the editor input in the target cell\n const rowEl = internalGrid.findRenderedRowElement?.(rowIndex);\n if (!rowEl) return;\n\n const cellEl = rowEl.querySelector(`.cell[data-col=\"${colIdx}\"]`) as HTMLElement | null;\n if (cellEl?.classList.contains(GridClasses.EDITING)) {\n const editor = cellEl.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n editor?.focus({ preventScroll: true });\n }\n }\n\n /**\n * Apply value changes for a single or compound action.\n * Wraps `#applyValue` calls with `#suppressRecording` to prevent feedback loops.\n */\n #applyUndoRedoAction(action: UndoRedoAction, direction: 'undo' | 'redo'): void {\n this.#suppressRecording = true;\n if (action.type === 'compound') {\n const subActions = direction === 'undo' ? [...action.actions].reverse() : action.actions;\n for (const sub of subActions) {\n this.#applyValue(sub, direction === 'undo' ? sub.oldValue : sub.newValue);\n }\n } else {\n this.#applyValue(action, direction === 'undo' ? action.oldValue : action.newValue);\n }\n this.#suppressRecording = false;\n }\n\n /**\n * Focus the cell associated with an undo/redo action.\n * For compound actions, focuses the **last** action's cell. When consumers\n * use `beginTransaction()` + `recordEdit()` (cascaded fields) followed by\n * `queueMicrotask(() => endTransaction())`, the grid's auto-recorded\n * primary field edit is appended last. Focusing it ensures the cursor\n * lands on the field the user originally edited, not on a cascaded field\n * whose column may not even be visible.\n *\n * Focus is applied in `afterRender()`, not here: the change can re-sort or\n * re-filter the view, so the row's display index is only known after the\n * ROWS phase that `requestRenderWithFocus()` schedules.\n */\n #focusUndoRedoAction(action: UndoRedoAction): void {\n const target = action.type === 'compound' ? action.actions[action.actions.length - 1] : action;\n this.#pendingFocus = target ?? null;\n }\n\n /**\n * Apply the focus deferred by an undo/redo once the view reflects the change.\n * @internal\n */\n override afterRender(): void {\n const target = this.#pendingFocus;\n if (!target) return;\n this.#pendingFocus = null;\n this.#focusActionCell(target);\n }\n\n /**\n * Subscribe to cell-edit-committed events from EditingPlugin.\n * @internal\n */\n override attach(grid: GridElement): void {\n super.attach(grid);\n // Auto-record edits via Event Bus\n this.on(\n 'cell-edit-committed',\n (detail: { rowIndex: number; rowId?: string; field: string; oldValue: unknown; newValue: unknown }) => {\n // Skip recording during undo/redo operations. When undo/redo applies a\n // value via updateRow, two things can cause re-entry:\n // 1. updateRow → cell-change → onValueChange → editor triggers commit\n // 2. Browser native undo (if not fully suppressed) fires input event → commit\n // The suppress flag prevents these from corrupting the history stacks.\n if (this.#suppressRecording) return;\n this.recordEdit(detail.rowIndex, detail.field, detail.oldValue, detail.newValue, detail.rowId);\n },\n );\n }\n\n /**\n * Clean up state when plugin is detached.\n * @internal\n */\n override detach(): void {\n this.undoStack = [];\n this.redoStack = [];\n this.#transactionBuffer = null;\n this.#transactionDepth = 0;\n this.#pendingFocus = null;\n }\n\n /**\n * Handle keyboard shortcuts for undo/redo.\n * - Ctrl+Z / Cmd+Z: Undo\n * - Ctrl+Y / Cmd+Y / Ctrl+Shift+Z / Cmd+Shift+Z: Redo\n *\n * When the keystroke originates from a focusable form control (e.g. an\n * active cell editor input, a filter text box) we defer to the browser's\n * native undo/redo first. We watch for a `beforeinput` event with\n * `inputType === 'historyUndo'` / `'historyRedo'` on the target — if the\n * browser had something to undo, it consumes the keystroke and we do\n * nothing; if it didn't (history depleted), we fall back to the grid's\n * own undo/redo. This lets users undo their typing/paste inside an open\n * editor, then continue undoing committed cell edits with the same key.\n * @internal\n */\n override onKeyDown(event: KeyboardEvent): boolean {\n const isUndo = (event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey;\n const isRedo = (event.ctrlKey || event.metaKey) && (event.key === 'y' || (event.key === 'z' && event.shiftKey));\n\n if (!isUndo && !isRedo) return false;\n\n const kind: 'undo' | 'redo' = isUndo ? 'undo' : 'redo';\n\n if (this.#isNativeHistoryTarget(event.target)) {\n // Let the browser try its native undo/redo first. Only run ours if\n // the browser had nothing left in its own history.\n this.#deferToNativeHistory(event.target as HTMLElement, kind);\n return true;\n }\n\n // Prevent browser native undo on text inputs — it would conflict with\n // the grid's undo by mutating the input text independently, triggering\n // re-commits that cancel the grid undo.\n event.preventDefault();\n this.#performHistory(kind);\n return true;\n }\n\n /**\n * Apply an undo or redo against the grid's own history stack.\n */\n #performHistory(kind: 'undo' | 'redo'): void {\n const state = { undoStack: this.undoStack, redoStack: this.redoStack };\n const result = kind === 'undo' ? undo(state) : redo(state);\n if (!result.action) return;\n\n this.#applyUndoRedoAction(result.action, kind);\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n\n this.emit<UndoRedoDetail>(kind, { action: result.action, type: kind });\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n\n /**\n * True when the keystroke target is a form control with its own native\n * undo history (text inputs, textareas, contenteditable). `<select>` is\n * intentionally excluded — it has no undo history.\n */\n #isNativeHistoryTarget(target: EventTarget | null): boolean {\n if (!(target instanceof Element)) return false;\n return !!target.closest('input, textarea, [contenteditable=\"\"], [contenteditable=\"true\"]');\n }\n\n /**\n * Watch for the browser's native undo/redo on `target`. The browser fires\n * `beforeinput` with `inputType: 'historyUndo' | 'historyRedo'` when it\n * actually has something to undo/redo. If we don't observe that within a\n * microtask of the keydown returning, the native history is depleted and\n * we run the grid's own undo/redo to extend the chain.\n */\n #deferToNativeHistory(target: HTMLElement, kind: 'undo' | 'redo'): void {\n const wantedInputType = kind === 'undo' ? 'historyUndo' : 'historyRedo';\n let nativeHandled = false;\n\n const onBeforeInput = (e: Event) => {\n if ((e as InputEvent).inputType === wantedInputType) nativeHandled = true;\n };\n target.addEventListener('beforeinput', onBeforeInput, { capture: true });\n\n queueMicrotask(() => {\n target.removeEventListener('beforeinput', onBeforeInput, { capture: true } as EventListenerOptions);\n if (!nativeHandled) this.#performHistory(kind);\n });\n }\n\n // #region Public API Methods\n\n /**\n * Record a cell edit for undo/redo tracking.\n * Call this when a cell value changes.\n *\n * The row's stable ID is captured from the row currently displayed at `rowIndex`,\n * so call this while `rowIndex` still points at the edited row (e.g. synchronously\n * inside a `cell-commit` handler). Undo/redo then target that row by ID even after\n * the view is sorted or filtered. Pass `rowId` explicitly when the row is not in\n * the view (e.g. `cell-commit` from an `updateRow()` on a filtered-out row reports\n * `rowIndex: -1`) — `detail.rowId` from `cell-commit` is the natural source.\n *\n * @param rowIndex - The display (processed-row) index where the edit occurred\n * @param field - The field (column key) that was edited\n * @param oldValue - The value before the edit\n * @param newValue - The value after the edit\n * @param rowId - Stable ID of the edited row; resolved from `rowIndex` when omitted (since 3.9.2)\n */\n recordEdit(rowIndex: number, field: string, oldValue: unknown, newValue: unknown, rowId?: string): void {\n const action = createEditAction(rowIndex, field, oldValue, newValue, rowId || this.#resolveRowId(rowIndex));\n\n // Buffer during transactions instead of pushing to undo stack\n if (this.#transactionBuffer) {\n this.#transactionBuffer.push(action);\n return;\n }\n\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Begin grouping subsequent edits into a single compound action.\n *\n * While a transaction is active, all `recordEdit()` calls (both manual\n * and auto-recorded from `cell-edit-committed`) are buffered instead of\n * pushed to the undo stack. Call `endTransaction()` to finalize the group.\n *\n * **Nesting:** transactions are re-entrant. Calling `beginTransaction()` while\n * one is already open increments a depth counter rather than throwing; the\n * buffer is finalized only when a matching `endTransaction()` returns the depth\n * to 0. This lets a synchronous burst of commits (e.g. a multi-cell paste, where\n * each `cell-commit` handler brackets itself with `beginTransaction()` +\n * `queueMicrotask(endTransaction)`) coalesce into a single compound undo entry.\n *\n * **Typical usage** — group a user edit with its cascaded side-effects:\n *\n * ```ts\n * grid.on('cell-commit', () => {\n * const undoRedo = grid.getPluginByName('undoRedo');\n * undoRedo.beginTransaction();\n *\n * // Record cascaded updates (these won't auto-record)\n * const oldB = row.fieldB;\n * undoRedo.recordEdit(rowIndex, 'fieldB', oldB, computedB);\n * grid.updateRow(rowId, { fieldB: computedB });\n *\n * // End after the auto-recorded original edit is captured\n * queueMicrotask(() => undoRedo.endTransaction());\n * });\n * ```\n */\n beginTransaction(): void {\n if (this.#transactionDepth === 0) {\n this.#transactionBuffer = [];\n }\n this.#transactionDepth++;\n }\n\n /**\n * Finalize the current transaction, wrapping all buffered edits into a\n * single compound action on the undo stack.\n *\n * - If the buffer contains multiple edits, they are wrapped in a `CompoundEditAction`.\n * - If the buffer contains a single edit, it is pushed as a regular `EditAction`.\n * - If the buffer is empty, this is a no-op.\n *\n * Undoing a compound action reverts all edits in reverse order; redoing\n * replays them in forward order.\n *\n * When transactions are nested, only the outermost `endTransaction()` (the one\n * that returns the depth to 0) finalizes the buffer; inner calls just decrement.\n *\n * @throws Error if no transaction is in progress\n */\n endTransaction(): void {\n if (this.#transactionDepth === 0) {\n throwDiagnostic(NO_TRANSACTION, 'No transaction in progress. Call beginTransaction() first.');\n }\n\n this.#transactionDepth--;\n if (this.#transactionDepth > 0) return; // still inside an outer transaction\n\n const buffer = this.#transactionBuffer;\n this.#transactionBuffer = null;\n if (!buffer || buffer.length === 0) return;\n\n const action: UndoRedoAction = buffer.length === 1 ? buffer[0] : createCompoundAction(buffer);\n const newState = pushAction(\n { undoStack: this.undoStack, redoStack: this.redoStack },\n action,\n this.config.maxHistorySize ?? 100,\n );\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n }\n\n /**\n * Programmatically undo the last action.\n *\n * @returns The undone action, or null if nothing to undo\n */\n undo(): UndoRedoAction | null {\n const result = undo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'undo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Programmatically redo the last undone action.\n *\n * @returns The redone action, or null if nothing to redo\n */\n redo(): UndoRedoAction | null {\n const result = redo({ undoStack: this.undoStack, redoStack: this.redoStack });\n if (result.action) {\n this.#applyUndoRedoAction(result.action, 'redo');\n this.undoStack = result.newState.undoStack;\n this.redoStack = result.newState.redoStack;\n this.#focusUndoRedoAction(result.action);\n this.requestRenderWithFocus();\n }\n return result.action;\n }\n\n /**\n * Check if there are any actions that can be undone.\n */\n canUndo(): boolean {\n return canUndo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Check if there are any actions that can be redone.\n */\n canRedo(): boolean {\n return canRedo({ undoStack: this.undoStack, redoStack: this.redoStack });\n }\n\n /**\n * Clear all undo/redo history.\n */\n clearHistory(): void {\n const newState = clearHistory();\n this.undoStack = newState.undoStack;\n this.redoStack = newState.redoStack;\n this.#transactionBuffer = null;\n this.#transactionDepth = 0;\n }\n\n /**\n * Get a copy of the current undo stack.\n */\n getUndoStack(): UndoRedoAction[] {\n return [...this.undoStack];\n }\n\n /**\n * Get a copy of the current redo stack.\n */\n getRedoStack(): UndoRedoAction[] {\n return [...this.redoStack];\n }\n // #endregion\n}\n"],"mappings":"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"}
|
|
@@ -30,8 +30,20 @@ export interface UndoRedoConfig {
|
|
|
30
30
|
export interface EditAction {
|
|
31
31
|
/** Type of action - currently only 'cell-edit' is supported */
|
|
32
32
|
type: 'cell-edit';
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The display (processed-row) index where the edit occurred, at the time of the edit.
|
|
35
|
+
* Only used to locate the row when {@link EditAction.rowId | rowId} is unavailable —
|
|
36
|
+
* it goes stale as soon as a sort, filter, or row insert/removal changes the view.
|
|
37
|
+
*/
|
|
34
38
|
rowIndex: number;
|
|
39
|
+
/**
|
|
40
|
+
* Stable ID of the edited row (from `getRowId` / `row.id`), captured when the edit is recorded.
|
|
41
|
+
* Undo/redo resolve the target row by this ID, so they keep hitting the right row after the
|
|
42
|
+
* view is sorted or filtered — including rows currently filtered out of view.
|
|
43
|
+
* `undefined` when the row has no resolvable ID; the action then falls back to `rowIndex`.
|
|
44
|
+
* @since 3.9.2
|
|
45
|
+
*/
|
|
46
|
+
rowId?: string;
|
|
35
47
|
/** The field (column key) that was edited */
|
|
36
48
|
field: string;
|
|
37
49
|
/** The value before the edit */
|
|
@@ -64,6 +64,11 @@ export declare class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> {
|
|
|
64
64
|
protected get defaultConfig(): Partial<UndoRedoConfig>;
|
|
65
65
|
private undoStack;
|
|
66
66
|
private redoStack;
|
|
67
|
+
/**
|
|
68
|
+
* Apply the focus deferred by an undo/redo once the view reflects the change.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
afterRender(): void;
|
|
67
72
|
/**
|
|
68
73
|
* Subscribe to cell-edit-committed events from EditingPlugin.
|
|
69
74
|
* @internal
|
|
@@ -94,12 +99,20 @@ export declare class UndoRedoPlugin extends BaseGridPlugin<UndoRedoConfig> {
|
|
|
94
99
|
* Record a cell edit for undo/redo tracking.
|
|
95
100
|
* Call this when a cell value changes.
|
|
96
101
|
*
|
|
97
|
-
*
|
|
102
|
+
* The row's stable ID is captured from the row currently displayed at `rowIndex`,
|
|
103
|
+
* so call this while `rowIndex` still points at the edited row (e.g. synchronously
|
|
104
|
+
* inside a `cell-commit` handler). Undo/redo then target that row by ID even after
|
|
105
|
+
* the view is sorted or filtered. Pass `rowId` explicitly when the row is not in
|
|
106
|
+
* the view (e.g. `cell-commit` from an `updateRow()` on a filtered-out row reports
|
|
107
|
+
* `rowIndex: -1`) — `detail.rowId` from `cell-commit` is the natural source.
|
|
108
|
+
*
|
|
109
|
+
* @param rowIndex - The display (processed-row) index where the edit occurred
|
|
98
110
|
* @param field - The field (column key) that was edited
|
|
99
111
|
* @param oldValue - The value before the edit
|
|
100
112
|
* @param newValue - The value after the edit
|
|
113
|
+
* @param rowId - Stable ID of the edited row; resolved from `rowIndex` when omitted (since 3.9.2)
|
|
101
114
|
*/
|
|
102
|
-
recordEdit(rowIndex: number, field: string, oldValue: unknown, newValue: unknown): void;
|
|
115
|
+
recordEdit(rowIndex: number, field: string, oldValue: unknown, newValue: unknown, rowId?: string): void;
|
|
103
116
|
/**
|
|
104
117
|
* Begin grouping subsequent edits into a single compound action.
|
|
105
118
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toolbox-web/grid",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.2",
|
|
4
4
|
"description": "Zero-dependency, framework-agnostic data grid web component with virtualization, sorting, filtering, editing, and 20+ plugins. Works in vanilla JS, React, Vue, Angular, and any framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|