@keenmate/web-grid 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  A feature-rich, framework-agnostic data grid web component built with TypeScript. Sorting, filtering, pagination, inline editing (8 editor types), cell range selection, clipboard support, row toolbar, context menus, frozen columns, column reorder/resize, fill handle, virtual scroll, dark mode, and full CSS variable theming — all in a Shadow DOM encapsulated `<web-grid>` element.
4
4
 
5
+ ## What's New in v1.1.0
6
+
7
+ - **Tree / hierarchy mode**: Render tree-structured data using ltree-style path strings (`"1.2.3"`, `"/a/b/c"`, `"C:\\Win\\Sys"` — separator auto-detected). Mark one column with `isTree: true` to add depth-based indentation and an expand/collapse chevron. Sort is sibling-aware, filter auto-expands ancestors of matches, pagination operates on visible rows. New props: `treePathMember`, `treeLevelMember`, `treeParentMember`, `treeSeparator`, `treeDataSorted`, `expandedPaths`, `defaultExpandDepth`. Methods: `toggleExpandedPath`, `expandAll`, `collapseAll`, `getRowTreeInfo`. Event: `onexpandedpathschange`.
8
+ - **Custom chevron icons via `treeChevronCallback`**: Receives `{ expanded, hasChildren, row, level, path }`, returns HTML for the chevron's inner content. Result is cached per `(row, expanded, hasChildren)` so the callback fires at most a handful of times per row. Use it for file/folder icons, status indicators, anything per-row. Static fallbacks: `treeExpandedGlyph` / `treeCollapsedGlyph`.
9
+ - **Double-click to expand/collapse**: New `treeDoubleClickBehavior: 'none' | 'toggle'`. When set to `'toggle'`, double-clicking anywhere in the tree column expands/collapses the node — works reliably even when the cell DOM is re-rendered between clicks (uses `MouseEvent.detail`, not the unreliable native `dblclick`).
10
+ - **No more spurious `onrowchange` events**: Entering edit mode and exiting via arrow keys without typing no longer fires phantom "X → X" change events. `commitEdit` now only fires `onrowchange` when the value actually changed (or validation failed).
11
+ - **Pathological filler-cell width fix**: Removed `min-width: max-content` from `.wg__table` — it was redundant with `table-layout: fixed` and triggered intrinsic-size computation that ballooned the filler to hundreds of thousands of pixels in some browsers (Firefox especially) when cells contained absolutely-positioned editors.
12
+
5
13
  ## What's New in v1.0.5
6
14
 
7
15
  - **Context menus flip at screen edges**: Both cell and header context menus now correctly flip to the opposite side when opened near a viewport edge (they were clipping before). Switched the root menu to `strategy: 'fixed'` + `position: fixed` so Floating UI's `flip`/`shift` run in viewport coordinates.
@@ -10,11 +18,6 @@ A feature-rich, framework-agnostic data grid web component built with TypeScript
10
18
  - **Context menu offset flips with placement**: `contextMenuXOffset`/`contextMenuYOffset` are now applied via Floating UI's `offset` middleware (`mainAxis` / `alignmentAxis`), so the gap between cursor and menu stays correct even when the menu flips to `*-end` or `top-*`.
11
19
  - **Dropdown selected option readable in dark mode**: The selected option in select/combobox/autocomplete dropdowns no longer renders as pale blue against the dark surface. `--wg-accent-color-light` now falls back to a transparent `color-mix` that blends with the underlying surface in either theme.
12
20
 
13
- ## What's New in v1.0.4
14
-
15
- - **Dirty cell/row indicator**: New `isDirtyIndicatorVisible` property (default: `true`). Edited cells show a subtle orange tint + corner triangle; row numbers get an orange left border. Themable via `--wg-dirty-*` variables. Public methods: `isCellDirty()`, `isRowDirty()`.
16
- - **Dropdown positioning fix**: Fixed dropdown editors appearing offset in shadow DOM by switching from `position: fixed` to `position: absolute`.
17
-
18
21
  ## Installation
19
22
 
