flexdesk 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -0
- package/css/base.css +333 -0
- package/css/flexdesk.css +333 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-WLASPRGP.js → chunk-JLCJWCRS.js} +1577 -56
- package/dist/chunk-JLCJWCRS.js.map +7 -0
- package/dist/{chunk-WAVLVXYV.js → chunk-WKTYKXWF.js} +2 -2
- package/dist/flexdesk.css +333 -0
- package/dist/flow.js +24 -8
- package/dist/flow.js.map +2 -2
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +672 -1
- package/dist/widgets.js.map +4 -4
- package/package.json +1 -1
- package/src/flow/outline/editor.js +46 -8
- package/src/ui/components/data_table.js +1650 -66
- package/src/ui/components/fact_table.js +342 -0
- package/src/ui/components/record_list.js +594 -0
- package/src/ui/components/table_filter_chips.js +153 -0
- package/dist/chunk-WLASPRGP.js.map +0 -7
- /package/dist/{chunk-WAVLVXYV.js.map → chunk-WKTYKXWF.js.map} +0 -0
package/README.md
CHANGED
|
@@ -397,6 +397,324 @@ blocks the next call until it settles, and the strip shows the table busy.
|
|
|
397
397
|
`setPaging(patch)` redraws the strip only, for example when a count arrives.
|
|
398
398
|
In pages, `showRowNumbers: 'position'` numbers rows from `offset`. Row indexes
|
|
399
399
|
(`__rowIndex`, the selection, `onRowClick`) still refer to positions in `rows`.
|
|
400
|
+
In 0.7.0 the table can fetch the rows for you: see `dataSource` with `paging`
|
|
401
|
+
below.
|
|
402
|
+
|
|
403
|
+
### Record lists and fact sheets (0.7.0)
|
|
404
|
+
|
|
405
|
+
Two read-only components for the parts of an application page that are not a
|
|
406
|
+
table of rows. Both are new: nothing that exists changes, and a page that does
|
|
407
|
+
not call them looks as it did. Neither needs a height: each is exactly as tall
|
|
408
|
+
as its contents, in any parent.
|
|
409
|
+
|
|
410
|
+
**`createRecordList(host, config)`** draws a list of records, each as a name
|
|
411
|
+
with a quiet line of facts under it, pills and verbs beside them, and a state.
|
|
412
|
+
It is for the administration list of a few records with their own actions
|
|
413
|
+
(keys, connections, deliveries, members), where a table would be the wrong
|
|
414
|
+
shape.
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
import { createRecordList } from '@flexdesk/widgets';
|
|
418
|
+
|
|
419
|
+
const list = createRecordList(host, {
|
|
420
|
+
records: keys,
|
|
421
|
+
describe: (k) => ({
|
|
422
|
+
name: k.name,
|
|
423
|
+
facts: [k.key_id, k.role, k.expires ? `expires ${k.expires}` : 'never expires'],
|
|
424
|
+
pills: [k.paused ? { text: 'paused', tone: 'warn' } : { text: 'live', tone: 'success' }],
|
|
425
|
+
state: k.revoked ? 'off' : k.paused ? 'paused' : 'active',
|
|
426
|
+
verbs: [
|
|
427
|
+
{ label: k.paused ? 'Resume' : 'Pause', onClick: (key) => toggle(key),
|
|
428
|
+
refusal: k.revoked ? 'This key is revoked.' : null },
|
|
429
|
+
{ label: 'Revoke', danger: true, onClick: (key) => revoke(key) },
|
|
430
|
+
],
|
|
431
|
+
}),
|
|
432
|
+
getKey: (k) => k.key_id,
|
|
433
|
+
rowAttrs: (k) => ({ 'data-key-id': k.key_id }),
|
|
434
|
+
onActivate: (k) => open(k), // one click, or Enter
|
|
435
|
+
activeKey: openKeyId,
|
|
436
|
+
emptyState: 'No keys yet.',
|
|
437
|
+
});
|
|
438
|
+
list.setRecords(next); // focus stays on the same record's same verb
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
What `describe(record, idx)` returns (without `describe`, each record is
|
|
442
|
+
this object itself):
|
|
443
|
+
|
|
444
|
+
| Field | What it is |
|
|
445
|
+
|---|---|
|
|
446
|
+
| `name` | The first line: text or a node. `mono: true` draws it in the monospace face. |
|
|
447
|
+
| `facts` | The quiet line: text, a node, or an array whose `null`, `false` and `''` parts are left out and the rest joined with `factSeparator` (`' — '`). A part may be a node. |
|
|
448
|
+
| `pills` | Strings or `{ text, tone, title }`. The tones drawn are `muted`, `info`, `success`, `warn` and `danger`. |
|
|
449
|
+
| `verbs` | `{ label, onClick(record, ev), refusal, danger, primary, icon, title, name }`. A verb with a `refusal` is a disabled button with the reason on its `title` and `data-refusal`, **and the reason is a line of text under the facts** (`twm-record__refusal`, named by `data-refusal-for`, which the button is `aria-describedby`). A reason shared by every verb is said once. `null` and `false` verbs are left out. |
|
|
450
|
+
| `state` | A word: `data-state` and `twm-record--state-<word>` on the row. `paused`, `off` and `dead` dim the words and pills but never the verbs; `dead` and `error` mark the left edge in the danger colour. |
|
|
451
|
+
| `error` | A line in the error colour under the facts. |
|
|
452
|
+
| `key` | The record's key, over `getKey`. |
|
|
453
|
+
|
|
454
|
+
| Option | What it does |
|
|
455
|
+
|---|---|
|
|
456
|
+
| `getKey(record, idx)` | The key (`data-record-key`). Defaults to `spec.key`, then `record.key`, then `record.id`, then the index. Keys compare as strings. |
|
|
457
|
+
| `onActivate(record, idx, ev)` | Opens a record. It follows `DataTable`'s `onRowActivate` rule (`row_activation.js`): not for a press on a verb or any control, not for the click that ends a drag selecting text in the list, not for the second click of a double-click. Rows get a pointer and a hover. |
|
|
458
|
+
| `activateOn` | `'click'`, `'dblclick'`, `'enter'`, or an array. Default `['click', 'enter']`. |
|
|
459
|
+
| `keyboard` | Defaults to on exactly when `onActivate` is set. The records are then one tab stop (the last focused, else the open one, else the first), each enabled verb keeps its own, ArrowUp/ArrowDown move between records (from a record or one of its verbs), Home/End go to the ends, and Enter on a record opens it. |
|
|
460
|
+
| `activeKey`, `setActiveKey(key)`, `getActiveKey()` | The record that is open: `twm-record--active` and `aria-current="true"`, drawn as an accent on the left edge. It survives `setRecords`. |
|
|
461
|
+
| `rowClass(record, idx)`, `rowAttrs(record, idx)` | Classes and attributes for the record's `<li>`, as `DataTable`'s (`null`/`false` leaves an attribute off, `true` sets it empty). |
|
|
462
|
+
| `emptyState` | Text, a node, or a function returning either, shown when there are no records. |
|
|
463
|
+
| `facts: 'ellipsis'` | Keeps every fact line to one line and puts the whole line on its `title`. By default a long fact line wraps. |
|
|
464
|
+
| `verbClass` | A class string, or `(verb) => string`, for a consumer with its own buttons. By default verbs are FlexDesk's `twm-btn`, with `--primary` and `--danger`. |
|
|
465
|
+
| `label`, `className` | The list's `aria-label`; a class on the root. |
|
|
466
|
+
|
|
467
|
+
Methods: `setRecords(records)` (keeps the focus on the same record's same verb,
|
|
468
|
+
by key), `getRecords()`, `refresh(key)` (asks `describe` again for that record
|
|
469
|
+
only; with no key, all), `getItem(key)`, `focusRecord(key)`, `dispose()`, and
|
|
470
|
+
`element` (the root).
|
|
471
|
+
|
|
472
|
+
**`createFactTable(host, config)`** draws a sheet of facts: a name and its
|
|
473
|
+
value, row after row. A `DataTable` is the wrong tool for this, because it
|
|
474
|
+
draws a header row, clips a long value to one line and needs a box with a
|
|
475
|
+
height. A fact sheet has no header row, never truncates (a value wraps at
|
|
476
|
+
spaces, inside a long word or URL when it must, and keeps its own line breaks),
|
|
477
|
+
and is as tall as its rows. The label column stays on one line.
|
|
478
|
+
|
|
479
|
+
```js
|
|
480
|
+
import { createFactTable } from '@flexdesk/widgets';
|
|
481
|
+
|
|
482
|
+
createFactTable(host, {
|
|
483
|
+
rows: [
|
|
484
|
+
{ label: 'Server', value: 'db-main-01.internal' },
|
|
485
|
+
{ label: 'Connection string', value: dsn, mono: true },
|
|
486
|
+
{ label: 'Options', value: { pool: { min: 2, max: 16 } } }, // drawn as indented JSON
|
|
487
|
+
{ label: 'Replica', value: null },
|
|
488
|
+
],
|
|
489
|
+
nullDisplay: '—',
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Before and after:
|
|
493
|
+
createFactTable(host, {
|
|
494
|
+
compare: true,
|
|
495
|
+
headers: ['Field', 'Now', 'After the restore'],
|
|
496
|
+
rows: [{ label: 'Amount', before: 1200, after: 950 }, { label: 'Reviewer', after: 'Bea Beta' }],
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
| Option | What it does |
|
|
501
|
+
|---|---|
|
|
502
|
+
| `rows` | Objects `{ label, value }` (or `{ label, before, after }` with `compare`), with optional `mono`, `numeric`, `labelTitle` (a tooltip on the label), `key` (`data-key`) and `status`; or arrays of cells, `[label, value, …]`. A value is text, a number, a node, or an object (drawn as indented JSON). |
|
|
503
|
+
| `compare: true` | Each row draws its label, its before, an arrow and its after. A row is `twm-facts__row--changed`, `--same`, `--added` (no `before`) or `--removed` (no `after`), worked out from the two sides (objects by their JSON, a node always changed) unless the row's `status` says otherwise; `data-status` says the same. An absent side reads as `missing` (`'—'`). A row with a plain `value` spans both sides. |
|
|
504
|
+
| `headers` | Adds a header row (there is none by default). With `compare`, three names stand over the label, before and after columns. |
|
|
505
|
+
| `numeric` | Column indices drawn in tabular numerals (`twm-facts__num`), header included. |
|
|
506
|
+
| `mono` | Every value in the monospace face. A row's own `mono` does it for that row. |
|
|
507
|
+
| `nullDisplay` | What `null` and `undefined` read as (`twm-facts__null`). By default the cell is empty. |
|
|
508
|
+
| `labelColumn: false` | The first column is ordinary data, not a label. |
|
|
509
|
+
| `wrapLabels: true` | Lets a long label wrap. |
|
|
510
|
+
| `wrap` | How a value wraps. `'anywhere'` (the default) wraps at spaces, and inside a long word or URL when it must. `'words'` wraps at spaces only, so a long word stays whole and the sheet grows wider; use it inside a sideways scroller. `'none'` keeps only the value's own line breaks. |
|
|
511
|
+
| `rowClass(row, idx)`, `rowAttrs(row, idx)` | Classes and attributes for each `<tr>`. |
|
|
512
|
+
| `emptyState`, `label`, `className` | Shown (spanning the sheet) when there are no rows; the `aria-label`; a class on the `<table>`. |
|
|
513
|
+
|
|
514
|
+
Methods: `setRows(rows)`, `getRows()`, `getRow(idx)` (the `<tr>`), `dispose()`,
|
|
515
|
+
and `element` (the `<table>`).
|
|
516
|
+
|
|
517
|
+
Both were measured in headless Edge with real input (`demo/records.html`,
|
|
518
|
+
driven by `demo/records_probe.mjs`): no list, record, sheet or row has zero
|
|
519
|
+
height and none overflows its card; a long fact line, a long URL, a connection
|
|
520
|
+
string and a JSON value wrap inside their column while labels stay on one line;
|
|
521
|
+
every verb, refused ones included, is what `elementFromPoint` finds at its
|
|
522
|
+
centre, and a real press runs it without opening the record; real Tab,
|
|
523
|
+
ArrowDown and Enter open a record; at 300px the verbs go under the record's
|
|
524
|
+
words, inside the card.
|
|
525
|
+
|
|
526
|
+
### DataTable options in 0.7.0: details, rows from a server, and filters that are not text
|
|
527
|
+
|
|
528
|
+
0.7.0 adds what a log viewer needs: a row's detail folded under it, rows read
|
|
529
|
+
page by page from a server, a pager that does not need a total, a live tail,
|
|
530
|
+
and column filters that are not text boxes. Every option is off until you set
|
|
531
|
+
it, and a table that sets none of them draws and behaves as in 0.6.
|
|
532
|
+
|
|
533
|
+
```js
|
|
534
|
+
import { DataTable } from '@flexdesk/widgets';
|
|
535
|
+
|
|
536
|
+
const LEVELS = { kind: 'chips', options: [
|
|
537
|
+
{ value: '', label: 'All', icon: 'list_alt' },
|
|
538
|
+
{ value: 'warn', label: 'Warn', icon: 'warning' },
|
|
539
|
+
{ value: 'error', label: 'Error', icon: 'error' },
|
|
540
|
+
] };
|
|
541
|
+
const entryOf = new WeakMap(); // each drawn row -> the entry it came from
|
|
542
|
+
|
|
543
|
+
const log = DataTable.mount(host, {
|
|
544
|
+
headers: ['When', 'Level', 'Who', 'What'],
|
|
545
|
+
columnTypes: ['datetime', null, null, null],
|
|
546
|
+
getRowKey: (row) => entryOf.get(row)?.id,
|
|
547
|
+
pageSize: 100,
|
|
548
|
+
sortable: true, filterable: true,
|
|
549
|
+
filterKinds: { 1: LEVELS },
|
|
550
|
+
// Rows from the server. Only the newest answer is drawn; `signal` aborts the rest.
|
|
551
|
+
dataSource: async (query, { signal }) => {
|
|
552
|
+
const [page, facets] = await Promise.all([api.audit(query, { signal }), api.facets(query)]);
|
|
553
|
+
const rows = page.data.map((e) => {
|
|
554
|
+
const row = [e.at, e.level, e.who, e.action];
|
|
555
|
+
entryOf.set(row, e);
|
|
556
|
+
return row;
|
|
557
|
+
});
|
|
558
|
+
return { rows, total: facets.total ?? null, hasMore: page.meta.has_more,
|
|
559
|
+
filterCounts: { 1: { '': facets.total, ...facets.levels } } };
|
|
560
|
+
},
|
|
561
|
+
pagerLabels: { first: 'Newest', prev: 'Newer', next: 'Older', last: 'Oldest' },
|
|
562
|
+
pagerPosition: 'bottom',
|
|
563
|
+
maxJumpOffset: 50_000,
|
|
564
|
+
follow: { interval: 10_000 },
|
|
565
|
+
onFollowChange: (on) => followButton.setAttribute('aria-pressed', String(on)),
|
|
566
|
+
rowDetail: (row) => {
|
|
567
|
+
const pre = document.createElement('pre');
|
|
568
|
+
pre.textContent = JSON.stringify(entryOf.get(row).detail, null, 2);
|
|
569
|
+
return pre;
|
|
570
|
+
},
|
|
571
|
+
hasRowDetail: (row) => !!entryOf.get(row)?.detail,
|
|
572
|
+
});
|
|
573
|
+
// Your own filters, outside the table, ask again from the first page:
|
|
574
|
+
searchBox.oninput = () => log.reload();
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**A row's detail**
|
|
578
|
+
|
|
579
|
+
| Option | What it does |
|
|
580
|
+
|---|---|
|
|
581
|
+
| `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. |
|
|
582
|
+
| `hasRowDetail(row, idx)` | Which rows have a detail. A row without one gets no toggle. |
|
|
583
|
+
| `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. |
|
|
584
|
+
| `onRowDetailToggle(idx, row, open, key)` | Called after a detail opens or closes. |
|
|
585
|
+
| `rowDetailLabel` | The toggle's accessible name (default `'Details'`). Its state is `aria-expanded`, and `aria-controls` names the detail. |
|
|
586
|
+
|
|
587
|
+
The toggle is a button, so Tab reaches it and Enter or Space opens it. It
|
|
588
|
+
neither selects the row nor opens it (`onRowActivate`). On the focused table,
|
|
589
|
+
ArrowRight opens the current row's detail and ArrowLeft closes it; the same
|
|
590
|
+
keys work on a toggle. Rows under an open detail keep their stripes. Under
|
|
591
|
+
`virtualize`, an open detail counts in its row's height, and it leaves and
|
|
592
|
+
re-enters the window with its row. It is measured when it is drawn and again
|
|
593
|
+
whenever the rows are (0.6.3: on every window update, on a resize and once the
|
|
594
|
+
column widths are fixed), so a detail that wraps differently at the final
|
|
595
|
+
widths does not leave the offsets wrong. A detail is never one of the rows the
|
|
596
|
+
row height is measured from.
|
|
597
|
+
|
|
598
|
+
**Rows from a server**
|
|
599
|
+
|
|
600
|
+
| Option | What it does |
|
|
601
|
+
|---|---|
|
|
602
|
+
| `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. |
|
|
603
|
+
| 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. |
|
|
604
|
+
| 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. |
|
|
605
|
+
| `onLoad(result, query)` | Called after each answer that was drawn. |
|
|
606
|
+
| `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. |
|
|
607
|
+
| `getQuery()`, `getPageInfo()` | The last query sent, and `{ offset, total, hasMore }` for the page drawn. |
|
|
608
|
+
| `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. |
|
|
609
|
+
|
|
610
|
+
While a request is out, the loading overlay shows and the empty state stays
|
|
611
|
+
hidden. An empty answer under a filter keeps the filter row, so the filter can
|
|
612
|
+
be cleared. A filter that is still being typed when an answer arrives keeps its
|
|
613
|
+
text, its focus and its pending request. Row indices (`onRowActivate`,
|
|
614
|
+
`getSelection`, `updateRow`) are positions in the page drawn, so they always
|
|
615
|
+
point at a row on screen. `showRowNumbers: 'position'` numbers rows from the
|
|
616
|
+
page's offset.
|
|
617
|
+
|
|
618
|
+
**A pager for an unknown total**
|
|
619
|
+
|
|
620
|
+
| Option | What it does |
|
|
621
|
+
|---|---|
|
|
622
|
+
| `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). |
|
|
623
|
+
| `hasMore` (with `onPageChange`) | The same pager for a table that pages through `onPageChange`: with `totalCount: null`, set `hasMore` to `true` or `false`. |
|
|
624
|
+
| `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`. |
|
|
625
|
+
| `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. |
|
|
626
|
+
| `pagerPosition: 'bottom'` | Puts the pager under the rows. |
|
|
627
|
+
|
|
628
|
+
An empty page after the first says `Page 4 · no rows` and keeps the pager, so
|
|
629
|
+
you can go back. A table without these options keeps the 0.6 pager.
|
|
630
|
+
|
|
631
|
+
**Pages or one scrolling list from a server: `dataSource` with `paging`**
|
|
632
|
+
|
|
633
|
+
0.6.2's `paging` and 0.7.0's `dataSource` answer two different questions, and
|
|
634
|
+
they combine. `paging` decides how the rows are shown: in pages, or as one list
|
|
635
|
+
that loads more near the end, with its strip under the body and its soft and
|
|
636
|
+
hard limits. `dataSource` decides who fetches them: the table. Without
|
|
637
|
+
`dataSource`, your code fetches, exactly as in 0.6.2. With it, the table is its
|
|
638
|
+
own `paging` consumer:
|
|
639
|
+
|
|
640
|
+
```js
|
|
641
|
+
DataTable.mount(host, {
|
|
642
|
+
headers, virtualize: true,
|
|
643
|
+
dataSource: (query, { signal }) => api.rows(query, { signal }), // { rows, total, hasMore }
|
|
644
|
+
paging: { mode: 'scroll', pageSize: 1000, softLimit: 10000, onModeChange: remember },
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
| `paging` key | Without `dataSource` (0.6.2) | With `dataSource` (0.7.0) |
|
|
649
|
+
|---|---|---|
|
|
650
|
+
| `mode`, `pageSize`, `softLimit`, `hardLimit`, `loadMorePx` | Yours | Yours. `pageSize` is also each question's `limit`. |
|
|
651
|
+
| `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. |
|
|
652
|
+
| `onPage(offset, limit)` | Yours: fetch the page | The table's: asks `{ offset, reason: 'page' }`. |
|
|
653
|
+
| `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. |
|
|
654
|
+
| `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. |
|
|
655
|
+
| `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. |
|
|
656
|
+
|
|
657
|
+
So there is one strip, one set of limits and one load trigger. With `paging`
|
|
658
|
+
the 0.7.0 pager is not drawn, and `pagerPosition` does nothing. `pagerLabels`
|
|
659
|
+
and `maxJumpOffset` apply to the strip's page buttons.
|
|
660
|
+
|
|
661
|
+
- While the source answers a sort, a filter or a refresh, the table asks for no
|
|
662
|
+
more rows. A sort or filter sent while more rows are on their way aborts that
|
|
663
|
+
request, as any newer question does. More rows do not show the loading
|
|
664
|
+
overlay; the strip shows the table is busy.
|
|
665
|
+
- More rows keep the scroll position, the selection and the open details.
|
|
666
|
+
- When the rows scrolled in prove the source holds more than `softLimit`, the
|
|
667
|
+
table is shown in pages from then on (`resolvePaging`, as in 0.6.2). The page
|
|
668
|
+
shown is the rows just loaded, so nothing is fetched again and the reader
|
|
669
|
+
carries on where they were.
|
|
670
|
+
- `refresh()` and `follow` in scroll mode read the rows held again, in one
|
|
671
|
+
question from the first row. Scrolling down to load more turns following
|
|
672
|
+
off, the way paging away does. Rows loaded only because the first ones did not
|
|
673
|
+
fill the box do not, since the reader is still at the top.
|
|
674
|
+
- `setPaging({ mode })` that changes how the rows are shown asks again from the
|
|
675
|
+
first row. Other patches only redraw the strip, as in 0.6.2.
|
|
676
|
+
- `goToPage`, `goToRow` and `getPageInfo` work as they do without `paging`,
|
|
677
|
+
in `paging.pageSize` pages.
|
|
678
|
+
|
|
679
|
+
**Refresh and follow**
|
|
680
|
+
|
|
681
|
+
| Option | What it does |
|
|
682
|
+
|---|---|
|
|
683
|
+
| `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`. |
|
|
684
|
+
| `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. |
|
|
685
|
+
| `setFollow(on)`, `isFollowing()`, `onFollowChange(on)` | Turn following on and off from your own toggle, and hear when the table turns it off. |
|
|
686
|
+
|
|
687
|
+
The follow timer holds the table weakly, so it never keeps a discarded table
|
|
688
|
+
alive, and it stops on `dispose()`.
|
|
689
|
+
|
|
690
|
+
**Filters that are not text**
|
|
691
|
+
|
|
692
|
+
| Option | What it does |
|
|
693
|
+
|---|---|
|
|
694
|
+
| `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. |
|
|
695
|
+
| 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. |
|
|
696
|
+
| 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. |
|
|
697
|
+
| `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. |
|
|
698
|
+
| `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. |
|
|
699
|
+
|
|
700
|
+
Under `dataSource`, a filter's value goes to the server as it is. Without a
|
|
701
|
+
server, the control's `match` decides which rows pass. If it has none, the
|
|
702
|
+
chips use equality (or membership, with `multiple`), a `{ from, to }` value is
|
|
703
|
+
an inclusive range over dates or numbers, a list means "one of these", and text
|
|
704
|
+
means "contains". A control is built once, and the same element is put back
|
|
705
|
+
after every render, so its own state, the focus and the text selection inside
|
|
706
|
+
it survive a redraw. It is destroyed when the columns change or the table is
|
|
707
|
+
disposed.
|
|
708
|
+
|
|
709
|
+
**Housekeeping**
|
|
710
|
+
|
|
711
|
+
| Option | What it does |
|
|
712
|
+
|---|---|
|
|
713
|
+
| `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. |
|
|
714
|
+
|
|
715
|
+
**One change that needs no option.** `autoDispose` now cancels a pending
|
|
716
|
+
dispose when the table is put back. Before 0.7.0, a table taken out, put back
|
|
717
|
+
and taken out again could be disposed by the first departure's timer.
|
|
400
718
|
|
|
401
719
|
### Scrollbars
|
|
402
720
|
|
|
@@ -875,6 +1193,14 @@ grip drag drew its ghost and a drop line naming the place, moved the step as
|
|
|
875
1193
|
one edit and started no native drag (a `dragstart` counter at 0). A refused
|
|
876
1194
|
place said why and moved nothing, and Escape cancelled the drag.
|
|
877
1195
|
|
|
1196
|
+
**In a narrow editor** (under 1000px the list, the settings and the aside are
|
|
1197
|
+
stacked and the body scrolls as one), a step picked with the mouse, with Enter,
|
|
1198
|
+
with *Go to it* or with `select()` scrolls its settings on screen (`'nearest'`)
|
|
1199
|
+
when they lie outside the body's visible box. ↑ and ↓ leave the list where it
|
|
1200
|
+
is, and side by side nothing scrolls. `demo/flow_outline_narrow_probe.mjs`
|
|
1201
|
+
pressed a step at a 900px viewport and found the panel's first field with
|
|
1202
|
+
`elementFromPoint`. At 1440px the same press moved no scroll position.
|
|
1203
|
+
|
|
878
1204
|
### The lane editor
|
|
879
1205
|
|
|
880
1206
|
`createLaneEditor(host, options)` draws a data flow as lanes, left to right.
|