@guiexpert/table 1.1.96 → 1.1.97

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.
@@ -26,6 +26,8 @@ import { AreaObjectMapType } from './data/common/area-map.type.js';
26
26
  import { TableCellUpdateEventIf } from './data/common/event/input/table-cell-update-event.if.js';
27
27
  import { AriaGridInfo } from './aria/aria-grid-info.js';
28
28
  import { AriaTextsIf } from './aria/aria-texts.if.js';
29
+ import { RowLayout } from './row-layout/row-layout.js';
30
+ import { MasterDetailService } from './master-detail/master-detail-service.js';
29
31
 
30
32
  /** @category Internals */
31
33
  export declare class RenderScope extends EleScope {
@@ -103,6 +105,16 @@ export declare class RenderScope extends EleScope {
103
105
  * @since 1.1.96
104
106
  */
105
107
  fillHandleService?: FillHandleService;
108
+ /**
109
+ * Master/detail of this table (detail rows below master rows) – created by {@link TableScope} with
110
+ * `tableOptions.masterDetail`.
111
+ * @since 1.1.97
112
+ */
113
+ masterDetailService?: MasterDetailService;
114
+ /** Positions of the body rows (prefix sums of the heights + detail rows), see {@link getBodyRowLayout}. */
115
+ private readonly bodyRowLayout;
116
+ private bodyRowLayoutDirty;
117
+ private bodyRowLayoutModel?;
106
118
  constructor(hostElement: HTMLDivElement, tableModel: TableModelIf, dom: ConvenienceDomService, tableOptions: TableOptionsIf);
107
119
  /**
108
120
  * The DOM service of this table (all DOM access of the table goes through it).
@@ -155,6 +167,52 @@ export declare class RenderScope extends EleScope {
155
167
  * @since 1.1.96
156
168
  */
157
169
  getBodyCellAtHostPoint(x: number, y: number): [number, number];
170
+ /**
171
+ * The positions of the body rows: prefix sums of the row heights (O(log n) lookups also for variable heights) plus
172
+ * the heights of open detail rows. Built lazily – again after {@link invalidateRowLayout}, a new body model or
173
+ * another row count.
174
+ *
175
+ * @returns {RowLayout} the layout of the body rows
176
+ * @since 1.1.97
177
+ */
178
+ getBodyRowLayout(): RowLayout;
179
+ /**
180
+ * Marks the positions of the body rows as outdated (rows filtered, sorted, edited, added or removed, a detail row
181
+ * opened or closed) – they are computed again on the next access.
182
+ *
183
+ * @since 1.1.97
184
+ */
185
+ invalidateRowLayout(): void;
186
+ /**
187
+ * Sets the height of the detail row behind one body row in the current row layout without building it again
188
+ * (a detail row was expanded or collapsed). Does nothing if the layout is outdated anyway.
189
+ *
190
+ * @param {number} rowIndex - display index of the master row
191
+ * @param {number} height - the detail height, 0 = none
192
+ * @returns {boolean} true if the layout was updated, false if it is outdated (it is built again on the next access)
193
+ * @since 1.1.97
194
+ */
195
+ setBodyRowExtra(rowIndex: number, height: number): boolean;
196
+ /** True if open detail rows take space between the body rows. */
197
+ protected hasBodyRowExtras(): boolean;
198
+ /**
199
+ * The top of a body row in content pixels: {@link AreaModelIf.getYPosByRowIndex} plus the heights of the open detail
200
+ * rows above it (master/detail).
201
+ *
202
+ * @param {number} rowIndex - display index
203
+ * @returns {number} px from the top of the first body row
204
+ * @since 1.1.97
205
+ */
206
+ getBodyRowTop(rowIndex: number): number;
207
+ /**
208
+ * The body row at a content position (a position in an open detail row belongs to its master row). Above the rows:
209
+ * 0, below: the last row, without rows: -1.
210
+ *
211
+ * @param {number} contentY - px from the top of the first body row
212
+ * @returns {number} display index
213
+ * @since 1.1.97
214
+ */
215
+ getBodyRowIndexAtY(contentY: number): number;
158
216
  /**
159
217
  * Scroll bar pixels per content pixel: 1, below 1 for a capped scroll height ({@link isScrollHeightCapped}).
160
218
  *
@@ -338,6 +396,15 @@ export declare class RenderScope extends EleScope {
338
396
  * scaled and scrollTop is no longer a content pixel position.
339
397
  */
340
398
  isScrollHeightCapped(): boolean;
399
+ /**
400
+ * The model's content height plus the heights of open detail rows (master/detail).
401
+ *
402
+ * @returns {number} px
403
+ * @since 1.1.97
404
+ */
405
+ /** The detail layer (master/detail) covers the visible body. @since 1.1.97 */
406
+ protected adjustDetailLayerBounds(left: number, top: number, width: number, height: number): void;
407
+ protected getBodyContentHeight(): number;
341
408
  /** Scrollable range of the content in content pixels (content height minus visible body height). */
342
409
  protected getContentScrollRangeY(): number;
343
410
  /** Scrollable range of the scroll viewport in scroll bar pixels. */
@@ -550,6 +617,12 @@ export declare class RenderScope extends EleScope {
550
617
  *
551
618
  * @returns {void}
552
619
  */
620
+ /**
621
+ * Hides the hover row and column – e.g. while the pointer is over a detail row (master/detail).
622
+ *
623
+ * @since 1.1.97
624
+ */
625
+ hideHover(): void;
553
626
  protected hideHoverRow(): void;
554
627
  /**
555
628
  * Adjusts the position and size of the hover column based on the mouse move event.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Vertical positions of the body rows: the row heights as prefix sums plus optional extra heights behind single rows
3
+ * (the detail rows of master/detail). DOM-free; the table keeps one instance for its body
4
+ * ({@link RenderScope.getBodyRowTop}, {@link RenderScope.getBodyRowIndexAtY}).
5
+ *
6
+ * - Constant row height: positions are computed (`i * h`), no array.
7
+ * - Variable row heights: a `Float64Array` of `rowCount + 1` prefix sums, built lazily on the first access after
8
+ * {@link setRows} – then {@link getTop} is O(1) and {@link findIndexAt} O(log n) (before 1.1.97 the table summed the
9
+ * heights of all rows above the viewport in every scroll frame).
10
+ * - Extras: sorted row indices with their extra heights and the prefix sums of the extras (O(log k) per lookup,
11
+ * k = number of extras). Changing one extra height ({@link setExtraHeight}) costs O(k), not O(n).
12
+ *
13
+ * The slot of a row is the row plus its extra height below it: `getTop(i + 1) = getTop(i) + getRowHeight(i) +
14
+ * getExtraHeight(i)`.
15
+ *
16
+ * @example
17
+ * const layout = new RowLayout();
18
+ * layout.setRows(1000, i => i % 2 ? 30 : 50); // variable heights
19
+ * layout.setExtras([12, 40], [180, 240]); // details behind rows 12 and 40
20
+ * const top = layout.getTop(41); // top of row 41 below both details
21
+ * const row = layout.findIndexAt(5000); // the row whose slot contains y = 5000
22
+ *
23
+ * @category Internals
24
+ * @since 1.1.97
25
+ */
26
+ export declare class RowLayout {
27
+ private rowCount;
28
+ private constantHeight?;
29
+ private heightOf;
30
+ private prefix?;
31
+ private extraIndices;
32
+ private extraHeights;
33
+ private extraPrefix;
34
+ /**
35
+ * Sets the rows. Prefix sums of variable heights are built lazily on the first access.
36
+ * Extras behind rows that no longer exist are dropped.
37
+ *
38
+ * @param {number} rowCount - number of rows
39
+ * @param {(rowIndex: number) => number} heightOf - height of a row (called only for variable heights)
40
+ * @param {number} [constantHeight] - the height of every row, if they are all equal (`AreaModelIf.getConstantRowHeight`)
41
+ */
42
+ setRows(rowCount: number, heightOf: (rowIndex: number) => number, constantHeight?: number): void;
43
+ /**
44
+ * Sets the extra heights (e.g. detail rows) behind rows. Unsorted input is fine; heights of the same row add up;
45
+ * indices outside the rows and heights ≤ 0 are ignored. Replaces all previous extras.
46
+ *
47
+ * @param {ArrayLike<number>} rowIndices - the rows with an extra height below them
48
+ * @param {ArrayLike<number>} heights - the extra heights (same order as `rowIndices`)
49
+ */
50
+ setExtras(rowIndices: ArrayLike<number>, heights: ArrayLike<number>): void;
51
+ /**
52
+ * Changes the extra height behind one row that already has one (see {@link setExtras}).
53
+ *
54
+ * @param {number} rowIndex - the row
55
+ * @param {number} height - the new extra height (> 0)
56
+ * @returns {boolean} false if the row has no extra or the height did not change
57
+ */
58
+ setExtraHeight(rowIndex: number, height: number): boolean;
59
+ /**
60
+ * Sets, changes or (height ≤ 0) removes the extra height behind one row – O(k), the other extras stay.
61
+ *
62
+ * @param {number} rowIndex - the row (ignored outside the rows)
63
+ * @param {number} height - the extra height, ≤ 0 removes it
64
+ * @returns {boolean} true if something changed
65
+ */
66
+ setExtra(rowIndex: number, height: number): boolean;
67
+ /** The number of rows. */
68
+ getRowCount(): number;
69
+ /** The number of rows with an extra height. */
70
+ getExtraCount(): number;
71
+ /** True after the prefix sums of variable heights were built (never for constant heights). */
72
+ hasPrefixSums(): boolean;
73
+ /**
74
+ * The height of a row (without its extra).
75
+ *
76
+ * @param {number} rowIndex - the row
77
+ * @returns {number} the height in px
78
+ */
79
+ getRowHeight(rowIndex: number): number;
80
+ /**
81
+ * The extra height below a row (0 = none).
82
+ *
83
+ * @param {number} rowIndex - the row
84
+ * @returns {number} the extra height in px
85
+ */
86
+ getExtraHeight(rowIndex: number): number;
87
+ /**
88
+ * The sum of the extra heights of the rows above a row.
89
+ *
90
+ * @param {number} rowIndex - the row (`rowCount` = all extras)
91
+ * @returns {number} px
92
+ */
93
+ getExtraBefore(rowIndex: number): number;
94
+ /** The sum of all extra heights. */
95
+ getExtraTotal(): number;
96
+ /**
97
+ * The top of a row: heights and extras of all rows above it. `getTop(rowCount)` = total height. The index is
98
+ * clamped to `0 … rowCount`.
99
+ *
100
+ * @param {number} rowIndex - the row
101
+ * @returns {number} px from the top of the first row
102
+ */
103
+ getTop(rowIndex: number): number;
104
+ /** The height of all rows and extras. */
105
+ getTotalHeight(): number;
106
+ /**
107
+ * The row whose slot (row + extra) contains `y`; rows of height 0 are skipped. Above the first row: 0, below the
108
+ * last: the last row, without rows: -1. O(1) for constant heights without extras, otherwise O(log n).
109
+ *
110
+ * @param {number} y - px from the top of the first row
111
+ * @returns {number} the row index
112
+ */
113
+ findIndexAt(y: number): number;
114
+ private getBaseTop;
115
+ private ensurePrefix;
116
+ private rebuildExtraPrefix;
117
+ /** The number of extras behind rows with an index < rowIndex (lower bound). */
118
+ private countExtrasBelow;
119
+ private findExtra;
120
+ }
@@ -21,11 +21,12 @@ export type TableStateRowId = string | number;
21
21
  * | `selection` | selected rows, columns, cells or ranges | `autoRestoreSelectedState` |
22
22
  * | `headerGroups` | collapsed header groups | `autoRestoreHeaderGroupState` |
23
23
  * | `custom` | data of your application ({@link TableApi.setCustomState}) | `autoRestoreCustomState` |
24
+ * | `details` | expanded detail rows of master/detail (since 1.1.97) | `autoRestoreDetailState` |
24
25
  *
25
26
  * @category State
26
27
  * @since 1.1.92
27
28
  */
28
- export type TableStatePart = "columns" | "sort" | "scroll" | "tree" | "checked" | "selection" | "headerGroups" | "custom";
29
+ export type TableStatePart = "columns" | "sort" | "scroll" | "tree" | "checked" | "selection" | "headerGroups" | "custom" | "details";
29
30
  /**
30
31
  * The state of a table as plain JSON: column layout, sorting, scroll position, tree, checked and selected rows,
31
32
  * header groups and data of your own. Get it with {@link TableApi.getState}, apply it with
@@ -72,6 +73,8 @@ export interface TableStateIf {
72
73
  tree?: TableTreeStateIf;
73
74
  /** The ids of the checked rows. */
74
75
  checkedRowIds?: TableStateRowId[];
76
+ /** The ids of the master rows with an expanded detail row (master/detail). @since 1.1.97 */
77
+ detailRowIds?: TableStateRowId[];
75
78
  /** The selection. */
76
79
  selection?: TableSelectionStateIf;
77
80
  /** The state of the header groups with a toggle. */
@@ -115,6 +115,10 @@ export declare class TableStateService {
115
115
  private computeScroll;
116
116
  private computeTree;
117
117
  private computeChecked;
118
+ /** The ids of the expanded master rows (master/detail, since 1.1.97); `undefined` without master/detail. */
119
+ private computeDetails;
120
+ /** Expands exactly the master rows with these ids (master/detail, since 1.1.97). */
121
+ private applyDetails;
118
122
  private computeSelection;
119
123
  /**
120
124
  * A cell range → ids and properties. A range to the last row / column (`MAX_SAFE_INTEGER`) has no `toRowId` /
@@ -1538,6 +1538,107 @@ export declare class TableApi {
1538
1538
  * @since 1.1.96
1539
1539
  */
1540
1540
  cancelFill(): void;
1541
+ /**
1542
+ * Expands or collapses the detail row of a master row (master/detail, {@link TableOptionsIf.masterDetail}). The
1543
+ * content is created when the detail row becomes visible; collapsing destroys it. Reports
1544
+ * {@link EventListenerIf.onDetailExpandedChanged} (source `"api"`).
1545
+ *
1546
+ * @param {number | object} rowOrIndex - display index or row object
1547
+ * @param {boolean} expanded - `true` = expand, `false` = collapse
1548
+ * @returns {boolean} false if nothing changed (no master row, already in that state, no master/detail)
1549
+ *
1550
+ * @example
1551
+ * tableApi.setDetailExpanded(0, true); // the first shown row
1552
+ * tableApi.setDetailExpanded(orders[42], true); // a row object (also if it is filtered out)
1553
+ *
1554
+ * @since 1.1.97
1555
+ */
1556
+ setDetailExpanded(rowOrIndex: number | object, expanded: boolean): boolean;
1557
+ /**
1558
+ * Expands a collapsed or collapses an expanded detail row (master/detail).
1559
+ *
1560
+ * @param {number | object} rowOrIndex - display index or row object
1561
+ * @returns {boolean} false if it is no master row (or no master/detail)
1562
+ *
1563
+ * @example
1564
+ * tableApi.toggleDetail(3); // e.g. from a button outside the table
1565
+ *
1566
+ * @since 1.1.97
1567
+ */
1568
+ toggleDetail(rowOrIndex: number | object): boolean;
1569
+ /**
1570
+ * True if the detail row of a master row is expanded (master/detail).
1571
+ *
1572
+ * @param {number | object} rowOrIndex - display index or row object
1573
+ * @returns {boolean} whether it is expanded
1574
+ *
1575
+ * @example
1576
+ * const open = tableApi.isDetailExpanded(3);
1577
+ *
1578
+ * @since 1.1.97
1579
+ */
1580
+ isDetailExpanded(rowOrIndex: number | object): boolean;
1581
+ /**
1582
+ * The master rows whose detail rows are expanded, in the order they were expanded (also rows that are filtered out).
1583
+ *
1584
+ * @returns {T[]} the row objects
1585
+ *
1586
+ * @example
1587
+ * const openOrders = tableApi.getExpandedDetailRows<Order>().map(order => order.id);
1588
+ *
1589
+ * @since 1.1.97
1590
+ */
1591
+ getExpandedDetailRows<T = any>(): T[];
1592
+ /**
1593
+ * Expands the detail rows of all shown master rows. The contents are created only when they scroll into view, so this
1594
+ * is cheap also for many rows. No event per row.
1595
+ *
1596
+ * @returns {number} the number of rows that were expanded
1597
+ *
1598
+ * @example
1599
+ * document.querySelector("#expand-all")!.addEventListener("click", () => tableApi.expandAllDetails());
1600
+ *
1601
+ * @since 1.1.97
1602
+ */
1603
+ expandAllDetails(): number;
1604
+ /**
1605
+ * Collapses all detail rows (their contents are destroyed). No event per row.
1606
+ *
1607
+ * @returns {number} the number of rows that were collapsed
1608
+ *
1609
+ * @example
1610
+ * tableApi.collapseAllDetails();
1611
+ *
1612
+ * @since 1.1.97
1613
+ */
1614
+ collapseAllDetails(): number;
1615
+ /**
1616
+ * Destroys the content of an expanded detail row and creates it again (`context.recreated` is `true`) – e.g. after
1617
+ * the data it shows changed.
1618
+ *
1619
+ * @param {number | object} rowOrIndex - display index or row object
1620
+ * @returns {boolean} false if the row is not expanded
1621
+ *
1622
+ * @example
1623
+ * order.items.push(newItem);
1624
+ * tableApi.refreshDetail(order);
1625
+ *
1626
+ * @since 1.1.97
1627
+ */
1628
+ refreshDetail(rowOrIndex: number | object): boolean;
1629
+ /**
1630
+ * The container of a detail row (the element your `detailRenderer` renders into) – only while its content exists
1631
+ * (it is created when the detail row becomes visible).
1632
+ *
1633
+ * @param {number | object} rowOrIndex - display index or row object
1634
+ * @returns {HTMLDivElement | undefined} the container
1635
+ *
1636
+ * @example
1637
+ * const notes = tableApi.getDetailElement(order)?.querySelector("textarea");
1638
+ *
1639
+ * @since 1.1.97
1640
+ */
1641
+ getDetailElement(rowOrIndex: number | object): HTMLDivElement | undefined;
1541
1642
  /**
1542
1643
  * Starts a batch edit: from now on edited and filled values are **collected** – shown in the cells (class
1543
1644
  * `ge-table-cell-pending`), but not written into your rows – until {@link commitBatchEdit} or
@@ -36,6 +36,7 @@ import { ResizeHandler } from './resize-handler.js';
36
36
  import { ExcelServiceIf } from './service/excel-service.if.js';
37
37
  import { ColumnDefIf } from './data/tablemodel/column/column-def.if.js';
38
38
  import { LiveAnnouncer } from './aria/live-announcer.js';
39
+ import { GeDetailEventSource } from './data/common/event/ge-detail-event.js';
39
40
  import { FillHandleOptionsIf } from './fill/fill-handle-options.if.js';
40
41
  import { CellChangeIf, CellChangeSource } from './batch-edit/cell-change.if.js';
41
42
  import { CellValueChangeIf } from './batch-edit/cell-value-change.if.js';
@@ -377,6 +378,69 @@ export declare class TableScope extends RenderScope implements OnActionTriggered
377
378
  * new TableScope(hostDiv, newTableModel, new SimpleDomService(), new TableOptions(), new EventAdapter()).firstInit();
378
379
  */
379
380
  destroy(): void;
381
+ /**
382
+ * True if a DOM event comes from a detail row (master/detail) – e.g. from a nested table. The mouse, keyboard and
383
+ * pointer handlers of this table ignore such events.
384
+ *
385
+ * @param {Event} evt - an event while it is dispatched
386
+ * @returns {boolean} whether it comes from the detail layer
387
+ * @since 1.1.97
388
+ */
389
+ isEventInDetail(evt: Event): boolean;
390
+ /**
391
+ * Expands or collapses the detail row of a master row (see {@link TableApi.setDetailExpanded}).
392
+ *
393
+ * @param {number | object} rowOrIndex - display index or row object
394
+ * @param {boolean} expanded - the new state
395
+ * @param {GeDetailEventSource} [source] - what changed it
396
+ * @returns {boolean} false if nothing changed
397
+ * @since 1.1.97
398
+ */
399
+ setDetailExpanded(rowOrIndex: unknown, expanded: boolean, source?: GeDetailEventSource): boolean;
400
+ /**
401
+ * Toggles the detail row of a master row (see {@link TableApi.toggleDetail}).
402
+ *
403
+ * @param {number | object} rowOrIndex - display index or row object
404
+ * @param {GeDetailEventSource} [source] - what changed it
405
+ * @returns {boolean} false if it is no master row
406
+ * @since 1.1.97
407
+ */
408
+ toggleDetail(rowOrIndex: unknown, source?: GeDetailEventSource): boolean;
409
+ /**
410
+ * Called by {@link MasterDetailService} after detail rows were expanded or collapsed: new row positions and scroll
411
+ * height, repaint, event, announcement, table state.
412
+ *
413
+ * @param {unknown} row - the master row (`undefined` for several rows)
414
+ * @param {number} rowIndex - its display index (-1 for several rows or a row that is not shown)
415
+ * @param {boolean} expanded - the new state
416
+ * @param {GeDetailEventSource} source - what changed it
417
+ * @param {boolean} [emit] - false: no event (several rows at once)
418
+ * @param {boolean} [layoutUpdated] - true: the row layout was already updated for this one row
419
+ * @since 1.1.97
420
+ */
421
+ onDetailToggled(row: unknown, rowIndex: number, expanded: boolean, source: GeDetailEventSource, emit?: boolean, layoutUpdated?: boolean): void;
422
+ /**
423
+ * Called by {@link MasterDetailService} after a detail row with `detailHeight: "auto"` got a new height: new scroll
424
+ * height and repaint (the rows below move).
425
+ *
426
+ * @since 1.1.97
427
+ */
428
+ onDetailHeightChanged(): void;
429
+ /**
430
+ * The focus came back from a detail row (Shift+Tab): its master row gets the focus (same column).
431
+ *
432
+ * @param {number} rowIndex - display index of the master row
433
+ * @since 1.1.97
434
+ */
435
+ /**
436
+ * A detail row that holds the keyboard focus goes away (collapsed, destroyed, scrolled out of view): the host takes
437
+ * the focus and the master row becomes the focused row – without a repaint (the caller repaints).
438
+ *
439
+ * @param {number} rowIndex - display index of the master row (-1: only the host)
440
+ * @since 1.1.97
441
+ */
442
+ takeFocusFromDetail(rowIndex: number): void;
443
+ focusDetailMaster(rowIndex: number): void;
380
444
  /**
381
445
  * Returns `true` once {@link destroy} has been called.
382
446
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guiexpert/table",
3
- "version": "1.1.96",
3
+ "version": "1.1.97",
4
4
  "license": "MIT",
5
5
  "author": "Dr. Marc Kronberg <info@gui.expert> (https://gui.expert)",
6
6
  "repository": {