20
23
  ```bash
package/dist/grid.d.ts CHANGED
@@ -1,4 +1,16 @@
1
- import type { Column, CellValidationState, RowToolbarConfig, ContextMenuItem, RowShortcut, RowChangeDetail, ToolbarClickDetail, RowActionClickDetail, ContextMenuContext, HeaderMenuConfig, HeaderMenuContext, EditTrigger, EditStartSelection, EditingCell, FocusedCell, SortMode, SortState, DataRequestDetail, DataRequestTrigger, BeforeCommitResult, GridMode, ToggleVisibility, PaginationLabelsCallback, SummaryContentCallback, ValidationTooltipContext, ToolbarPosition, GridLabels, RowLockInfo, RowLockingOptions, RowLockChangeDetail, ColumnWidthState, ColumnResizeDetail, ColumnOrderState, ColumnReorderDetail, FillDragDetail, FillDirection, RangeShortcut, CellSelectionMode, CellRange, CellSelectionChangeDetail, RowFocusDetail, PasteMode, BeforePasteDetail, PasteDetail, CreateRowCallback, NewRowPosition } from './types.js';
1
+ import type { Column, CellValidationState, RowToolbarConfig, ContextMenuItem, RowShortcut, RowChangeDetail, ToolbarClickDetail, RowActionClickDetail, ContextMenuContext, HeaderMenuConfig, HeaderMenuContext, EditTrigger, EditStartSelection, EditingCell, FocusedCell, SortMode, SortState, DataRequestDetail, DataRequestTrigger, BeforeCommitResult, GridMode, ToggleVisibility, PaginationLabelsCallback, SummaryContentCallback, ValidationTooltipContext, ToolbarPosition, GridLabels, RowLockInfo, RowLockingOptions, RowLockChangeDetail, ColumnWidthState, ColumnResizeDetail, ColumnOrderState, ColumnReorderDetail, FillDragDetail, FillDirection, RangeShortcut, CellSelectionMode, CellRange, CellSelectionChangeDetail, RowFocusDetail, PasteMode, BeforePasteDetail, PasteDetail, CreateRowCallback, NewRowPosition, TreeExpandedChangeDetail, TreeChevronCallback, TreeDoubleClickBehavior } from './types.js';
2
+ type TreeNode = {
3
+ path: string;
4
+ parent: string | null;
5
+ level: number;
6
+ rowIndex: number;
7
+ childPaths: string[];
8
+ };
9
+ type TreeIndex = {
10
+ separator: string;
11
+ nodes: Map<string, TreeNode>;
12
+ rootPaths: string[];
13
+ };
2
14
  /**
3
15
  * WebGrid - Core logic class for the data grid
4
16
  *
@@ -155,6 +167,21 @@ export declare class WebGrid<T = unknown> {
155
167
  protected _newRowIndicator: string;
156
168
  protected _createEmptyRowCallback: (() => T | Promise<T>) | undefined;
157
169
  protected _emptyRowDraft: T | null;
170
+ protected _treePathMember: string | null;
171
+ protected _treeLevelMember: string | null;
172
+ protected _treeParentMember: string | null;
173
+ protected _treeSeparator: string | null;
174
+ protected _treeDataSorted: boolean;
175
+ protected _expandedPaths: Set<string>;
176
+ protected _expandedPathsExternal: Set<string> | null;
177
+ protected _defaultExpandDepth: number | null;
178
+ protected _treeIndex: TreeIndex | null;
179
+ protected _onexpandedpathschange: ((detail: TreeExpandedChangeDetail) => void) | undefined;
180
+ protected _treeDoubleClickBehavior: TreeDoubleClickBehavior;
181
+ protected _treeExpandedGlyph: string;
182
+ protected _treeCollapsedGlyph: string;
183
+ protected _treeChevronCallback: TreeChevronCallback<T> | undefined;
184
+ protected _treeChevronCache: WeakMap<object, Map<string, string>>;
158
185
  get items(): T[];
159
186
  set items(value: T[]);
160
187
  get columns(): Column<T>[];
@@ -440,6 +467,64 @@ export declare class WebGrid<T = unknown> {
440
467
  set newRowIndicator(value: string);
441
468
  get createEmptyRowCallback(): (() => T | Promise<T>) | undefined;
442
469
  set createEmptyRowCallback(value: (() => T | Promise<T>) | undefined);
470
+ get treePathMember(): string | null;
471
+ set treePathMember(value: string | null);
472
+ get treeLevelMember(): string | null;
473
+ set treeLevelMember(value: string | null);
474
+ get treeParentMember(): string | null;
475
+ set treeParentMember(value: string | null);
476
+ get treeSeparator(): string | null;
477
+ set treeSeparator(value: string | null);
478
+ get treeDataSorted(): boolean;
479
+ set treeDataSorted(value: boolean);
480
+ get expandedPaths(): Set<string>;
481
+ set expandedPaths(value: Set<string> | null | undefined);
482
+ get defaultExpandDepth(): number | null;
483
+ set defaultExpandDepth(value: number | null);
484
+ get onexpandedpathschange(): ((detail: TreeExpandedChangeDetail) => void) | undefined;
485
+ set onexpandedpathschange(value: ((detail: TreeExpandedChangeDetail) => void) | undefined);
486
+ get isTreeMode(): boolean;
487
+ isPathExpanded(path: string): boolean;
488
+ toggleExpandedPath(path: string): void;
489
+ expandAll(): void;
490
+ collapseAll(): void;
491
+ /** Returns tree info for a row (used by rendering) */
492
+ getRowTreeInfo(item: T): {
493
+ path: string;
494
+ level: number;
495
+ hasChildren: boolean;
496
+ } | null;
497
+ get treeDoubleClickBehavior(): TreeDoubleClickBehavior;
498
+ set treeDoubleClickBehavior(value: TreeDoubleClickBehavior);
499
+ get treeExpandedGlyph(): string;
500
+ set treeExpandedGlyph(value: string);
501
+ get treeCollapsedGlyph(): string;
502
+ set treeCollapsedGlyph(value: string);
503
+ get treeChevronCallback(): TreeChevronCallback<T> | undefined;
504
+ set treeChevronCallback(value: TreeChevronCallback<T> | undefined);
505
+ /**
506
+ * Resolve the inner HTML for a row's chevron, using callback (cached) or static glyph.
507
+ * Cache is keyed per (row reference, expanded). When the row reference changes
508
+ * (immutable update) or items are replaced, stale entries are dropped via WeakMap GC
509
+ * or explicit cache clear.
510
+ */
511
+ getTreeChevronHtml(item: T, expanded: boolean, hasChildren: boolean, level: number, path: string): string;
512
+ /** Read the path field from a row */
513
+ protected getRowTreePath(item: T): string | null;
514
+ /** Detect separator from a sample path */
515
+ protected detectTreeSeparator(path: string): string;
516
+ /** Rebuild _treeIndex from current items */
517
+ protected rebuildTreeState(): void;
518
+ /** Re-initialize the active expanded set after items/config changes */
519
+ protected rebuildExpandedPaths(): void;
520
+ /** Compare two tree nodes using current sort state, falling back to path */
521
+ protected compareTreeNodes(a: TreeNode, b: TreeNode): number;
522
+ /** Compute paths matched by current text filters, plus their ancestors */
523
+ protected getTreeFilterAllowedPaths(): Set<string> | null;
524
+ /** Items in tree-aware sort order (depth-first, sibling-sorted), with filter applied */
525
+ protected getTreeSortedItems(): T[];
526
+ /** Items currently visible (post-collapse). When filter is active, ancestors of matches are auto-expanded. */
527
+ protected getTreeVisibleItems(): T[];
443
528
  selectCellRange(range: CellRange): void;
444
529
  clearCellSelection(): void;
445
530
  /** Clear cell selection state without triggering re-render (caller handles visuals) */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { GridElement } from './web-component.js';
2
2
  export { WebGrid } from './grid.js';
3
- export type { EditorType, EditTrigger, OptionsLoadTrigger, DateOutputFormat, EditorOption, EditorOptions, CustomEditorContext, CellValidationState, ValidationResult, BeforeCommitContext, BeforeCommitResult, Column, CellRenderCallback, RowChangeDetail, RowFocusDetail, PredefinedToolbarItemType, ToolbarPosition, NewRowPosition, ToolbarTooltip, RowToolbarItem, RowToolbarConfig, NormalizedToolbarItem, ToolbarClickDetail, RowActionType, RowActionClickDetail, ContextMenuContext, ContextMenuItem, QuickGridProps, SortState, DataRequestDetail, DataRequestTrigger, GridLabels, RowLockInfo, RowLockingOptions, RowLockChangeDetail, LockedRowEditBehavior, ColumnWidthState, ColumnResizeDetail, ColumnOrderState, ColumnReorderDetail, FillDragDetail, FillDirection, RangeShortcut, RangeShortcutContext, CellSelectionMode, CellRange, CellSelectionChangeDetail, PasteMode, PasteColumnMapping, BeforePasteDetail, PasteCellResult, PasteDetail, CreateRowCallback, EditingCell, FocusedCell, SortDirection, ToolbarRowGroup, PopupPosition, ConnectorArrowDir } from './types.js';
3
+ export type { EditorType, EditTrigger, OptionsLoadTrigger, DateOutputFormat, EditorOption, EditorOptions, CustomEditorContext, CellValidationState, ValidationResult, BeforeCommitContext, BeforeCommitResult, Column, CellRenderCallback, RowChangeDetail, RowFocusDetail, PredefinedToolbarItemType, ToolbarPosition, NewRowPosition, ToolbarTooltip, RowToolbarItem, RowToolbarConfig, NormalizedToolbarItem, ToolbarClickDetail, RowActionType, RowActionClickDetail, ContextMenuContext, ContextMenuItem, QuickGridProps, SortState, DataRequestDetail, DataRequestTrigger, GridLabels, RowLockInfo, RowLockingOptions, RowLockChangeDetail, LockedRowEditBehavior, ColumnWidthState, ColumnResizeDetail, ColumnOrderState, ColumnReorderDetail, FillDragDetail, FillDirection, RangeShortcut, RangeShortcutContext, CellSelectionMode, CellRange, CellSelectionChangeDetail, PasteMode, PasteColumnMapping, BeforePasteDetail, PasteCellResult, PasteDetail, CreateRowCallback, TreeExpandedChangeDetail, TreeChevronContext, TreeChevronCallback, TreeDoubleClickBehavior, EditingCell, FocusedCell, SortDirection, ToolbarRowGroup, PopupPosition, ConnectorArrowDir } from './types.js';
4
4
  export { GridElement as default } from './web-component.js';
@@ -0,0 +1,10 @@
1
+ import type { Column } from '../../types.js';
2
+ import type { GridContext } from '../types.js';
3
+ /**
4
+ * Wrap cell content with tree indent + chevron when column is the tree column.
5
+ * Returns the original innerHtml unchanged when tree mode is off or column isn't isTree.
6
+ *
7
+ * Used by both the full-table render (table.ts) and the surgical single-cell
8
+ * render (cell.ts), so both paths keep the chevron after focus/edit transitions.
9
+ */
10
+ export declare function wrapTreeCell<T>(ctx: GridContext<T>, column: Column<T>, item: T, innerHtml: string): string;
package/dist/types.d.ts CHANGED
@@ -134,6 +134,7 @@ export type Column<T> = {
134
134
  isMovable?: boolean;
135
135
  fillDirection?: FillDirection;
136
136
  isHidden?: boolean;
137
+ isTree?: boolean;
137
138
  };
138
139
  export type ValidationTooltipContext<T> = {
139
140
  field: string;
@@ -401,6 +402,18 @@ export type QuickGridProps<T> = {
401
402
  cellSelectionMode?: CellSelectionMode;
402
403
  shouldCopyWithHeaders?: boolean;
403
404
  oncellselectionchange?: (detail: CellSelectionChangeDetail) => void;
405
+ treePathMember?: keyof T | string;
406
+ treeLevelMember?: keyof T | string;
407
+ treeParentMember?: keyof T | string;
408
+ treeSeparator?: string;
409
+ treeDataSorted?: boolean;
410
+ expandedPaths?: Set<string>;
411
+ defaultExpandDepth?: number;
412
+ treeDoubleClickBehavior?: TreeDoubleClickBehavior;
413
+ onexpandedpathschange?: (detail: TreeExpandedChangeDetail) => void;
414
+ treeExpandedGlyph?: string;
415
+ treeCollapsedGlyph?: string;
416
+ treeChevronCallback?: TreeChevronCallback<T>;
404
417
  isNewRowEnabled?: boolean;
405
418
  newRowPosition?: NewRowPosition;
406
419
  newRowIndicator?: string;
@@ -595,3 +608,25 @@ export type PasteDetail<T> = {
595
608
  };
596
609
  /** Callback to create new rows from pasted data */
597
610
  export type CreateRowCallback<T> = (pastedData: Record<string, unknown>, rowIndex: number) => T;
611
+ /** What happens when the user double-clicks a tree-column cell */
612
+ export type TreeDoubleClickBehavior = 'none' | 'toggle';
613
+ /** Detail passed to onexpandedpathschange when user toggles a tree node */
614
+ export type TreeExpandedChangeDetail = {
615
+ path: string;
616
+ expanded: boolean;
617
+ expandedPaths: Set<string>;
618
+ };
619
+ /** Context passed to treeChevronCallback */
620
+ export type TreeChevronContext<T> = {
621
+ expanded: boolean;
622
+ hasChildren: boolean;
623
+ row: T;
624
+ level: number;
625
+ path: string;
626
+ };
627
+ /**
628
+ * Callback that returns HTML for the chevron's inner content.
629
+ * Result is cached per (row, expanded) — the callback is invoked at most twice
630
+ * per row, once per expanded state. Invalidated when items or the callback change.
631
+ */
632
+ export type TreeChevronCallback<T> = (context: TreeChevronContext<T>) => string;
@@ -196,6 +196,34 @@ export declare class GridElement<T = unknown> extends HTMLElement implements Gri
196
196
  set newRowIndicator(value: string);
197
197
  get createEmptyRowCallback(): () => T | Promise<T>;
198
198
  set createEmptyRowCallback(value: () => T | Promise<T>);
199
+ get treePathMember(): string | null;
200
+ set treePathMember(value: string | null);
201
+ get treeLevelMember(): string | null;
202
+ set treeLevelMember(value: string | null);
203
+ get treeParentMember(): string | null;
204
+ set treeParentMember(value: string | null);
205
+ get treeSeparator(): string | null;
206
+ set treeSeparator(value: string | null);
207
+ get treeDataSorted(): boolean;
208
+ set treeDataSorted(value: boolean);
209
+ get expandedPaths(): Set<string>;
210
+ set expandedPaths(value: Set<string> | null | undefined);
211
+ get defaultExpandDepth(): number | null;
212
+ set defaultExpandDepth(value: number | null);
213
+ set onexpandedpathschange(value: ((detail: import('./types.js').TreeExpandedChangeDetail) => void) | undefined);
214
+ get onexpandedpathschange(): ((detail: import("./types.js").TreeExpandedChangeDetail) => void) | undefined;
215
+ isPathExpanded(path: string): boolean;
216
+ toggleExpandedPath(path: string): void;
217
+ expandAll(): void;
218
+ collapseAll(): void;
219
+ get treeDoubleClickBehavior(): import('./types.js').TreeDoubleClickBehavior;
220
+ set treeDoubleClickBehavior(value: import('./types.js').TreeDoubleClickBehavior);
221
+ get treeExpandedGlyph(): string;
222
+ set treeExpandedGlyph(value: string);
223
+ get treeCollapsedGlyph(): string;
224
+ set treeCollapsedGlyph(value: string);
225
+ get treeChevronCallback(): import('./types.js').TreeChevronCallback<T> | undefined;
226
+ set treeChevronCallback(value: import('./types.js').TreeChevronCallback<T> | undefined);
199
227
  get isShortcutsHelpVisible(): boolean;
200
228
  set isShortcutsHelpVisible(value: boolean);
201
229
  get shortcutsHelpPosition(): 'top-right' | 'top-left';