flexdesk 0.17.2 → 0.17.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +100 -63
  2. package/README.md +27 -4517
  3. package/css/base.css +18 -1
  4. package/css/flexdesk.css +18 -1
  5. package/dist/{chunk-4A7SJC3T.js → chunk-RKNFBX5K.js} +15 -4
  6. package/dist/{chunk-4A7SJC3T.js.map → chunk-RKNFBX5K.js.map} +2 -2
  7. package/dist/flexdesk.css +18 -1
  8. package/dist/tiles.js +1 -1
  9. package/dist/widgets.js +59 -26
  10. package/dist/widgets.js.map +2 -2
  11. package/docs/README.md +97 -0
  12. package/docs/best-practices.md +45 -0
  13. package/docs/components/charts.md +211 -0
  14. package/docs/components/command-palette.md +102 -0
  15. package/docs/components/conversation.md +72 -0
  16. package/docs/components/data-table-grid.md +328 -0
  17. package/docs/components/data-table.md +596 -0
  18. package/docs/components/editor.md +22 -0
  19. package/docs/components/flow-canvas-editor.md +482 -0
  20. package/docs/components/flow-editors.md +176 -0
  21. package/docs/components/flow-lane-editor.md +165 -0
  22. package/docs/components/flow-outline-editor.md +176 -0
  23. package/docs/components/forms-and-dialogs.md +294 -0
  24. package/docs/components/input-fields.md +72 -0
  25. package/docs/components/keymap.md +143 -0
  26. package/docs/components/layout-widgets.md +163 -0
  27. package/docs/components/markdown-and-outline.md +106 -0
  28. package/docs/components/menus.md +86 -0
  29. package/docs/components/notifications.md +194 -0
  30. package/docs/components/panels-and-desktops.md +127 -0
  31. package/docs/components/record-list-and-fact-table.md +128 -0
  32. package/docs/components/settings-view.md +112 -0
  33. package/docs/components/shell.md +189 -0
  34. package/docs/components/slide-out-panel.md +68 -0
  35. package/docs/components/tiles.md +210 -0
  36. package/docs/components/wizard.md +54 -0
  37. package/docs/concepts/building-and-packaging.md +33 -0
  38. package/docs/concepts/domain-neutrality.md +110 -0
  39. package/docs/concepts/host-port.md +137 -0
  40. package/docs/concepts/packages-and-entry-points.md +32 -0
  41. package/docs/concepts/peer-dependencies.md +42 -0
  42. package/docs/concepts/state-guard.md +58 -0
  43. package/docs/concepts/styling.md +47 -0
  44. package/docs/quickstart.md +179 -0
  45. package/package.json +2 -1
  46. package/src/ui/components/notification_history.js +17 -24
  47. package/src/ui/components/slide_out_panel.js +14 -3
  48. package/src/ui/notification_center.js +41 -3
  49. package/src/ui/utils/auto_scrollbars.js +16 -2
