@guiexpert/table 1.1.105 → 1.1.107

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.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * macOS or not – the one place where the table asks for the platform (⌘ shortcuts, ⌥+letter types text,
3
+ * `aria-keyshortcuts`). Reads `navigator.platform` ("MacIntel", also iPadOS Safari with a keyboard); without a
4
+ * `navigator` (SSR, Node) `false`. Tests switch the platform here with `vi.spyOn(navigator, "platform", "get")`.
5
+ *
6
+ * `navigator.userAgentData.platform` is not used on purpose: only Chromium has it, and the result would be the same.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare function isMacPlatform(): boolean;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A scroll position – the argument of {@link TableApi.repaintHard} (since 1.1.107): the rows are counted again and the
3
+ * table shows this position in the same render pass. A missing axis keeps its position.
4
+ *
5
+ * - `px`: the horizontal scroll position from the start (0 = the first center column at the start; right-to-left
6
+ * measured from the right edge, see {@link TableOptionsIf.direction}) – the same as the first argument of
7
+ * {@link TableApi.scrollToPixel} (scroll bar pixels, not a column offset: for a column use `scrollToIndex`).
8
+ * - `py`: a **content** position from the top of the first body row (with the heights of open detail rows above) – the same as the second
9
+ * argument of {@link TableApi.scrollToPixel}, also exact with a capped scroll height (more than
10
+ * {@link EleScope.maxScrollHeightInPx} px of rows).
11
+ *
12
+ * @example
13
+ * // live tail: rows were appended – count them and show row 500,000 (20 px rows) at the top
14
+ * tableApi.repaintHard({ py: 500_000 * 20 });
15
+ *
16
+ * @example
17
+ * // only horizontally: back to the first column, the vertical position stays
18
+ * tableApi.repaintHard({ px: 0 });
19
+ *
20
+ * @category Table & API
21
+ * @since 1.1.107
22
+ */
23
+ export interface ScrollPositionIf {
24
+ /** Horizontal scroll position in px from the start (like `scrollToPixel`). Missing: the horizontal position stays. */
25
+ px?: number;
26
+ /** Vertical content position in px from the top of the first body row. Missing: the vertical position stays. */
27
+ py?: number;
28
+ }
@@ -118,15 +118,25 @@ export interface TableOptionsIf {
118
118
  */
119
119
  horizontalBorderVisible: boolean;
120
120
  /**
121
- * The value of the css `overflowX` determines the behavior of horizontal overflow for the table body (scroll port).
121
+ * The css `overflow-x` of the scroll viewport (`.ge-table-scroll-viewport`): `"auto"` (default) shows the horizontal
122
+ * scroll bar only when the columns are wider than the table, `"scroll"` always.
122
123
  *
123
- * @typedef {'auto' | 'scroll'} OverflowX
124
+ * To hide the scroll bars completely use CSS – there is no `"hidden"` (wheel, touch, keyboard and the API still
125
+ * scroll, the table lays out without the bars):
126
+ * ```css
127
+ * .my-table .ge-table-scroll-viewport { scrollbar-width: none; }
128
+ * ```
124
129
  */
125
130
  overflowX: "auto" | "scroll";
126
131
  /**
127
- * The value of the css `overflowX` determines the behavior of vertical overflow for the table body (scroll port).
132
+ * The css `overflow-y` of the scroll viewport (`.ge-table-scroll-viewport`): `"auto"` (default) shows the vertical
133
+ * scroll bar only when the rows are higher than the body, `"scroll"` always.
128
134
  *
129
- * @typedef {'auto' | 'scroll'} OverflowX
135
+ * To hide the scroll bars completely use CSS – there is no `"hidden"` (wheel, touch, keyboard and the API still
136
+ * scroll, the table lays out without the bars):
137
+ * ```css
138
+ * .my-table .ge-table-scroll-viewport { scrollbar-width: none; }
139
+ * ```
130
140
  */
131
141
  overflowY: "auto" | "scroll";
132
142
  /**
@@ -19,6 +19,10 @@ export declare abstract class AbstractAreaModel<T> implements AreaModelIf, HasDe
19
19
  private columnValuesInstance?;
20
20
  private calcTextRenderer?;
21
21
  protected cellRenderers: (CellRendererIf | undefined)[];
22
+ /**
23
+ * @deprecated since 1.1.107 – filled by `init()` for `defaultRowHeight -1`, but no longer read:
24
+ * {@link getYPosByRowIndex} sums `getRowHeight()` (the cache missed rows added after `init()`).
25
+ */
22
26
  protected yPositions: number[];
23
27
  /** Cell formulas of the table (since 1.1.99, set by the table for a flat body). @internal */
24
28
  sheetCells?: SheetCellsIf;
@@ -41,6 +45,25 @@ export declare abstract class AbstractAreaModel<T> implements AreaModelIf, HasDe
41
45
  setRowChecked(rowIndex: number, checked: boolean): void;
42
46
  getMaxColspan(): number;
43
47
  getMaxRowspan(): number;
48
+ /**
49
+ * The top of a row in px from the first row of this area.
50
+ *
51
+ * With a constant row height (`getConstantRowHeight()` of the subclass – the built-in models return their
52
+ * `defaultRowHeight` as long as `getRowHeight()` is not overridden) it is `rowIndex × height` (O(1)); otherwise the
53
+ * sum of `getRowHeight()` of all rows above (O(n), always up to date). Before 1.1.107 it was
54
+ * `defaultRowHeight × rowIndex` also when a subclass overrode `getRowHeight()`, and for `defaultRowHeight -1` a cache
55
+ * from `init()` that missed rows added later. The table itself positions the body rows with its own row layout.
56
+ *
57
+ * @param {number} rowIndex - the index of the row
58
+ * @returns {number} px from the top of the first row
59
+ *
60
+ * @example
61
+ * // a subclass with taller even rows: the positions follow the overridden getRowHeight()
62
+ * class Striped extends AreaModelObjectArray<any> {
63
+ * override getRowHeight(rowIndex: number): number { return rowIndex % 2 ? 20 : 40; }
64
+ * }
65
+ * const y = new Striped("body", rows, 34, columnDefs).getYPosByRowIndex(3); // 40 + 20 + 40 = 100
66
+ */
44
67
  getYPosByRowIndex(rowIndex: number): number;
45
68
  init(): void;
46
69
  abstract externalFilterChanged<T>(predictFn: FilterFunction<T>): void;
@@ -47,7 +47,7 @@ export declare class AreaModelCellGroups implements AreaModelIf {
47
47
  getTooltipAt(_rowIndex: number, _columnIndex: number): any;
48
48
  getValueAt(rowIndex: number, columnIndex: number): any;
49
49
  getTextValueAt(rowIndex: number, columnIndex: number): string;
50
- getYPosByRowIndex(_rowIndex: number): number;
50
+ getYPosByRowIndex(rowIndex: number): number;
51
51
  getRowHeight(_rowIndex: number): number;
52
52
  isEditable(_rowIndex: number, _columnIndex: number): boolean;
53
53
  isFilterable(): boolean;
@@ -663,6 +663,10 @@ export interface AreaModelIf {
663
663
  * Returns the height of the row at the specified index, in pixels.
664
664
  * This method allows for variable row heights in the table.
665
665
  *
666
+ * The table draws the rows with these heights, and since 1.1.107 the height of the area (scroll height of the body,
667
+ * height of header and footer) is their sum as well – {@link getConstantRowHeight} × row count if given.
668
+ * `defaultRowHeights` of the table model only counts if every row height is 0.
669
+ *
666
670
  * @param {number} rowIndex - The index of the row
667
671
  * @returns {number} The height of the row in pixels
668
672
  *
@@ -699,6 +703,8 @@ export interface AreaModelIf {
699
703
  * of all rows above it – with millions of rows this keeps scrolling at the end of the table as fast as at
700
704
  * the beginning. Only return a number if `getRowHeight(i)` returns exactly this value for every row.
701
705
  * If the method is missing or returns `undefined`, the renderer falls back to iterating the row heights.
706
+ * The height of the area (the scroll height of the body) is then this value × row count – O(1) – and wins over
707
+ * `defaultRowHeights` of the table model (since 1.1.107; before, `defaultRowHeights` came first).
702
708
  *
703
709
  * @returns {number | undefined} The constant row height in pixels, or `undefined` if row heights may vary.
704
710
  *
@@ -7,7 +7,22 @@ import { SortItem } from '../../common/sort-item.js';
7
7
  import { AreaIdent } from '../area-ident.type.js';
8
8
  import { SorterService } from '../../../service/sorter.service.js';
9
9
 
10
- /** @category Area models */
10
+ /**
11
+ * Body of a cover grid: `filteredItems[row * columns + column]` is the item of a tile (`getValueAt`), `columns` =
12
+ * `floor(width / coverWidth)`. `items` = all items, `filteredItems` = shown items (filter and `sort()` work on it).
13
+ * Change `coverWidth`/`coverHeight` or the items and call `tableApi.repaintHard()`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * declare const grid: CoverGridTableModel<string>;
18
+ * const tiles = grid.bodyModel;
19
+ * tiles.items = ["new-1.jpg", "new-2.jpg"];
20
+ * tiles.filteredItems = tiles.items; // or tableApi.externalFilterChanged() with a filter
21
+ * tableApi.repaintHard();
22
+ * ```
23
+ *
24
+ * @category Area models
25
+ */
11
26
  export declare class CoverGridBodyModel<T> implements AreaModelIf {
12
27
  items: T[];
13
28
  cellRenderer: CellRendererIf;
@@ -9,7 +9,20 @@ import { ColumnDefIf } from './column/column-def.if.js';
9
9
  import { Padding } from './padding.js';
10
10
  import { SelectionModelIf } from '../../selection/selection-model.if.js';
11
11
 
12
- /** @category Table model */
12
+ /**
13
+ * Table model of a cover grid (created by {@link CoverGridFactory.createCoverGridModel}): items as tiles, the number of
14
+ * columns follows the width (`floor(width / coverWidth)`), header and footer are hidden. The data and the tile size
15
+ * live in {@link CoverGridBodyModel} (`tableModel.bodyModel`).
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * declare const grid: CoverGridTableModel<{ title: string }>;
20
+ * const columns = grid.getColumnCount();
21
+ * tableApi.scrollToIndex(0, Math.floor(500 / columns)); // the row of item 500
22
+ * ```
23
+ *
24
+ * @category Table model
25
+ */
13
26
  export declare class CoverGridTableModel<T> implements TableModelIf {
14
27
  bodyModel: CoverGridBodyModel<T>;
15
28
  paddingTop: number;
@@ -39,6 +52,7 @@ export declare class CoverGridTableModel<T> implements TableModelIf {
39
52
  setColumnWidth(_columnIndex: number, width: number): void;
40
53
  recalcSize(_clientWidth: number): void;
41
54
  recalcPadding(): void;
55
+ /** No-op: the height follows from the item count, the column count and `coverHeight` – `repaintHard()` is enough. */
42
56
  recalcHeightAndPadding(): void;
43
57
  getPadding(): Padding;
44
58
  getContentHeightInPixel(): number;
@@ -157,10 +157,24 @@ export declare class TableModel implements TableModelIf {
157
157
  */
158
158
  getModel(rowAreaIdent: AreaIdent): AreaModelIf;
159
159
  /**
160
- * Calculates the height of the area identified by the given area identifier.
160
+ * Calculates the height of the area identified by the given area identifier – the height of the rows the table
161
+ * draws (since 1.1.107 the same source as the rendering):
162
+ *
163
+ * 1. `getConstantRowHeight()` of the area model (> 0): row count × that height – O(1), also for millions of rows,
164
+ * 2. otherwise the sum of `getRowHeight(i)` of all rows (variable heights; O(n), like the row layout of the body),
165
+ * 3. only if the model has no row heights of its own (every `getRowHeight(i)` is 0): row count ×
166
+ * `defaultRowHeights[area]`.
167
+ *
168
+ * Before 1.1.107 `defaultRowHeights[area]` came first: an area model with other heights got a wrong scroll height
169
+ * (a scroll bar without overflow, a wrong thumb). The built-in area models take their row height from
170
+ * `defaultRowHeights` anyway, so nothing changes for them.
161
171
  *
162
172
  * @param {AreaIdent} areaIdent - The identifier of the area.
163
- * @return {number} - The height of the area.
173
+ * @return {number} - The height of the area in px.
174
+ *
175
+ * @example
176
+ * // an own body with 20 px rows: 3000 rows are 60,000 px high – also if defaultRowHeights.body is 34
177
+ * const height = (tableModel as TableModel).getAreaHeight("body");
164
178
  */
165
179
  getAreaHeight(areaIdent: AreaIdent): number;
166
180
  /**
@@ -140,11 +140,20 @@ export declare class EleScope {
140
140
  /** The element that holds the rows of an area (`DivScope.child`). */
141
141
  protected getAreaChild(area: AreaIdent, side: SideIdent): HTMLDivElement;
142
142
  /**
143
- * Adjusts the containers and rows of the table based on the current state.
143
+ * Adjusts the containers and rows of the table based on the current state: lays out the areas
144
+ * ({@link layoutContainers}) and renders the visible rows ({@link adjustAfterScrolling}).
144
145
  *
145
146
  * @return {void}
146
147
  */
147
148
  adjustContainersAndRows(): void;
149
+ /**
150
+ * Lays out the scroll viewport, the nine areas, the separator borders, the overlay and the detail layer for the
151
+ * current size of the host and the current padding – without rendering the rows (that is
152
+ * {@link adjustAfterScrolling}). `repaintHard()` uses it to set a new scroll position before its one render pass.
153
+ *
154
+ * @since 1.1.107
155
+ */
156
+ protected layoutContainers(): void;
148
157
  /**
149
158
  * The height the table lays out header, body and footer in: the host height minus a horizontal scroll bar that
150
159
  * takes space (see {@link horizontalScrollBarHeight}).
@@ -2,8 +2,73 @@ import { CoverGridTableModel } from '../data/tablemodel/cover-grid-table-model.j
2
2
  import { CellRendererIf } from '../renderer/cell-render.if.js';
3
3
  import { TableOptionsIf } from '../data/options/table-options.if.js';
4
4
 
5
- /** @category Table model */
5
+ /**
6
+ * Creates cover grids: a flat array of items shown as tiles (book covers, photos, products) – as many per row as fit
7
+ * into the width of the table (`floor(width / coverWidth)`), without header and footer. Only the visible rows are
8
+ * rendered, like in every GuiExpert Table; filter (`externalFilterFunction`), `sort()` and the scroll position work as
9
+ * usual. Guide: instructions.md 5.10, showcase https://gui.expert/showcase/covergrid/run.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * interface Book { title: string; }
14
+ * declare const books: Book[];
15
+ *
16
+ * const renderer: CellRendererIf = {
17
+ * render(cellDiv: HTMLDivElement, _r: number, _c: number, _a: AreaIdent, _m: AreaModelIf, book: any) {
18
+ * if (book) cellDiv.textContent = (book as Book).title; // the item of this tile (undefined after the last one)
19
+ * return undefined;
20
+ * }
21
+ * };
22
+ * const gridModel = CoverGridFactory.createCoverGridModel(books, renderer, 160, 240);
23
+ * const gridOptions = CoverGridFactory.createCoverGrisDefaultOptions(240, () => "books");
24
+ * const gridApi = new TableScope(host, gridModel, new SimpleDomService(), gridOptions, new EventAdapter())
25
+ * .firstInit()
26
+ * .getApi();
27
+ * ```
28
+ *
29
+ * @category Table model
30
+ */
6
31
  export declare class CoverGridFactory {
32
+ /**
33
+ * Creates the model of a cover grid.
34
+ *
35
+ * @param items - the items, one per tile (the renderer gets the item as `cellValue`)
36
+ * @param cellRenderer - draws one tile into the cell `<div>`
37
+ * @param coverWidth - width of a tile in px (default 200) – change it later via `bodyModel.coverWidth` + `repaintHard()`
38
+ * @param coverHeight - height of a tile in px (default 300)
39
+ * @param width - initial width of the grid; the table sets the width of its host on start and after a resize
40
+ * @param firstRowPaddingTop - extra height of the first row in px (room above the first tiles, e.g. for a toolbar)
41
+ * @returns the table model; `tableModel.bodyModel` holds `items`, `filteredItems`, `coverWidth`, `coverHeight`
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * declare const renderer: CellRendererIf;
46
+ * const gridModel = CoverGridFactory.createCoverGridModel(["a.jpg", "b.jpg", "c.jpg"], renderer, 120, 180);
47
+ * // bigger tiles – no new model:
48
+ * gridModel.bodyModel.coverWidth = 200;
49
+ * gridModel.bodyModel.coverHeight = 300;
50
+ * tableApi.repaintHard();
51
+ * ```
52
+ */
7
53
  static createCoverGridModel<T>(items: T[], cellRenderer: CellRendererIf, coverWidth?: number, coverHeight?: number, width?: number, firstRowPaddingTop?: number): CoverGridTableModel<T>;
54
+ /**
55
+ * Options for a cover grid: no borders and separators, no row or column hover, header and footer height 0, and the
56
+ * scroll position restored under the key of `getStorageKeyFn` (`localStorage`). (The name has a typo – kept for
57
+ * compatibility.)
58
+ *
59
+ * @param defaultRowHeight - the height of a tile (`coverHeight`)
60
+ * @param getStorageKeyFn - storage key of the scroll position (default `"CoverGrid"`)
61
+ * @returns new options – add your own fields with a spread
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * let photoFilter = "";
66
+ * const gridOptions: TableOptionsIf = {
67
+ * ...CoverGridFactory.createCoverGrisDefaultOptions(240, () => "photos"),
68
+ * externalFilterFunction: (photo: { name: string }) => photo.name.includes(photoFilter),
69
+ * shortcutActionsDisabled: true
70
+ * };
71
+ * ```
72
+ */
8
73
  static createCoverGrisDefaultOptions(defaultRowHeight: number, getStorageKeyFn?: () => string): TableOptionsIf;
9
74
  }
@@ -103,7 +103,12 @@ export interface CreateTableModelPara {
103
103
  * @since 1.1.99
104
104
  */
105
105
  columnLetters: boolean;
106
- /** Row heights of header, body and footer in px. Default: `tableOptions.defaultRowHeights`, otherwise 34 / 34 / 34. */
106
+ /**
107
+ * Row heights of header, body and footer in px. Default: `tableOptions.defaultRowHeights`, otherwise 34 / 34 / 34.
108
+ * The area models built by the factory get these heights. An own area model (`bodyAreaModel` …) keeps its own:
109
+ * since 1.1.107 the table measures every area with the heights of its model (`getConstantRowHeight()`, otherwise
110
+ * `getRowHeight(i)`) – `defaultRowHeights` only counts for a model whose `getRowHeight()` returns 0.
111
+ */
107
112
  defaultRowHeights: DefaultRowHeightsIf;
108
113
  /** The columns: property, header label, width, classes, renderers, sorting, editing. See {@link ColumnDef}. */
109
114
  columnDefs: ColumnDefIf[];
@@ -27,6 +27,10 @@ export declare class MouseHandler {
27
27
  private mouseDown;
28
28
  private repaintAfterMouseUp;
29
29
  private dragging;
30
+ /** Counts the drags: a frame callback of a drag that is over (a new one started) does nothing. */
31
+ private dragGeneration;
32
+ /** The end of the last drag, until its frame ran – a new mousedown handles it at once (see {@link flushDraggingEnd}). */
33
+ private pendingDraggingEnd?;
30
34
  constructor(tableScope: TableScope);
31
35
  /**
32
36
  * Handles the "contextmenu" event.
@@ -76,7 +80,8 @@ export declare class MouseHandler {
76
80
  private onMouseMove;
77
81
  private onMouseUp;
78
82
  private mouseDraggingOnFrame;
79
- private mouseDraggingEndOnFrame;
83
+ /** Handles the end of the last drag now (in its frame, or earlier when the next mousedown comes first). */
84
+ private flushDraggingEnd;
80
85
  private mouseMoveOnFrame;
81
86
  /** A mouse event on a blocking loading overlay ({@link TableOverlay.isEventBlocked}): the table ignores it. */
82
87
  private isBlocked;
@@ -30,6 +30,7 @@ import { AriaTextsIf } from './aria/aria-texts.if.js';
30
30
  import { RowLayout } from './row-layout/row-layout.js';
31
31
  import { MasterDetailService } from './master-detail/master-detail-service.js';
32
32
  import { TableSheet } from './sheet/table-sheet.js';
33
+ import { ScrollPositionIf } from './data/common/scroll-position.if.js';
33
34
 
34
35
  /** @category Internals */
35
36
  export declare class RenderScope extends EleScope {
@@ -67,6 +68,12 @@ export declare class RenderScope extends EleScope {
67
68
  scrollTop: number;
68
69
  contentTop: number;
69
70
  };
71
+ /**
72
+ * The scroll position (`scrollTop`, logical `scrollLeft`) of the last render pass – the `scroll` event of a position
73
+ * that is already rendered (`repaintHard({ py })`, `scrollToPixel()` render it at once) renders nothing.
74
+ */
75
+ protected renderedScrollTop: number;
76
+ protected renderedScrollLeft: number;
70
77
  /** Area heights read once at the start of adjustAfterScrolling() (before the DOM is modified). */
71
78
  protected areaHeightsOfRenderPass?: {
72
79
  [key in AreaIdent]?: number;
@@ -214,8 +221,10 @@ export declare class RenderScope extends EleScope {
214
221
  /** True if open detail rows take space between the body rows. */
215
222
  protected hasBodyRowExtras(): boolean;
216
223
  /**
217
- * The top of a body row in content pixels: {@link AreaModelIf.getYPosByRowIndex} plus the heights of the open detail
218
- * rows above it (master/detail).
224
+ * The top of a body row in content pixels – from the row layout of the table ({@link getBodyRowLayout}: the heights
225
+ * the rows are drawn with, plus the open detail rows above it). Since 1.1.107 always from the layout (before: without
226
+ * detail rows from {@link AreaModelIf.getYPosByRowIndex}, which an area model with its own `getRowHeight()` could
227
+ * get wrong – `scrollToIndex()` landed beside the row). O(1) for constant heights, otherwise after one O(n) build.
219
228
  *
220
229
  * @param {number} rowIndex - display index
221
230
  * @returns {number} px from the top of the first body row
@@ -296,13 +305,39 @@ export declare class RenderScope extends EleScope {
296
305
  */
297
306
  invalidateAggregates(change?: DerivedValuesChangeIf): void;
298
307
  /**
299
- * Repaints the UI by resetting the size of the wrapper div,
300
- * adjusting the containers and rows, and performing additional adjustments
301
- * after scrolling.
308
+ * Repaints the UI after the row count or the row heights changed: new heights and padding, new size of the scroll
309
+ * content, new layout of the areas – then **one** render pass (since 1.1.107; before: two).
302
310
  *
311
+ * @param {ScrollPositionIf} [scrollTo] - a content position to show in that render pass (like {@link scrollToPixel},
312
+ * exact also with a capped scroll height); a missing axis keeps its position
303
313
  * @return {void} This method does not return any value.
304
314
  */
305
- repaintHard(): void;
315
+ repaintHard(scrollTo?: ScrollPositionIf): void;
316
+ /**
317
+ * Sets the scroll position of the viewport to a content position **without rendering** (shared by
318
+ * `scrollToPixel()` and `repaintHard({ px, py })`). `py` is a content position: the scroll bar range differs from the
319
+ * content range – always with a capped scroll height, but also slightly without (the scroll content is 1 px higher
320
+ * than the rows, the scroll viewport includes the footer and the horizontal scroll bar). `scrollTop = py` would leave
321
+ * the target row a few pixels off, so the exact content position is remembered (`pendingContentScroll`) and used by
322
+ * the next render passes as long as the scroll bar is not moved.
323
+ *
324
+ * @param {number | undefined} px - from the start of the rows (logical, right-to-left from the right edge);
325
+ * `undefined` keeps the horizontal position
326
+ * @param {number | undefined} py - content px from the top of the first body row; `undefined` keeps the vertical
327
+ * position
328
+ * @returns {boolean} true if the exact content position was set (then render now – a scroll event may not fire);
329
+ * false if the browser renders it with its scroll event (nothing to scroll vertically, or only `px`)
330
+ * @since 1.1.107
331
+ */
332
+ protected setContentScrollPosition(px: number | undefined, py: number | undefined): boolean;
333
+ /**
334
+ * The `scroll` listener of the scroll viewport: renders the new position – nothing if the position is already
335
+ * rendered (the event of a `repaintHard({ py })`, `scrollToPixel()` or `ensureColumnIsVisible()` arrives after
336
+ * their own render pass).
337
+ *
338
+ * @since 1.1.107
339
+ */
340
+ onViewportScroll(): void;
306
341
  /**
307
342
  * Recalculates the column widths of the table (% and weight columns depend on the available width) and repaints.
308
343
  *
@@ -316,7 +351,7 @@ export declare class RenderScope extends EleScope {
316
351
  protected setParentWidth(widthInPixel: number): void;
317
352
  /** True if a column has a % or weight width (it depends on the width of the host). */
318
353
  protected hasRelativeColumnWidths(): boolean;
319
- adjustContainersAndRows(): void;
354
+ protected layoutContainers(): void;
320
355
  /**
321
356
  * % and weight columns share the width next to the vertical scroll bar. The bar appears (or disappears) only after
322
357
  * the rows are laid out – the first layout, rows added or removed, a filter: then the columns were computed for the
@@ -448,14 +483,14 @@ export declare class RenderScope extends EleScope {
448
483
  * scaled and scrollTop is no longer a content pixel position.
449
484
  */
450
485
  isScrollHeightCapped(): boolean;
486
+ /** The detail layer (master/detail) covers the visible body. @since 1.1.97 */
487
+ protected adjustDetailLayerBounds(left: number, top: number, width: number, height: number): void;
451
488
  /**
452
489
  * The model's content height plus the heights of open detail rows (master/detail).
453
490
  *
454
491
  * @returns {number} px
455
492
  * @since 1.1.97
456
493
  */
457
- /** The detail layer (master/detail) covers the visible body. @since 1.1.97 */
458
- protected adjustDetailLayerBounds(left: number, top: number, width: number, height: number): void;
459
494
  protected getBodyContentHeight(): number;
460
495
  /** Scrollable range of the content in content pixels (content height minus visible body height). */
461
496
  protected getContentScrollRangeY(): number;
@@ -601,7 +636,7 @@ export declare class RenderScope extends EleScope {
601
636
  */
602
637
  private rowNumberWidth?;
603
638
  /** true while the containers are adjusted to a new row number width (no second change in that nested pass). */
604
- private adjustingRowNumberWidth;
639
+ protected adjustingRowNumberWidth: boolean;
605
640
  /** Guards {@link adjustRelativeColumnWidthsToScrollBar} against re-entry. */
606
641
  private adjustingRelativeWidths;
607
642
  /** The width % and weight columns were last computed for (`TableModelIf.setParentWidth`), -1 = unknown. */
@@ -17,6 +17,8 @@ import { ValueLabelIf } from '../../data/common/value-label.if.js';
17
17
  export declare class SelectCellEditor implements CellEditorIf {
18
18
  protected readonly options: ValueLabelIf[];
19
19
  private select?;
20
+ /** The text of the cell when the editor opened: "nothing chosen" counts as unchanged (since 1.1.106). */
21
+ private startText;
20
22
  private readonly abort;
21
23
  constructor(options: ValueLabelIf[]);
22
24
  /**
@@ -28,6 +30,12 @@ export declare class SelectCellEditor implements CellEditorIf {
28
30
  static create(values: string[]): SelectCellEditor;
29
31
  open(container: HTMLDivElement, ctx: CellEditorContextIf): void;
30
32
  getValue(): unknown;
33
+ /**
34
+ * The chosen value as text; with nothing chosen (the cell's value is not in the list) the start text, so that
35
+ * Enter without a choice writes nothing (before 1.1.106 it emptied the cell).
36
+ *
37
+ * @returns {string} the text
38
+ */
31
39
  getText(): string;
32
40
  focus(): void;
33
41
  getFocusElement(): HTMLElement | undefined;
@@ -27,6 +27,7 @@ import { CellRange } from './data/common/cell-range.js';
27
27
  import { EventListenerIf } from './event-listener.if.js';
28
28
  import { ExcelExportOptionsIf } from './service/excel-export-options.if.js';
29
29
  import { SheetEngine } from './sheet/sheet-engine.js';
30
+ import { ScrollPositionIf } from './data/common/scroll-position.if.js';
30
31
 
31
32
  /**
32
33
  * The public API of one table – everything you do with a table after it has been created:
@@ -464,19 +465,24 @@ export declare class TableApi {
464
465
  */
465
466
  destroy(): void;
466
467
  /**
467
- * Performs a complete repaint of the table by triggering a hard repaint on the underlying table scope.
468
+ * Performs a complete repaint of the table: use it when the row count or the row heights changed (rows added or
469
+ * removed, a filter, a new model content, `ColumnDefIf.isVisible` changed yourself …). It
470
+ * - drops cached derived values (footer totals, calculated columns),
471
+ * - recalculates the heights and the padding of the table model and the size of the scroll content,
472
+ * - lays out header, body and footer again and
473
+ * - renders the visible rows – **once** (since 1.1.107; before: twice).
468
474
  *
469
- * A hard repaint is more thorough than a regular repaint and performs the following operations:
470
- * - Recalculates the height and padding of the table model
471
- * - Resets the size of the wrapper div elements
472
- * - Adjusts containers and rows positioning
473
- * - Makes additional adjustments after scrolling
475
+ * With `scrollTo` (since 1.1.107) it also sets the scroll position in that same render pass – exactly like
476
+ * {@link scrollToPixel}, also with a capped scroll height (more than {@link EleScope.maxScrollHeightInPx} px of rows).
477
+ * A live tail that appends rows and keeps a position needs one render per update instead of three
478
+ * (`repaintHard()` + `scrollToPixel()`). The `scroll` event of the browser for that position renders nothing.
479
+ * A missing axis keeps its position.
474
480
  *
475
- * Use this method when regular repaint isn't sufficient, such as when:
476
- * - Table dimensions have significantly changed
477
- * - The structure of data has been modified (rows/columns added or removed)
478
- * - The table layout needs complete recalculation
479
- * - After major DOM changes affecting the table's container
481
+ * For a repaint with the same row count (values changed) {@link repaint} is enough.
482
+ *
483
+ * @param {ScrollPositionIf} [scrollTo] - the position to show: `px` horizontal scroll position from the start, `py`
484
+ * content pixels from the top of the first body row (both like {@link scrollToPixel}); without it the position stays
485
+ * @returns {void} This method doesn't return a value
480
486
  *
481
487
  * @example
482
488
  * // Example 1: Basic usage
@@ -499,9 +505,20 @@ export declare class TableApi {
499
505
  * column.isVisible = FalseFn;
500
506
  * tableApi.repaintHard();
501
507
  *
502
- * @returns {void} This method doesn't return a value
508
+ * @example
509
+ * // Example 5: live tail – rows were appended to the area model, the first visible row stays at the top
510
+ * // (pixel-exact: keep your own py from the last update instead – e.g. from onScroll)
511
+ * const rowHeight = 20; // the row height of the area model
512
+ * const first = tableApi.getFirstVisibleRowIndex();
513
+ * rows.push(...newData); // the row count of the model grows
514
+ * tableApi.repaintHard({ py: first * rowHeight }); // one render pass, exact also for millions of rows
515
+ *
516
+ * @example
517
+ * // Example 6: follow the end ("tail -f") – show the last row after every update
518
+ * rows.push(...newData);
519
+ * tableApi.repaintHard({ py: Number.MAX_SAFE_INTEGER }); // clamped to the end of the content
503
520
  */
504
- repaintHard(): void;
521
+ repaintHard(scrollTo?: ScrollPositionIf): void;
505
522
  /**
506
523
  * Recalculates the column widths of the table.
507
524
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guiexpert/table",
3
- "version": "1.1.105",
3
+ "version": "1.1.107",
4
4
  "license": "MIT",
5
5
  "author": "Dr. Marc Kronberg <info@gui.expert> (https://gui.expert)",
6
6
  "repository": {