@@ -0,0 +1,596 @@
1
+ # DataTable
2
+
3
+ > **Package:** `@flexdesk/widgets` · **Continues in:** [cells, groups and crosstab](data-table-grid.md) · **Not a table of rows?** See [record lists and fact sheets](record-list-and-fact-table.md) · [All docs](../README.md)
4
+
5
+ `DataTable` is FlexDesk's table: sticky headers, sorting, filtering, paging, a CSV export, a row's detail folded under it, and rows fetched from a server. Every option on this page is off until you set it, and each section says what a table that sets none of them does.
6
+
7
+ **Where to look**
8
+
9
+ - An everyday list in a page (opening a row, marking the open one, dates and durations as values, `DataTable.mount`, `DataTable.withDefaults`): [DataTable options in 0.5.0: lists that need no help](#datatable-options-in-050-lists-that-need-no-help)
10
+ - Column widths and sorting: [Column sizing](#column-sizing), [Sorting a paged DataTable](#sorting-a-paged-datatable)
11
+ - Rows from a server (pages or one scrolling list, `dataSource`, row details, following, chip filters): [A server's rows in pages or as one list](#a-servers-rows-in-pages-or-as-one-list-paging) and [DataTable options in 0.7.0: details, rows from a server, and filters that are not text](#datatable-options-in-070-details-rows-from-a-server-and-filters-that-are-not-text)
12
+ - Two tables side by side: [Comparing two tables: `diff` and `createScrollLock`](#comparing-two-tables-diff-and-createscrolllock)
13
+ - Your own right-click items, a render hook, the keyboard, a remembered page: [Right-click items, a render hook, the keyboard, a remembered page](#datatable-your-own-right-click-items-a-render-hook-the-keyboard-a-remembered-page)
14
+ - Which columns show, and in what order: [Columns hidden until somebody asks for them](#datatable-columns-hidden-until-somebody-asks-for-them), [Columns in the order somebody puts them](#datatable-columns-in-the-order-somebody-puts-them-0170)
15
+ - CSV export and a select mode: [The CSV button](#datatable-the-csv-button-on-every-table-0141), [A select mode](#datatable-a-select-mode-0120)
16
+ - Editing cells, column groups, crosstabs: [DataTable as a grid](data-table-grid.md)
17
+
18
+ ## Column sizing
19
+
20
+ `fitColumns(columns, avail)` (from `@flexdesk/widgets`) turns per-column
21
+ measurements into widths. Pass each column's widest cell (`natural`), the width
22
+ most cells fit (`typical`), the header label (`header`) and any user-dragged
23
+ width (`pinned`). Short columns such as ids, dates and statuses keep their full
24
+ values, and free-text columns (`text`, or anything 240px and wider) take the
25
+ rest. When space runs short, short columns lose their rare outliers first,
26
+ then text shrinks to a floor. The widths are fractional and add up to `avail`
27
+ exactly whenever the floors fit. `DataTable` does not use it yet; an embedder
28
+ that measures its own columns can.
29
+
30
+ ## Sorting a paged DataTable
31
+
32
+ A client-paged `DataTable` (no `onPageChange`) keeps its page when it is
33
+ sorted, so a sort on page 3 shows rows 201–300 of the new order. Pass
34
+ `resetPageOnSort: true` (0.4.7) to start every new order at the first page, a
35
+ header click included, the way a new filter already does. It is off by default
36
+ because keeping the page is what every earlier release did. Server-side paging
37
+ is not affected: its page is your `offset`, which a sort never changes.
38
+
39
+ ## DataTable options in 0.5.0: lists that need no help
40
+
41
+ 0.5.0 adds options for the ordinary list in an application: one that sits in a
42
+ page, opens a row, marks the open one, and sorts dates as dates. Every option
43
+ is off until you set it. A table that sets none of them looks and behaves as it
44
+ did in 0.4, apart from the few changes listed at the end of this section. This
45
+ was measured in headless Edge: 64 cells in 7 default tables, an empty and a
46
+ loading one among them, computed the same under 0.4.7 and 0.5.0.
47
+
48
+ ```js
49
+ import { DataTable } from '@flexdesk/widgets';
50
+
51
+ // House rules once, then one call per list: build AND draw.
52
+ const ListTable = DataTable.withDefaults({
53
+ mode: 'compact', sortable: true, pagination: false,
54
+ fitContent: true, firstColumn: 'plain', autoDispose: true,
55
+ });
56
+
57
+ const table = ListTable.mount(host, {
58
+ headers: ['Name', 'Role', 'Last seen', 'Took'],
59
+ rows, // values, not formatted text
60
+ columnTypes: [null, null, 'datetime', 'duration'],
61
+ getRowKey: (row) => row[0],
62
+ activeRow: openName,
63
+ onRowActivate: (i, row) => open(row), // one click opens
64
+ rowClass: (row) => (row[1] === 'Suspended' ? 'is-off' : null),
65
+ emptyState: 'Nobody yet.',
66
+ });
67
+ ```
68
+
69
+ **Layout**
70
+
71
+ | Option | What it does |
72
+ |---|---|
73
+ | `fitContent: true` | Makes the table as tall as its rows. By default the wrapper is `height: 100%`. In a parent without a height (an ordinary block in a page that scrolls) that resolves to nothing, so the header draws and the rows do not. |
74
+ | `maxHeight: 240` | Implies `fitContent` and caps the table at this height. A number is px; a string is used as CSS. Past the cap, the body scrolls under its header. |
75
+ | `firstColumn: 'plain'` | Styles the first column like the others. By default it is styled as a time column: 120–150px wide, grey, weight 500. |
76
+ | `--twm-dt-cell-pad-block`, `--twm-dt-cell-pad-inline`, `--twm-dt-cell-line-height` | Three CSS tokens. Set them on `:root` or on any ancestor to give both densities one row height. They are unset by default, and then each density keeps its own 0.4 values. In Edge, with the tokens set to `2px`, `6px` and `1.35`, normal and compact rows both measured 21.19px. Without them, normal rows measured 27px. The normal density reads the line-height token on the table's `thead`/`tbody` and the cells inherit it, so your own line-height for the cells, the rows or the table still wins, from a cascade layer too. |
77
+
78
+ **Opening a row, and which one is open**
79
+
80
+ | Option | What it does |
81
+ |---|---|
82
+ | `onRowActivate(rowIdx, row, ev)` | Opens a row. It fires after the selection has been updated (`onRowClick` still fires before it, as in 0.4). It does not fire for a press on a control in the row (a button, link, field, label, `<summary>`, editable text, or `[data-twm-action]`), for a click that ends a drag selecting text in the table, for the second click of a double-click, or for the Ctrl click that takes a `selectable: 'single'` table's row off the selection. `ev.type` tells you which gesture fired it. If your `onSelectionChange` or `onRowClick` redraws the table (`setData`, `render`), the row that was pressed still opens; `row` is that row as it was drawn. |
83
+ | `activateOn` | Which gestures open a row: `'click'` (the default), `'dblclick'`, `'enter'`, or an array of them. A pick list, where one click selects, uses `['dblclick', 'enter']`. Enter opens the selected row (the one row selected, or the selection's anchor), which is the row a double-click on it would open. If nothing is selected, Enter opens the active row. |
84
+ | `clickable` | Adds `twm-dt--clickable` to the table, which gives rows a pointer cursor and a hover, readonly tables included. It defaults to on exactly when `onRowActivate` is set. |
85
+ | `selectable: 'single'` | Lets a click select exactly one row. Shift and Ctrl do not extend the selection, Ctrl+A selects nothing, and `setSelection` keeps the last index it is given. |
86
+ | `getRowKey(row, idx)`, `activeRow`, `setActiveRow(key)`, `getActiveRow()` | Mark the open row: `twm-dt-row--active` and `aria-current="true"`, drawn as an accent on the left edge. The mark is not the selection. It survives `setData`, sorting, filtering and paging, and a right-click does not move it. |
87
+
88
+ The landing helper `attachLandingTableBehavior` now uses the same rule as
89
+ `onRowActivate` to decide whether a click opens a row. That changes three
90
+ things, with no option (see the list at the end of this section).
91
+
92
+ **Row state**
93
+
94
+ | Option | What it does |
95
+ |---|---|
96
+ | `rowClass(row, idx)`, `rowAttrs(row, idx)` | Add classes and attributes to the row's `<tr>`. They follow the row through sorts and filters, and `updateRow` asks again, so a class the row no longer earns is removed. |
97
+ | `cellClass(value, colIdx, row, idx)` | Adds classes to a cell, after its type class. |
98
+ | `rowIcon(row, idx)` | Draws an icon (`'name'` or `{ icon, title, tone }`) at the start of the first cell. It is drawn from an attribute, so the icon's name is not part of the cell's text, its tooltip or a copy. |
99
+ | `emptyState` | Text, a node, or a function, shown in place of `emptyMessage`. It is hidden while `setLoading(true)` is on. |
100
+ | `setError(message \| node \| Error \| null)` | Shows a failure inside the table. If there are rows on screen, they stay, under a `role="alert"` banner. If there are none, the failure takes the place of the empty row. `setData({ rows })` and `setError(null)` clear it. |
101
+ | `showRowNumbers: 'position'` | Numbers rows 1, 2, 3 in the order shown, after a sort, a filter, or on a later page. `true` still numbers rows by their original index. |
102
+ | `nullDisplay: 'NULL'` | Draws `null` and `undefined` as this text, adds `twm-dt-cell--null` to the cell, and copies the text as well. |
103
+
104
+ **Values, not text**
105
+
106
+ | Option | What it does |
107
+ |---|---|
108
+ | `columnTypes: [..., 'date' \| 'datetime' \| 'duration']` | Sorts and filters the column by its value: an ISO 8601 string, a `Date` or epoch ms for dates, and milliseconds or text such as `'3.2 s'` for durations. A date-only value such as `2026-10-05` is a calendar date, so the time zone never moves it. Any other text, and a date that does not exist (`2026-02-31`), is shown as it came and sorts last. Non-ISO text is not passed to `Date.parse`, because browsers read it differently (V8 reads `'12'` as a date in 2001). Only `columnTypes` makes a column a typed column. A `getColumnType` that returns `'date'` still sets only the cell's class name, as in 0.4. |
109
+ | `dateFormat`, `dateTimeFormat` | **Default ISO: `YYYY-MM-DD` and `YYYY-MM-DD HH:mm`.** Pass a pattern (`DD.MM.YYYY`, `D MMM YYYY, h:mm a`, with `[literal]` text) or a function `(date) => string`, for every column or per column index. |
110
+ | `durationFormat` | `'auto'` (`850 ms`, `3.2 s`, `2m 5s`, `1h 2m`), `'clock'` (`1:02:05`), or a function. |
111
+ | `dateTimeZone: 'UTC'` | Reads and draws typed dates in UTC instead of the local zone. |
112
+ | `sortValue(value, colIdx, row)` | Sets what a column sorts by, for every column or (as an object or array) per column. Return `undefined` to fall back to the column's own rule. |
113
+
114
+ The column filters work on values. On a date column, `2026-10` matches October,
115
+ `>2026-10` matches after October, `<=2026-10-05` includes the 5th, and
116
+ `2026-01..2026-03` matches January through the end of March. A date operand is
117
+ ISO with two-digit parts, so an operator over a half-typed one (`>20`,
118
+ `>2026-1`) filters nothing until it is complete. Text without an operator that
119
+ is not such a date (`5 Oct`, `Oct`, `2026-1`) is matched against the cell as
120
+ drawn, so a filter narrows as you type it. On a duration column, `>1s`,
121
+ `<500ms` and `1s..1m` work. `formatDate`, `formatDuration`, `parseDateValue`
122
+ and `parseDuration` are exported, so text drawn elsewhere can use the same
123
+ words as the table.
124
+
125
+ **Housekeeping**
126
+
127
+ | Option | What it does |
128
+ |---|---|
129
+ | `DataTable.mount(host, config)` | Builds the table and draws it. The constructor still draws nothing until `render()`. |
130
+ | `DataTable.withDefaults(defaults)` | Returns a subclass whose `defaults` sit under every config it is given. `instanceof DataTable` holds, and it can be narrowed again. |
131
+ | `autoDispose: true` | Disposes the table once it has been taken out of the document and is still out a task later. It lets go of everything `dispose()` does, but it removes only the table's own elements and never empties the host. By then you may have mounted the next list into the host or written your own message there, and that stays. Do not set it on a table you detach and attach again later, such as a cached tab, because a disposed table comes back empty. |
132
+ | `resetColumnWidths()` | Forgets every dragged, fitted or restored width, the persisted ones included, and measures the columns again. Use it after a zoom or a font change. |
133
+
134
+ **Changes that need no option.** A table that sets none of the options above
135
+ behaves as it did in 0.4, except for these:
136
+
137
+ - The copy menu's document listeners (click, scroll, Escape, resize) now exist
138
+ only while the menu is open. Before 0.5.0, a table that had ever been
139
+ right-clicked kept them until `dispose()`.
140
+ - `attachLandingTableBehavior` judges a click by the `onRowActivate` rule:
141
+ - A double-click opens the row once, not twice.
142
+ - A click while text in the host is selected does not open the row. That is
143
+ the click that ends a drag across a cell.
144
+ - A click on a `<label>`, a `<summary>` or editable (`contenteditable`) text
145
+ in a row does not open the row either. 0.4.7 skipped only buttons, links
146
+ and form fields. A `[data-twm-action]` control still runs its action.
147
+ - The CSS reads the new tokens with the 0.4 values as fallbacks, so the
148
+ computed styles are the same while the tokens are unset. One exception: the
149
+ normal density's line-height token is declared on the `thead` and `tbody`.
150
+ A line-height rule of yours aimed at those two elements themselves now loses
151
+ to it if it is in a cascade layer, or has zero specificity and loads before
152
+ `flexdesk.css`. A rule for the cells, the rows or the table does not.
153
+
154
+ ## Comparing two tables: `diff` and `createScrollLock`
155
+
156
+ A side-by-side comparison is two `DataTable`s drawn from ONE aligned row list:
157
+ the rows as they are on one side, as they were on the other, and a gap where
158
+ a side has no row. Both options are off until you set them.
159
+
160
+ ```js
161
+ import { DataTable, createScrollLock } from '@flexdesk/widgets';
162
+
163
+ const lock = createScrollLock({ axis: 'y' }); // 'x', 'y' or 'both'
164
+ const side = (host, pick) => DataTable.mount(host, {
165
+ headers, rows: aligned.map((r) => pick(r) ?? blank), pagination: false,
166
+ scrollLock: lock,
167
+ diff: (_row, i) => ({ status: statusOf(aligned[i], pick), cells: changedCols(aligned[i]) }),
168
+ });
169
+ side(left, (r) => r.now);
170
+ side(right, (r) => r.then);
171
+ lock.setEnabled(false); // scroll each side on its own; true brings them level
172
+ ```
173
+
174
+ - `diff: (row, rowIdx) => {status, cells?}` draws the row
175
+ `twm-dt-diff twm-dt-diff--<status>`, for `added`, `removed`, `changed`,
176
+ `same` or `gap`, and each column index in `cells` (an array or a Set)
177
+ `twm-dt-diff__cell--changed`. `null` or any other status draws the row as
178
+ without `diff`. A `gap` row's cells are drawn empty, whatever its values and
179
+ `renderCell`, so the two sides keep their rows level. `updateRow` asks again.
180
+ - `scrollLock` takes a group from `createScrollLock()`. The table's body box
181
+ joins it on every render and leaves on the next render and on `dispose()`.
182
+ Any element can join with `lock.attach(el)`, which returns its detach. A
183
+ box that joins starts at the group's last position, and the echo of a
184
+ position the lock itself wrote moves nobody back. The lock copies pixels, so
185
+ it lines rows up only when both sides draw rows of the same height.
186
+
187
+ ## A server's rows in pages or as one list: `paging`
188
+
189
+ `paging` (off unless you set it) shows rows that your code fetches from a
190
+ server either one page at a time or as one list that loads more as the reader
191
+ scrolls. A strip under the body shows which rows are loaded. It has the page
192
+ buttons or a *Load all rows* button, and a Pages / Scroll switch if you pass
193
+ `onModeChange`. Set `paging` and the old pager (`pagination`) is not drawn.
194
+
195
+ ```js
196
+ new DataTable(host, {
197
+ headers, rows, // the rows you hold: one page, or all scrolled so far
198
+ virtualize: true,
199
+ paging: {
200
+ mode: 'scroll', // what was asked for: 'pages' or 'scroll'
201
+ pageSize: 1000,
202
+ softLimit: 10000, // above this many rows the table is always paged
203
+ total: 22521, // exact count, or null if unknown
204
+ atLeast: 0, // a lower bound when the count is not known
205
+ offset: 0, // in pages: where rows[0] is in the source
206
+ hasMore: true,
207
+ onPage: (offset, limit) => fetchPage(offset),
208
+ onLoadMore: () => fetchNext(), // called near the end of the scroll
209
+ onLoadAll: () => fetchRest(),
210
+ onModeChange: (mode) => remember(mode),
211
+ },
212
+ });
213
+ ```
214
+
215
+ Your code holds the rows and fetches them. The table decides how to show them
216
+ with `resolvePaging`, which is exported so your code gets the same answer when
217
+ it decides what to fetch. A source with more rows than `softLimit` is shown in
218
+ pages even if scroll was asked for. The strip says so, and Scroll is disabled
219
+ with the reason. `softLimit` can't be more than `hardLimit` (default and
220
+ ceiling `PAGING_HARD_LIMIT`, 100,000), and a scrolling table never asks for rows
221
+ past it. If no count is known, the rows already loaded count as evidence: a
222
+ table that has loaded more than `softLimit` rows and still has more is paged.
223
+ While scrolling, `onLoadMore` is called within 400px of the end, or after
224
+ render when the rows don't fill the box. It is never called when the box has
225
+ no height, for example in a hidden tab. A callback that returns a Promise
226
+ blocks the next call until it settles, and the strip shows the table busy.
227
+ `setPaging(patch)` redraws the strip only, for example when a count arrives.
228
+ In pages, `showRowNumbers: 'position'` numbers rows from `offset`. Row indexes
229
+ (`__rowIndex`, the selection, `onRowClick`) still refer to positions in `rows`.
230
+ In 0.7.0 the table can fetch the rows for you: see `dataSource` with `paging`
231
+ below.
232
+
233
+ ## DataTable options in 0.7.0: details, rows from a server, and filters that are not text
234
+
235
+ 0.7.0 adds what a log viewer needs: a row's detail folded under it, rows read
236
+ page by page from a server, a pager that does not need a total, a live tail,
237
+ and column filters that are not text boxes. Every option is off until you set
238
+ it, and a table that sets none of them draws and behaves as in 0.6.
239
+
240
+ ```js
241
+ import { DataTable } from '@flexdesk/widgets';
242
+
243
+ const LEVELS = { kind: 'chips', options: [
244
+ { value: '', label: 'All', icon: 'list_alt' },
245
+ { value: 'warn', label: 'Warn', icon: 'warning' },
246
+ { value: 'error', label: 'Error', icon: 'error' },
247
+ ] };
248
+ const entryOf = new WeakMap(); // each drawn row -> the entry it came from
249
+
250
+ const log = DataTable.mount(host, {
251
+ headers: ['When', 'Level', 'Who', 'What'],
252
+ columnTypes: ['datetime', null, null, null],
253
+ getRowKey: (row) => entryOf.get(row)?.id,
254
+ pageSize: 100,
255
+ sortable: true, filterable: true,
256
+ filterKinds: { 1: LEVELS },
257
+ // Rows from the server. Only the newest answer is drawn; `signal` aborts the rest.
258
+ dataSource: async (query, { signal }) => {
259
+ const [page, facets] = await Promise.all([api.audit(query, { signal }), api.facets(query)]);
260
+ const rows = page.data.map((e) => {
261
+ const row = [e.at, e.level, e.who, e.action];
262
+ entryOf.set(row, e);
263
+ return row;
264
+ });
265
+ return { rows, total: facets.total ?? null, hasMore: page.meta.has_more,
266
+ filterCounts: { 1: { '': facets.total, ...facets.levels } } };
267
+ },
268
+ pagerLabels: { first: 'Newest', prev: 'Newer', next: 'Older', last: 'Oldest' },
269
+ pagerPosition: 'bottom',
270
+ maxJumpOffset: 50_000,
271
+ follow: { interval: 10_000 },
272
+ onFollowChange: (on) => followButton.setAttribute('aria-pressed', String(on)),
273
+ rowDetail: (row) => {
274
+ const pre = document.createElement('pre');
275
+ pre.textContent = JSON.stringify(entryOf.get(row).detail, null, 2);
276
+ return pre;
277
+ },
278
+ hasRowDetail: (row) => !!entryOf.get(row)?.detail,
279
+ });
280
+ // Your own filters, outside the table, ask again from the first page:
281
+ searchBox.oninput = () => log.reload();
282
+ ```
283
+
284
+ **A row's detail**
285
+
286
+ | Option | What it does |
287
+ |---|---|
288
+ | `rowDetail(row, idx, { host, key })` | Gives each row a toggle at the start of its first cell. Opening it draws a row under the row, one cell across every column; the function fills `host` or returns a node or text for it. The detail is drawn when opened and dropped when closed. Its content does not change the column widths, wraps instead of being clipped, and is not part of a copy. A click in it selects nothing, and a right-click there opens the browser's own menu. |
289
+ | `hasRowDetail(row, idx)` | Which rows have a detail. A row without one gets no toggle. |
290
+ | `expandedRows`, `setRowExpanded(key, open)`, `isRowExpanded(key)`, `getExpandedRows()`, `collapseAllRows()` | Which details are open, by key (`getRowKey`, or the row's index). The state survives a sort, a filter, a page, `setData` and `refresh()`. A key that is not on screen opens when its row is drawn. Under `dataSource`, give `getRowKey`: without it a row's index is only its place in the page drawn, so a detail stays open across a new answer only where that answer has the same row (equal values) at the same place. |
291
+ | `onRowDetailToggle(idx, row, open, key)` | Called after a detail opens or closes. |
292
+ | `rowDetailLabel` | The toggle's accessible name (default `'Details'`). Its state is `aria-expanded`, and `aria-controls` names the detail. |
293
+
294
+ The toggle is a button, so Tab reaches it and Enter or Space opens it. It
295
+ neither selects the row nor opens it (`onRowActivate`). On the focused table,
296
+ ArrowRight opens the current row's detail and ArrowLeft closes it; the same
297
+ keys work on a toggle. Rows under an open detail keep their stripes. Under
298
+ `virtualize`, an open detail counts in its row's height, and it leaves and
299
+ re-enters the window with its row. It is measured when it is drawn and again
300
+ whenever the rows are (0.6.3: on every window update, on a resize and once the
301
+ column widths are fixed), so a detail that wraps differently at the final
302
+ widths does not leave the offsets wrong. A detail is never one of the rows the
303
+ row height is measured from.
304
+
305
+ **Rows from a server**
306
+
307
+ | Option | What it does |
308
+ |---|---|
309
+ | `dataSource(query, { signal })` | Asks for a page whenever the sort, a column filter or the page changes. `query` is `{ offset, limit, sort: { column, header, ascending } \| null, filters: [{ column, header, value }], reason }`, and `reason` is `'initial'`, `'sort'`, `'filter'`, `'page'`, `'reload'` or `'refresh'` (and, with `paging`, `'more'`, `'all'` or `'mode'`). Return `{ rows, total, hasMore, filterCounts }`, or just the rows. The answer is drawn as given, with no sort, filter or slice in the browser. |
310
+ | Only the newest answer is drawn | A request sent while another is out aborts the older one (`signal`), and an older answer that still arrives is dropped. So is an older request's failure. |
311
+ | A failure keeps the rows | The rows on screen stay, under `setError`'s banner with the error's message, and `onLoadError(error, query)` is called. The next answer clears it. |
312
+ | `onLoad(result, query)` | Called after each answer that was drawn. |
313
+ | `reload()` | Asks again from the first page. Call it when a filter of your own, outside the table, changes. It resolves with the answer, or with `undefined` if a newer request replaced it. |
314
+ | `getQuery()`, `getPageInfo()` | The last query sent, and `{ offset, total, hasMore }` for the page drawn. |
315
+ | `goToPage(page)`, `goToRow(idx)` | Ask the source for that page (the one holding row `idx`), up to the last page when the total is known, and resolve when it is drawn. |
316
+
317
+ While a request is out, the loading overlay shows and the empty state stays
318
+ hidden. An empty answer under a filter keeps the filter row, so the filter can
319
+ be cleared. A filter that is still being typed when an answer arrives keeps its
320
+ text, its focus and its pending request. Row indices (`onRowActivate`,
321
+ `getSelection`, `updateRow`) are positions in the page drawn, so they always
322
+ point at a row on screen. `showRowNumbers: 'position'` numbers rows from the
323
+ page's offset.
324
+
325
+ **A pager for an unknown total**
326
+
327
+ | Option | What it does |
328
+ |---|---|
329
+ | `total: null` in the answer | The total is unknown. The pager says `Rows 101–200` and `Page 2`, with no total. Next is enabled while `hasMore` is true or, without `hasMore`, while pages come back full. Last is disabled, and the reason is on the button (`data-refusal`, and its tooltip). |
330
+ | `hasMore` (with `onPageChange`) | The same pager for a table that pages through `onPageChange`: with `totalCount: null`, set `hasMore` to `true` or `false`. |
331
+ | `pagerLabels` | `{ first, prev, next, last }` draw a word beside each button's icon, such as `Newest`, `Newer`, `Older` and `Oldest` for a log read newest first; a labelled button is never narrower than its word. `noLast` is the reason given when there is no total, and `tooDeep(max)` the reason given past `maxJumpOffset`. |
332
+ | `maxJumpOffset` | The deepest offset that Last and Go-to-row may jump to. Past it, Last is disabled with its reason, and Go asks for nothing and marks the box `aria-invalid`, with the reason on it. Use it where a deep `OFFSET` would be slow. |
333
+ | `pagerPosition: 'bottom'` | Puts the pager under the rows. |
334
+
335
+ An empty page after the first says `Page 4 · no rows` and keeps the pager, so
336
+ you can go back. A table without these options keeps the 0.6 pager.
337
+
338
+ **Pages or one scrolling list from a server: `dataSource` with `paging`**
339
+
340
+ 0.6.2's `paging` and 0.7.0's `dataSource` answer two different questions, and
341
+ they combine. `paging` decides how the rows are shown: in pages, or as one list
342
+ that loads more near the end, with its strip under the body and its soft and
343
+ hard limits. `dataSource` decides who fetches them: the table. Without
344
+ `dataSource`, your code fetches, exactly as in 0.6.2. With it, the table is its
345
+ own `paging` consumer:
346
+
347
+ ```js
348
+ DataTable.mount(host, {
349
+ headers, virtualize: true,
350
+ dataSource: (query, { signal }) => api.rows(query, { signal }), // { rows, total, hasMore }
351
+ paging: { mode: 'scroll', pageSize: 1000, softLimit: 10000, onModeChange: remember },
352
+ });
353
+ ```
354
+
355
+ | `paging` key | Without `dataSource` (0.6.2) | With `dataSource` (0.7.0) |
356
+ |---|---|---|
357
+ | `mode`, `pageSize`, `softLimit`, `hardLimit`, `loadMorePx` | Yours | Yours. `pageSize` is also each question's `limit`. |
358
+ | `offset`, `total`, `atLeast`, `hasMore`, `held` | Yours, kept up to date with `setPaging` | Read from the source's answers (`atLeast` may be in the answer). What you pass is ignored. |
359
+ | `onPage(offset, limit)` | Yours: fetch the page | The table's: asks `{ offset, reason: 'page' }`. |
360
+ | `onLoadMore()` | Yours: fetch the next rows | The table's: asks `{ offset: rows held, limit: pageSize, reason: 'more' }` and draws the answer under the rows held. |
361
+ | `onLoadAll()` | Yours | The table's: one question, `{ limit: hardLimit − rows held, reason: 'all' }`. The source answers as many rows as it will, and `hasMore` says whether there are more. |
362
+ | `onModeChange(mode)` | Yours: show the rows the other way | Called after the table has switched and asked again from the first row (`reason: 'mode'`). Without it there is no switch, as in 0.6.2. |
363
+
364
+ So there is one strip, one set of limits and one load trigger. With `paging`
365
+ the 0.7.0 pager is not drawn, and `pagerPosition` does nothing. `pagerLabels`
366
+ and `maxJumpOffset` apply to the strip's page buttons.
367
+
368
+ - While the source answers a sort, a filter or a refresh, the table asks for no
369
+ more rows. A sort or filter sent while more rows are on their way aborts that
370
+ request, as any newer question does. More rows do not show the loading
371
+ overlay; the strip shows the table is busy.
372
+ - More rows keep the scroll position, the selection and the open details.
373
+ - When the rows scrolled in prove the source holds more than `softLimit`, the
374
+ table is shown in pages from then on (`resolvePaging`, as in 0.6.2). The page
375
+ shown is the rows just loaded, so nothing is fetched again and the reader
376
+ carries on where they were.
377
+ - `refresh()` and `follow` in scroll mode read the rows held again, in one
378
+ question from the first row. Scrolling down to load more turns following
379
+ off, the way paging away does. Rows loaded only because the first ones did not
380
+ fill the box do not, since the reader is still at the top.
381
+ - `setPaging({ mode })` that changes how the rows are shown asks again from the
382
+ first row. Other patches only redraw the strip, as in 0.6.2.
383
+ - `goToPage`, `goToRow` and `getPageInfo` work as they do without `paging`,
384
+ in `paging.pageSize` pages.
385
+
386
+ **Refresh and follow**
387
+
388
+ | Option | What it does |
389
+ |---|---|
390
+ | `refresh()` | Reads the current page again in place. A reader at the top sees the rows that arrived. A reader scrolled down keeps the rows they were reading at the same place on screen, because new rows above move the scroll position instead of the rows. The selection is kept by key, as are the active row and the open details. Rows that the last answer did not hold are drawn `twm-dt-row--new` (a short highlight); this needs `getRowKey`. |
391
+ | `follow: true` or `{ interval }` | Calls `refresh()` every `interval` ms (default 10 000) while the table is on its first page, in the document, and has no request out. A tick also waits while somebody is reading: while text inside the table is selected, or the focus is inside an open detail, it reads nothing, and following resumes at the next tick after. A `refresh()` you call yourself does not wait, and puts the focus back on the same place in a detail it draws again. Paging away turns following off and calls `onFollowChange(false)`. Coming back to the first page does not turn it on again. |
392
+ | `setFollow(on)`, `isFollowing()`, `onFollowChange(on)` | Turn following on and off from your own toggle, and hear when the table turns it off. |
393
+
394
+ The follow timer holds the table weakly, so it never keeps a discarded table
395
+ alive, and it stops on `dispose()`.
396
+
397
+ **Filters that are not text**
398
+
399
+ | Option | What it does |
400
+ |---|---|
401
+ | `filterKinds` | A filter control per column, as an object or array keyed by column index, or a function `(colIdx, header) => spec`. `'text'` (or nothing) is the usual text box. `{ kind: 'chips', options: [{ value, label, icon, title }], multiple }` draws chips. `{ create(ctx) }`, or a function, is your own control. A column is never narrower than its control at its narrowest (for chips, the widest chip); above that the cell wraps the control. |
402
+ | The chips | One chip per option, each with a count. An option whose value is `''` or `null` means no filter, and it is pressed when nothing is chosen. Pressing the chosen chip again clears the filter. With `multiple`, each chip toggles its value in a list. A chip whose count is 0 is dimmed, not hidden. `createChipsFilter(ctx, spec)` builds the same control outside a table. |
403
+ | Your own control | `create(ctx)` receives `{ column, header, value, counts, setValue(v), table }` and returns `{ el, setValue(v), setCounts(c), destroy(), match(cell, value, row) }`. Only `el` is required. Call `ctx.setValue` with the new value, which can be anything, such as `{ from, to }` for a date range or a person's id. |
404
+ | `setFilterCounts(colIdx, counts)` or `setFilterCounts({ [colIdx]: counts })` | Gives a column's control its counts, keyed by option value. They are kept, so a control built later starts with them. A `dataSource` answer can carry them as `filterCounts`. `getFilterCounts(colIdx)` reads them back. |
405
+ | `setFilter(colIdx, value)`, `getFilters()`, `onFilterChange(filters)` | Set one column's filter from code (an empty value clears it), read the filters in force as `[{ column, header, value }]`, and hear every change. `clearFilters()` resets each control as well. |
406
+
407
+ Under `dataSource`, a filter's value goes to the server as it is. Without a
408
+ server, the control's `match` decides which rows pass. If it has none, the
409
+ chips use equality (or membership, with `multiple`), a `{ from, to }` value is
410
+ an inclusive range over dates or numbers, a list means "one of these", and text
411
+ means "contains". A control is built once, and the same element is put back
412
+ after every render, so its own state, the focus and the text selection inside
413
+ it survive a redraw. It is destroyed when the columns change or the table is
414
+ disposed.
415
+
416
+ **Housekeeping**
417
+
418
+ | Option | What it does |
419
+ |---|---|
420
+ | `keepWhileDetached` | With `autoDispose`, how long a table may be out of the document and still come back drawn. A number is a grace period in ms. `true` keeps the table for as long as it is out: it is disposed only by `dispose()`, and while out it holds nothing on the document. A table put back within its grace is not disposed, however many tasks it was out, and the grace counts from its last departure, not its first. Use it for a tab whose content you detach and attach again later. Without it, `autoDispose` gives one task's grace, as in 0.5.0. |
421
+
422
+ **One change that needs no option.** `autoDispose` now cancels a pending
423
+ dispose when the table is put back. Before 0.7.0, a table taken out, put back
424
+ and taken out again could be disposed by the first departure's timer.
425
+
426
+ ## DataTable: your own right-click items, a render hook, the keyboard, a remembered page
427
+
428
+ These options let an application drop a private copy of the DataTable kept for
429
+ them. Every one is off until you set it, and a table that sets none of them
430
+ draws and behaves as in 0.9.1.
431
+
432
+ ```js
433
+ const queue = DataTable.mount(host, {
434
+ headers, rows, pagination: true, sortable: true, filterable: true,
435
+ stateStore, persistKey: `queue:${view}`,
436
+ persistPage: !!savedView, // an ad-hoc view shares its key: start it on page 1
437
+ getRowKey: (row) => row[0],
438
+ contextMenuItems: (row, rowIdx, ctx) => [
439
+ { label: ctx.selection.length > 1 ? `Open ${ctx.selection.length}` : 'Open',
440
+ icon: 'open_in_new', action: 'open' },
441
+ { separator: true },
442
+ { label: 'Delete…', icon: 'delete', action: 'delete', danger: true,
443
+ disabled: !canDelete(row), title: canDelete(row) ? '' : 'Closed records stay' },
444
+ ],
445
+ onContextMenuAction: (action, rowIdx, row, ctx) => run(action, ctx.selectedRows),
446
+ onRender: (t) => status.textContent = `${t.getDisplayedRows().length} of ${rows.length} shown`,
447
+ keyboardNav: true,
448
+ onRowKeyDown: (rowIdx, row, ev) => {
449
+ if (row && ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) { openElsewhere(row); return true; }
450
+ if (row && ev.altKey && ev.key.toLowerCase() === 't') { openInTab(row); return true; }
451
+ },
452
+ onRowActivate: (i, row) => open(row),
453
+ activateOn: ['click', 'enter'],
454
+ });
455
+ queue.focus();
456
+ ```
457
+
458
+ **The right-click menu**
459
+
460
+ | Option | What it does |
461
+ |---|---|
462
+ | `contextMenuItems` | Your items in the table's own right-click menu: an array, or `(row, rowIdx, ctx) => items`, asked on every right-click of a row. An item is `{ label, icon, action, disabled, danger, title }` or `{ separator: true }`, drawn like the copy items (`disabled` greys it out, `danger` draws it red, `title` says why). `ctx` is `{ rowIdx, row, colIdx, value, selection, selectedRows }`: the row and the cell under the pointer (`colIdx` is null on the row-number column) and the selection, which by then includes the row. Your items come first; with `copyable`, a separator and the two copy items follow. Set without `copyable`, the menu has your items only, and a row you give no items gets the browser's own menu. |
463
+ | `onContextMenuAction(action, rowIdx, row, ctx)` | Called when one of your items is chosen, after the menu has closed. The copy items never call it, so an action may be named anything. |
464
+
465
+ **What the table shows**
466
+
467
+ | Option | What it does |
468
+ |---|---|
469
+ | `onRender(table)` | Called at the end of every render: the first one, `setData`, a sort, a filter, a page. A hook that throws is reported on the console and the table still draws. |
470
+ | `getDisplayedRows()` | The rows the table shows, filtered and sorted, across every page and not only the one on screen. It returns a copy. With server-side paging it is the page held. |
471
+
472
+ **The keyboard**
473
+
474
+ | Option | What it does |
475
+ |---|---|
476
+ | `keyboardNav: true` | On the focused table, ArrowUp and ArrowDown move the selection one row through the page shown, in the order shown, and Home and End go to its first and last row. With Shift they select the range from the anchor, as Shift-click does (not on a `selectable: 'single'` table). The row is scrolled into view and `onSelectionChange` is told. Keys pressed in a control inside the table are left alone. Needs `selectable`. |
477
+ | `onRowKeyDown(rowIdx, row, ev)` | Called for every key pressed on the focused table, before the table's own keys, with the row Enter would open (the one row selected, the selection's anchor, or else the active row) or `null`. Return `true` to take the key: its default is prevented and nothing after it sees it, neither the table (Enter's `onRowActivate`, the arrows, copy) nor the page. Use it for the keys `activateOn` does not take, such as Ctrl+Enter or Alt+T. |
478
+ | `focus()` | Puts the keyboard focus on the table's body without scrolling the page. Returns `false` when nothing is drawn yet. |
479
+
480
+ **A remembered page**
481
+
482
+ | Option | What it does |
483
+ |---|---|
484
+ | `persistPage: true` | With `persistKey` and `stateStore`, saves the client page with the sort and filters, and opens the table on it again. If the rows have shrunk since, it opens on the last page. A new filter, and a sort under `resetPageOnSort`, go to the first page and save that. Rows given after mounting (`rows: []`, then `setData`) still open on the saved page. Server-side paging is not affected: its page is your `offset`. Off by default, because a key shared by different row sets, such as ad-hoc filters or search results, must not reopen on a page of the previous list. Without it, the saved state is exactly what 0.9.1 saves. |
485
+
486
+ ## DataTable: columns hidden until somebody asks for them
487
+
488
+ A column can be part of the row data without being drawn. People choose which
489
+ columns they see from the header's right-click menu, and the choice is
490
+ remembered. A table that sets none of these options draws every column, as in
491
+ 0.9.1.
492
+
493
+ ```js
494
+ const list = DataTable.mount(host, {
495
+ headers: ['Id', 'Summary', 'Status', ...customFields.map((f) => f.label)],
496
+ rows,
497
+ hiddenColumns: customFields.map((_, i) => 3 + i), // not shown until picked
498
+ columnChooser: true,
499
+ stateStore, persistKey: 'bugs:queue',
500
+ onColumnsChange: (hidden) => console.log('hidden now', hidden),
501
+ });
502
+ list.setColumnVisible(3, true);
503
+ ```
504
+
505
+ | Option | What it does |
506
+ |---|---|
507
+ | `hiddenColumns: [colIdx, ...]` | These columns are not drawn: no header, no filter box, no cell, no width in the fit, and they are left out of a copy and of `downloadCSV()`. They stay in the row data, so `renderCell`, `contextMenuItems`, `sortBy` and `setFilter` still see them. The last column shown can never be hidden; a list that hides all of them keeps the first. `setData({ hiddenColumns })` applies a new list. Column indices are positions, so a new `headers` list keeps the set by position. |
508
+ | `columnChooser: true` | A right-click on the header opens a menu that lists every column, with a check mark on those shown. Choosing one shows or hides it. The last column shown is greyed out and says why. Without the option the header keeps the browser's menu. |
509
+ | `setColumnVisible(colIdx, on)` | Shows or hides a column from code. Returns whether anything changed. The widths of the other columns, dragged or fitted, stay with their columns; a column shown again is measured. |
510
+ | `getHiddenColumns()` | The hidden columns' indices, ascending. |
511
+ | `onColumnsChange(hidden, { hidden, order })` | Called after every change of the set: the chooser, `setColumnVisible` and `setData({ hiddenColumns })`, and after every change of the column order (see below). `order` is `getColumnOrder()`. |
512
+ | With `persistKey` | The set is saved with the sort and the filters and restored on mount. A saved set wins over `hiddenColumns`, so a column somebody chose stays chosen. A table without these options saves exactly what 0.9.1 saves. |
513
+
514
+ A column sorted on and then hidden keeps the order; its arrow comes back with
515
+ the column.
516
+
517
+ ## DataTable: columns in the order somebody puts them (0.17.0)
518
+
519
+ Columns can be dragged into a new order by their headers, or ordered from
520
+ code, and the order is remembered. The order is kept by data index, so
521
+ `hiddenColumns`, the sort, the filters, `renderCell`'s `colIdx` and cell
522
+ mode's `col` stay on their columns. A table that sets none of these options
523
+ draws its columns in their natural order and saves what 0.16 saves.
524
+
525
+ ```js
526
+ const list = DataTable.mount(host, {
527
+ headers: ['Id', 'Summary', 'Status', 'Owner'],
528
+ rows,
529
+ reorderable: true,
530
+ columnOrder: [0, 3, 1, 2], // optional starting order
531
+ stateStore, persistKey: 'bugs:queue',
532
+ onColumnsChange: (hidden, { order }) => console.log('order now', order),
533
+ });
534
+ list.moveColumn(3, 1); // positions in getColumnOrder()
535
+ ```
536
+
537
+ | Option | What it does |
538
+ |---|---|
539
+ | `reorderable: true` | Every data column's header can be dragged onto another; dropped, it takes that column's place (to its left when moved left, to its right when moved right). A press that starts on a column's resize grip resizes it and never starts a drag. The row-number and select columns stay first: they cannot be dragged, and nothing can be dropped on or in front of them. The header's right-click menu offers Move left and Move right, after any `headerMenu` items and before the `columnChooser` items; they pass over hidden columns. |
540
+ | `columnOrder: [colIdx, ...]` | The order to draw the columns in: every data index once, hidden ones included. A list that is not a permutation of the columns is ignored with a console warning, and the natural order is drawn; a table mounted with no headers yet checks it, and warns once, when `setData` brings the first ones. Works without `reorderable`, in which case only code moves columns. `setData({ columnOrder })` applies a new order; `null` goes back to the natural one. |
541
+ | `moveColumn(from, to)` | Moves the column at position `from` of `getColumnOrder()` to position `to`, the others closing up: from the natural order, `moveColumn(3, 1)` draws 0, 3, 1, 2. Returns whether the order changed. Dragged widths stay with their columns. |
542
+ | `getColumnOrder()` | Every data index, hidden ones included, in the order drawn (a copy). |
543
+ | With `persistKey` | The order is saved with the sort, the filters and the widths, and restored on mount. A saved order wins over `columnOrder`. A saved order for a different number of columns than the table has today is ignored: `columnOrder` is drawn, or the natural order when there is none. That holds whether the headers are known when the blob is read or come later with `setData`; likewise, a dragged order that new headers no longer fit gives way to `columnOrder`, or the natural order. A blob saved before 0.17 (no order) loads as it did. |
544
+ | Copies and the CSV | Ctrl+C, the copy menu and `downloadCSV()` write the columns in the order shown. |
545
+
546
+ The drag is HTML5 drag-and-drop on the header cells. The cell being dragged is
547
+ `twm-dt-col--dragging`, the one under the pointer `twm-dt-col--drop` (plus
548
+ `twm-dt-col--drop-after` when the column would land to its right). The logic
549
+ lives in `column_order.js`, which is internal to the table.
550
+
551
+ ## DataTable: the CSV button on every table (0.14.1)
552
+
553
+ `showExportButton: true` draws "Download visible rows as CSV" in the pagination
554
+ strip, and now draws the strip for it on a table that fits on one page too (the
555
+ button alone, no page buttons). `exportFilename` — a string, or `(table) =>
556
+ string` — names the file (default `data.csv`). With a `host` that has `dialogs`
557
+ the button opens the native Save dialog, suggesting that name; without one it
558
+ is a browser download.
559
+
560
+ ```js
561
+ DataTable.mount(el, { headers, rows, pagination: true, showExportButton: true,
562
+ exportFilename: 'bugs.csv', host });
563
+ ```
564
+
565
+ ## DataTable: a select mode (0.12.0)
566
+
567
+ A list whose click opens a row can still let people pick many rows: a toggle
568
+ in the table's own header turns on a select mode, with a checkbox per row and
569
+ a click that ticks instead of opening. A table that sets none of these options
570
+ draws and behaves as in 0.11.
571
+
572
+ ```js
573
+ const list = DataTable.mount(host, {
574
+ headers, rows,
575
+ getRowKey: (row) => row[0], // keeps the ticks across new rows
576
+ onRowActivate: (i, row) => open(row), // not called while selecting
577
+ selectToggle: true,
578
+ onSelectModeChange: (on) => bar.hidden = !on,
579
+ onSelectionChange: (ticked) => bar.count = ticked.length,
580
+ });
581
+ list.setSelectMode(true);
582
+ ```
583
+
584
+ | Option | What it does |
585
+ |---|---|
586
+ | `selectToggle: true` | Draws a narrow leading column whose header holds a toggle button. While the mode is on, every row shows a checkbox and the header a select-all box. Needs a multi-row `selectable` (on `false` or `'single'` it is ignored, with a console warning). |
587
+ | `selectMode: true` | Starts in the mode. Draws the column even without `selectToggle`; the consumer then leaves the mode with `setSelectMode(false)`. |
588
+ | `setSelectMode(on)` / `isSelectMode()` | Turns the mode on or off from code (returns whether it changed), and says whether it is on. The selection is kept either way. |
589
+ | `onSelectModeChange(on)` | Called after every change of the mode: the toggle, Escape, `setSelectMode`. |
590
+ | In the mode | A plain click or Space ticks or unticks one row; Shift-click adds the range from the last row ticked. The arrows move a cursor without changing the ticks (Shift ticks the rows passed). A right-click keeps the ticks. Escape clears them, and a second Escape leaves the mode. Nothing opens a row: `onRowActivate` is not called (`onRowClick` still is, so a consumer that opens rows from it checks `isSelectMode()`). New rows (`setData`) keep the ticks by `getRowKey`. |
591
+ | Select-all | Ticks every row the filters let through, on every page; when all are ticked it clears them. It shows checked, mixed or clear. |
592
+
593
+ The select column is not a data column: it is not in a copy, a CSV,
594
+ `ctx.colIdx` or `hiddenColumns`, and it cannot be resized. It is drawn for the
595
+ table's whole life, so remembered widths stay on their columns when the mode
596
+ flips, and widths saved by a table without it land on the same columns.
@@ -0,0 +1,22 @@
1
+ # Editor
2
+
3
+ > **Package:** `@flexdesk/editor` · **Needs:** Monaco, as a [peer dependency](../concepts/peer-dependencies.md) · [All docs](../README.md)
4
+
5
+ A Monaco-backed multi-file editor: the loader, the editor factory, the tab bar, search, a split-pane container and an undo manager. Monaco itself is not in the bundle, and its location is not guessed: call `setMonacoBasePath()` and `setDefaultLanguage()` first.
6
+
7
+ ## Auto-height editors: `maxHeight` (0.17.0)
8
+
9
+ `createEditor()` sizes its container to the content (80px at least, see
10
+ `setMinHeight`). Cap it with `maxHeight`, in px; `Infinity`, the default, is
11
+ no cap. Past the cap Monaco scrolls internally and takes the mouse wheel;
12
+ below it the wheel reaches whatever scrolls around the editor.
13
+
14
+ ```js
15
+ const h = factory.createEditor(el, source, { maxHeight: 400 });
16
+ h.setMaxHeight(Infinity); // applies at once: grows to the content
17
+ ```
18
+
19
+ A non-positive or non-number cap throws a `RangeError`. With `noAutoHeight`
20
+ the container's own layout decides the size and `setMaxHeight` has no effect.
21
+ Before 0.17.0 the cap was a fixed 400px, switched on by `data-code-max-height="true"`
22
+ on a `.notebook-editor-container` ancestor; that attribute is no longer read.