flexdesk 0.6.0 → 0.6.2
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 +77 -0
- package/css/base.css +76 -0
- package/css/flexdesk.css +76 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-KH2GJTKR.js → chunk-CPYBQN3Q.js} +301 -3
- package/dist/chunk-CPYBQN3Q.js.map +7 -0
- package/dist/{chunk-CNXP3SIN.js → chunk-XYUZF5CD.js} +2 -2
- package/dist/flexdesk.css +76 -0
- package/dist/flow.js +1 -1
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +88 -2
- package/dist/widgets.js.map +3 -3
- package/package.json +1 -1
- package/src/ui/components/data_table.js +375 -3
- package/src/ui/utils/scroll_lock.js +111 -0
- package/dist/chunk-KH2GJTKR.js.map +0 -7
- /package/dist/{chunk-CNXP3SIN.js.map → chunk-XYUZF5CD.js.map} +0 -0
package/README.md
CHANGED
|
@@ -321,6 +321,83 @@ behaves as it did in 0.4, except for these:
|
|
|
321
321
|
to it if it is in a cascade layer, or has zero specificity and loads before
|
|
322
322
|
`flexdesk.css`. A rule for the cells, the rows or the table does not.
|
|
323
323
|
|
|
324
|
+
### Comparing two tables: `diff` and `createScrollLock`
|
|
325
|
+
|
|
326
|
+
A side-by-side comparison is two `DataTable`s drawn from ONE aligned row list:
|
|
327
|
+
the rows as they are on one side, as they were on the other, and a gap where
|
|
328
|
+
a side has no row. Both options are off until you set them.
|
|
329
|
+
|
|
330
|
+
```js
|
|
331
|
+
import { DataTable, createScrollLock } from '@flexdesk/widgets';
|
|
332
|
+
|
|
333
|
+
const lock = createScrollLock({ axis: 'y' }); // 'x', 'y' or 'both'
|
|
334
|
+
const side = (host, pick) => DataTable.mount(host, {
|
|
335
|
+
headers, rows: aligned.map((r) => pick(r) ?? blank), pagination: false,
|
|
336
|
+
scrollLock: lock,
|
|
337
|
+
diff: (_row, i) => ({ status: statusOf(aligned[i], pick), cells: changedCols(aligned[i]) }),
|
|
338
|
+
});
|
|
339
|
+
side(left, (r) => r.now);
|
|
340
|
+
side(right, (r) => r.then);
|
|
341
|
+
lock.setEnabled(false); // scroll each side on its own; true brings them level
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- `diff: (row, rowIdx) => {status, cells?}` draws the row
|
|
345
|
+
`twm-dt-diff twm-dt-diff--<status>`, for `added`, `removed`, `changed`,
|
|
346
|
+
`same` or `gap`, and each column index in `cells` (an array or a Set)
|
|
347
|
+
`twm-dt-diff__cell--changed`. `null` or any other status draws the row as
|
|
348
|
+
without `diff`. A `gap` row's cells are drawn empty, whatever its values and
|
|
349
|
+
`renderCell`, so the two sides keep their rows level. `updateRow` asks again.
|
|
350
|
+
- `scrollLock` takes a group from `createScrollLock()`. The table's body box
|
|
351
|
+
joins it on every render and leaves on the next render and on `dispose()`.
|
|
352
|
+
Any element can join with `lock.attach(el)`, which returns its detach. A
|
|
353
|
+
box that joins starts at the group's last position, and the echo of a
|
|
354
|
+
position the lock itself wrote moves nobody back. The lock copies pixels, so
|
|
355
|
+
it lines rows up only when both sides draw rows of the same height.
|
|
356
|
+
|
|
357
|
+
### A server's rows in pages or as one list: `paging`
|
|
358
|
+
|
|
359
|
+
`paging` (off unless you set it) shows rows that your code fetches from a
|
|
360
|
+
server either one page at a time or as one list that loads more as the reader
|
|
361
|
+
scrolls. A strip under the body shows which rows are loaded. It has the page
|
|
362
|
+
buttons or a *Load all rows* button, and a Pages / Scroll switch if you pass
|
|
363
|
+
`onModeChange`. Set `paging` and the old pager (`pagination`) is not drawn.
|
|
364
|
+
|
|
365
|
+
```js
|
|
366
|
+
new DataTable(host, {
|
|
367
|
+
headers, rows, // the rows you hold: one page, or all scrolled so far
|
|
368
|
+
virtualize: true,
|
|
369
|
+
paging: {
|
|
370
|
+
mode: 'scroll', // what was asked for: 'pages' or 'scroll'
|
|
371
|
+
pageSize: 1000,
|
|
372
|
+
softLimit: 10000, // above this many rows the table is always paged
|
|
373
|
+
total: 22521, // exact count, or null if unknown
|
|
374
|
+
atLeast: 0, // a lower bound when the count is not known
|
|
375
|
+
offset: 0, // in pages: where rows[0] is in the source
|
|
376
|
+
hasMore: true,
|
|
377
|
+
onPage: (offset, limit) => fetchPage(offset),
|
|
378
|
+
onLoadMore: () => fetchNext(), // called near the end of the scroll
|
|
379
|
+
onLoadAll: () => fetchRest(),
|
|
380
|
+
onModeChange: (mode) => remember(mode),
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Your code holds the rows and fetches them. The table decides how to show them
|
|
386
|
+
with `resolvePaging`, which is exported so your code gets the same answer when
|
|
387
|
+
it decides what to fetch. A source with more rows than `softLimit` is shown in
|
|
388
|
+
pages even if scroll was asked for. The strip says so, and Scroll is disabled
|
|
389
|
+
with the reason. `softLimit` can't be more than `hardLimit` (default and
|
|
390
|
+
ceiling `PAGING_HARD_LIMIT`, 100,000), and a scrolling table never asks for rows
|
|
391
|
+
past it. If no count is known, the rows already loaded count as evidence: a
|
|
392
|
+
table that has loaded more than `softLimit` rows and still has more is paged.
|
|
393
|
+
While scrolling, `onLoadMore` is called within 400px of the end, or after
|
|
394
|
+
render when the rows don't fill the box. It is never called when the box has
|
|
395
|
+
no height, for example in a hidden tab. A callback that returns a Promise
|
|
396
|
+
blocks the next call until it settles, and the strip shows the table busy.
|
|
397
|
+
`setPaging(patch)` redraws the strip only, for example when a count arrives.
|
|
398
|
+
In pages, `showRowNumbers: 'position'` numbers rows from `offset`. Row indexes
|
|
399
|
+
(`__rowIndex`, the selection, `onRowClick`) still refer to positions in `rows`.
|
|
400
|
+
|
|
324
401
|
### Scrollbars
|
|
325
402
|
|
|
326
403
|
`installAutoScrollbars(root)` (from `@flexdesk/widgets`) replaces the browser's
|
package/css/base.css
CHANGED
|
@@ -686,6 +686,39 @@ body.twm-dt-col-resizing {
|
|
|
686
686
|
color: var(--text-dim, #666);
|
|
687
687
|
font-style: italic;
|
|
688
688
|
}
|
|
689
|
+
/* 0.6.1 `diff` — one side of a comparison. A row that is NEW on this side is
|
|
690
|
+
* green, one that is GONE on this side red, a changed row carries an amber
|
|
691
|
+
* left edge and its changed cells an amber fill, and a GAP — the place a row
|
|
692
|
+
* holds on the other side — is hatched, so the eye reads it as "nothing here"
|
|
693
|
+
* rather than as an empty row. (0,3,2) and up, for `twm-dt-cell--null`'s
|
|
694
|
+
* reason above; a selected row keeps its own colour (`:not(.selected)`). */
|
|
695
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--added:not(.selected) > td {
|
|
696
|
+
background: color-mix(in srgb, var(--color-success, #3fb950) 16%, transparent);
|
|
697
|
+
}
|
|
698
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--removed:not(.selected) > td {
|
|
699
|
+
background: color-mix(in srgb, var(--color-danger, #c74e4e) 18%, transparent);
|
|
700
|
+
}
|
|
701
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--removed > td {
|
|
702
|
+
text-decoration: line-through;
|
|
703
|
+
text-decoration-color: color-mix(in srgb, var(--color-danger, #c74e4e) 70%, transparent);
|
|
704
|
+
}
|
|
705
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--added > td:first-child {
|
|
706
|
+
box-shadow: inset 3px 0 0 0 var(--color-success, #3fb950);
|
|
707
|
+
}
|
|
708
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--removed > td:first-child {
|
|
709
|
+
box-shadow: inset 3px 0 0 0 var(--color-danger, #c74e4e);
|
|
710
|
+
}
|
|
711
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--changed > td:first-child {
|
|
712
|
+
box-shadow: inset 3px 0 0 0 var(--color-warning, #ff9800);
|
|
713
|
+
}
|
|
714
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff:not(.selected) > td.twm-dt-diff__cell--changed {
|
|
715
|
+
background: color-mix(in srgb, var(--color-warning, #ff9800) 22%, transparent);
|
|
716
|
+
}
|
|
717
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--gap > td.twm-dt-diff__gap {
|
|
718
|
+
background: repeating-linear-gradient(135deg,
|
|
719
|
+
transparent 0 6px,
|
|
720
|
+
color-mix(in srgb, var(--text-muted, #888) 14%, transparent) 6px 7px);
|
|
721
|
+
}
|
|
689
722
|
/* `emptyState` and `setError` with no rows: the box's own words, which may
|
|
690
723
|
* wrap — muted for an empty state, red for a failure, in both densities. Quiet
|
|
691
724
|
* while loading — an empty table that is still loading is not empty. */
|
|
@@ -1615,6 +1648,49 @@ body.twm-window-maximized .twm-resize-handle {
|
|
|
1615
1648
|
opacity: 0.3;
|
|
1616
1649
|
cursor: not-allowed;
|
|
1617
1650
|
}
|
|
1651
|
+
/* `paging` — the strip UNDER a paged or scrolling DataTable's body: which rows
|
|
1652
|
+
* are held, the page buttons or Load all, why a large source is paged, and the
|
|
1653
|
+
* Pages / Scroll switch. */
|
|
1654
|
+
.twm-dt-paging {
|
|
1655
|
+
display: flex;
|
|
1656
|
+
align-items: center;
|
|
1657
|
+
gap: var(--space-md);
|
|
1658
|
+
flex: 0 0 auto;
|
|
1659
|
+
min-width: 0;
|
|
1660
|
+
padding: var(--space-xs) var(--space-md);
|
|
1661
|
+
border-top: 1px solid var(--border-subtle);
|
|
1662
|
+
font-size: var(--font-size-sm);
|
|
1663
|
+
color: var(--text-muted);
|
|
1664
|
+
white-space: nowrap;
|
|
1665
|
+
overflow: hidden;
|
|
1666
|
+
}
|
|
1667
|
+
.twm-dt-paging__info { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
1668
|
+
.twm-dt-paging__spacer { flex: 1 1 auto; }
|
|
1669
|
+
.twm-dt-paging__nav { display: inline-flex; align-items: center; gap: var(--space-xs); }
|
|
1670
|
+
.twm-dt-paging__page { min-width: 72px; text-align: center; }
|
|
1671
|
+
.twm-dt-paging .twm-pagination-btn { height: 20px; min-width: 22px; }
|
|
1672
|
+
.twm-dt-paging__note { color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; }
|
|
1673
|
+
.twm-dt-paging__all,
|
|
1674
|
+
.twm-dt-paging__mode {
|
|
1675
|
+
height: 20px;
|
|
1676
|
+
padding: 0 var(--space-md);
|
|
1677
|
+
background: transparent;
|
|
1678
|
+
border: 1px solid var(--border-default);
|
|
1679
|
+
border-radius: var(--radius-sm);
|
|
1680
|
+
color: var(--text-body);
|
|
1681
|
+
font: inherit;
|
|
1682
|
+
cursor: pointer;
|
|
1683
|
+
}
|
|
1684
|
+
.twm-dt-paging__all:hover:not(:disabled),
|
|
1685
|
+
.twm-dt-paging__mode:hover:not(:disabled) { border-color: var(--accent); color: var(--text-bright); }
|
|
1686
|
+
.twm-dt-paging__modes { display: inline-flex; }
|
|
1687
|
+
.twm-dt-paging__mode { border-radius: 0; }
|
|
1688
|
+
.twm-dt-paging__mode:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
|
|
1689
|
+
.twm-dt-paging__mode:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; margin-left: -1px; }
|
|
1690
|
+
.twm-dt-paging__mode[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--text-bright); }
|
|
1691
|
+
.twm-dt-paging__all:disabled,
|
|
1692
|
+
.twm-dt-paging__mode:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1693
|
+
.twm-dt-paging__mode[aria-pressed="true"]:disabled { opacity: 1; cursor: default; }
|
|
1618
1694
|
/* Preview Table Styles - Essential for data view */
|
|
1619
1695
|
.twm-preview-table-wrap {
|
|
1620
1696
|
overflow: auto;
|
package/css/flexdesk.css
CHANGED
|
@@ -713,6 +713,39 @@ body.twm-dt-col-resizing {
|
|
|
713
713
|
color: var(--text-dim, #666);
|
|
714
714
|
font-style: italic;
|
|
715
715
|
}
|
|
716
|
+
/* 0.6.1 `diff` — one side of a comparison. A row that is NEW on this side is
|
|
717
|
+
* green, one that is GONE on this side red, a changed row carries an amber
|
|
718
|
+
* left edge and its changed cells an amber fill, and a GAP — the place a row
|
|
719
|
+
* holds on the other side — is hatched, so the eye reads it as "nothing here"
|
|
720
|
+
* rather than as an empty row. (0,3,2) and up, for `twm-dt-cell--null`'s
|
|
721
|
+
* reason above; a selected row keeps its own colour (`:not(.selected)`). */
|
|
722
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--added:not(.selected) > td {
|
|
723
|
+
background: color-mix(in srgb, var(--color-success, #3fb950) 16%, transparent);
|
|
724
|
+
}
|
|
725
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--removed:not(.selected) > td {
|
|
726
|
+
background: color-mix(in srgb, var(--color-danger, #c74e4e) 18%, transparent);
|
|
727
|
+
}
|
|
728
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--removed > td {
|
|
729
|
+
text-decoration: line-through;
|
|
730
|
+
text-decoration-color: color-mix(in srgb, var(--color-danger, #c74e4e) 70%, transparent);
|
|
731
|
+
}
|
|
732
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--added > td:first-child {
|
|
733
|
+
box-shadow: inset 3px 0 0 0 var(--color-success, #3fb950);
|
|
734
|
+
}
|
|
735
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--removed > td:first-child {
|
|
736
|
+
box-shadow: inset 3px 0 0 0 var(--color-danger, #c74e4e);
|
|
737
|
+
}
|
|
738
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--changed > td:first-child {
|
|
739
|
+
box-shadow: inset 3px 0 0 0 var(--color-warning, #ff9800);
|
|
740
|
+
}
|
|
741
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff:not(.selected) > td.twm-dt-diff__cell--changed {
|
|
742
|
+
background: color-mix(in srgb, var(--color-warning, #ff9800) 22%, transparent);
|
|
743
|
+
}
|
|
744
|
+
.twm-data-table-component .twm-preview-table tbody tr.twm-dt-diff--gap > td.twm-dt-diff__gap {
|
|
745
|
+
background: repeating-linear-gradient(135deg,
|
|
746
|
+
transparent 0 6px,
|
|
747
|
+
color-mix(in srgb, var(--text-muted, #888) 14%, transparent) 6px 7px);
|
|
748
|
+
}
|
|
716
749
|
/* `emptyState` and `setError` with no rows: the box's own words, which may
|
|
717
750
|
* wrap — muted for an empty state, red for a failure, in both densities. Quiet
|
|
718
751
|
* while loading — an empty table that is still loading is not empty. */
|
|
@@ -1713,6 +1746,49 @@ body.twm-window-maximized .twm-resize-handle {
|
|
|
1713
1746
|
opacity: 0.3;
|
|
1714
1747
|
cursor: not-allowed;
|
|
1715
1748
|
}
|
|
1749
|
+
/* `paging` — the strip UNDER a paged or scrolling DataTable's body: which rows
|
|
1750
|
+
* are held, the page buttons or Load all, why a large source is paged, and the
|
|
1751
|
+
* Pages / Scroll switch. */
|
|
1752
|
+
.twm-dt-paging {
|
|
1753
|
+
display: flex;
|
|
1754
|
+
align-items: center;
|
|
1755
|
+
gap: var(--space-md);
|
|
1756
|
+
flex: 0 0 auto;
|
|
1757
|
+
min-width: 0;
|
|
1758
|
+
padding: var(--space-xs) var(--space-md);
|
|
1759
|
+
border-top: 1px solid var(--border-subtle);
|
|
1760
|
+
font-size: var(--font-size-sm);
|
|
1761
|
+
color: var(--text-muted);
|
|
1762
|
+
white-space: nowrap;
|
|
1763
|
+
overflow: hidden;
|
|
1764
|
+
}
|
|
1765
|
+
.twm-dt-paging__info { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
1766
|
+
.twm-dt-paging__spacer { flex: 1 1 auto; }
|
|
1767
|
+
.twm-dt-paging__nav { display: inline-flex; align-items: center; gap: var(--space-xs); }
|
|
1768
|
+
.twm-dt-paging__page { min-width: 72px; text-align: center; }
|
|
1769
|
+
.twm-dt-paging .twm-pagination-btn { height: 20px; min-width: 22px; }
|
|
1770
|
+
.twm-dt-paging__note { color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; }
|
|
1771
|
+
.twm-dt-paging__all,
|
|
1772
|
+
.twm-dt-paging__mode {
|
|
1773
|
+
height: 20px;
|
|
1774
|
+
padding: 0 var(--space-md);
|
|
1775
|
+
background: transparent;
|
|
1776
|
+
border: 1px solid var(--border-default);
|
|
1777
|
+
border-radius: var(--radius-sm);
|
|
1778
|
+
color: var(--text-body);
|
|
1779
|
+
font: inherit;
|
|
1780
|
+
cursor: pointer;
|
|
1781
|
+
}
|
|
1782
|
+
.twm-dt-paging__all:hover:not(:disabled),
|
|
1783
|
+
.twm-dt-paging__mode:hover:not(:disabled) { border-color: var(--accent); color: var(--text-bright); }
|
|
1784
|
+
.twm-dt-paging__modes { display: inline-flex; }
|
|
1785
|
+
.twm-dt-paging__mode { border-radius: 0; }
|
|
1786
|
+
.twm-dt-paging__mode:first-child { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
|
|
1787
|
+
.twm-dt-paging__mode:last-child { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; margin-left: -1px; }
|
|
1788
|
+
.twm-dt-paging__mode[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--text-bright); }
|
|
1789
|
+
.twm-dt-paging__all:disabled,
|
|
1790
|
+
.twm-dt-paging__mode:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
1791
|
+
.twm-dt-paging__mode[aria-pressed="true"]:disabled { opacity: 1; cursor: default; }
|
|
1716
1792
|
/* Preview Table Styles - Essential for data view */
|
|
1717
1793
|
.twm-preview-table-wrap {
|
|
1718
1794
|
overflow: auto;
|
package/dist/charts.js
CHANGED
|
@@ -39,10 +39,10 @@ import {
|
|
|
39
39
|
setPlotlySource,
|
|
40
40
|
uid,
|
|
41
41
|
updateChart
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-XYUZF5CD.js";
|
|
43
43
|
import {
|
|
44
44
|
createRafResizeObserver
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-CPYBQN3Q.js";
|
|
46
46
|
import {
|
|
47
47
|
ManagedWindow
|
|
48
48
|
} from "./chunk-6JSOVNID.js";
|
|
@@ -360,6 +360,35 @@ function matchDurationFilter(ms, filterText, shown) {
|
|
|
360
360
|
|
|
361
361
|
// src/ui/components/data_table.js
|
|
362
362
|
var DEFAULT_PAGE_SIZE = 100;
|
|
363
|
+
var DIFF_STATUSES = /* @__PURE__ */ new Set(["added", "removed", "changed", "same", "gap"]);
|
|
364
|
+
var PAGING_HARD_LIMIT = 1e5;
|
|
365
|
+
var PAGING_SOFT_LIMIT = 1e4;
|
|
366
|
+
function resolvePaging({
|
|
367
|
+
mode = "scroll",
|
|
368
|
+
total = null,
|
|
369
|
+
atLeast = 0,
|
|
370
|
+
held = 0,
|
|
371
|
+
hasMore = false,
|
|
372
|
+
softLimit = PAGING_SOFT_LIMIT,
|
|
373
|
+
hardLimit = PAGING_HARD_LIMIT
|
|
374
|
+
} = {}) {
|
|
375
|
+
const requested = mode === "pages" ? "pages" : "scroll";
|
|
376
|
+
const hard = Number.isFinite(hardLimit) && hardLimit >= 1 ? Math.floor(hardLimit) : PAGING_HARD_LIMIT;
|
|
377
|
+
const soft = Math.min(hard, Number.isFinite(softLimit) && softLimit >= 1 ? Math.floor(softLimit) : PAGING_SOFT_LIMIT);
|
|
378
|
+
const exact = Number.isFinite(total) && total >= 0;
|
|
379
|
+
const known = exact ? total : Math.max(Number.isFinite(atLeast) ? atLeast : 0, (held || 0) + (hasMore ? 1 : 0));
|
|
380
|
+
const forced = requested === "scroll" && known > soft;
|
|
381
|
+
return {
|
|
382
|
+
mode: forced ? "pages" : requested,
|
|
383
|
+
requested,
|
|
384
|
+
forced,
|
|
385
|
+
softLimit: soft,
|
|
386
|
+
hardLimit: hard,
|
|
387
|
+
known,
|
|
388
|
+
exact
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
var PAGING_LOAD_MORE_PX = 400;
|
|
363
392
|
var TYPED_COLUMNS = /* @__PURE__ */ new Set(["date", "datetime", "duration"]);
|
|
364
393
|
var DESELECTING_CLICKS = /* @__PURE__ */ new WeakSet();
|
|
365
394
|
var AUTO_DISPOSE = /* @__PURE__ */ new Map();
|
|
@@ -512,6 +541,9 @@ var DataTable = class {
|
|
|
512
541
|
// promise that adding the key changed no existing layout by a
|
|
513
542
|
// pixel. Only a consumer that asks for 'content' sees anything new.
|
|
514
543
|
columnFit: "container",
|
|
544
|
+
// Off unless asked for — see the typedef. With it the old pager is
|
|
545
|
+
// not drawn: the paging strip says the same and more.
|
|
546
|
+
paging: null,
|
|
515
547
|
// C10. Off unless asked for — see the typedef.
|
|
516
548
|
virtualize: false,
|
|
517
549
|
virtualRowHeight: null,
|
|
@@ -531,6 +563,8 @@ var DataTable = class {
|
|
|
531
563
|
rowClass: null,
|
|
532
564
|
rowAttrs: null,
|
|
533
565
|
cellClass: null,
|
|
566
|
+
diff: null,
|
|
567
|
+
scrollLock: null,
|
|
534
568
|
rowIcon: null,
|
|
535
569
|
emptyState: void 0,
|
|
536
570
|
nullDisplay: null,
|
|
@@ -922,7 +956,7 @@ var DataTable = class {
|
|
|
922
956
|
this._wrapperEl.style.cssText = "display:flex; flex-direction:column; height:100%; min-height:0;";
|
|
923
957
|
}
|
|
924
958
|
this._loading = false;
|
|
925
|
-
if (pagination && rows.length > 0) {
|
|
959
|
+
if (pagination && rows.length > 0 && !this._pagingConfig()) {
|
|
926
960
|
this._paginationEl = this._createPagination();
|
|
927
961
|
if (this._paginationEl) this._wrapperEl.appendChild(this._paginationEl);
|
|
928
962
|
}
|
|
@@ -1008,7 +1042,15 @@ var DataTable = class {
|
|
|
1008
1042
|
this._wrapperEl.appendChild(tableWrap);
|
|
1009
1043
|
}
|
|
1010
1044
|
this._tableWrapEl = tableWrap;
|
|
1045
|
+
this._pagingEl = null;
|
|
1046
|
+
if (this._pagingConfig()) {
|
|
1047
|
+
this._pagingEl = this._createPagingStrip();
|
|
1048
|
+
if (this._pagingEl) this._wrapperEl.appendChild(this._pagingEl);
|
|
1049
|
+
}
|
|
1011
1050
|
this.container.appendChild(this._wrapperEl);
|
|
1051
|
+
if (this.config.scrollLock && typeof this.config.scrollLock.attach === "function") {
|
|
1052
|
+
this._disposers.push(this.config.scrollLock.attach(tableWrap));
|
|
1053
|
+
}
|
|
1012
1054
|
if (this.config.autoDispose) autoDisposeWatch(this);
|
|
1013
1055
|
if (prevScroll && this._state.offset === this._renderedOffset) {
|
|
1014
1056
|
tableWrap.scrollTop = prevScroll.top;
|
|
@@ -1022,6 +1064,14 @@ var DataTable = class {
|
|
|
1022
1064
|
this._virtMeasure();
|
|
1023
1065
|
this._virtUpdate({ force: true, notify: false });
|
|
1024
1066
|
}
|
|
1067
|
+
if (this._pagingConfig()) {
|
|
1068
|
+
const onScroll = () => this._pagingMaybeLoadMore();
|
|
1069
|
+
tableWrap.addEventListener("scroll", onScroll, { passive: true });
|
|
1070
|
+
this._disposers.push(() => tableWrap.removeEventListener("scroll", onScroll));
|
|
1071
|
+
requestAnimationFrame(() => {
|
|
1072
|
+
if (this._tableWrapEl === tableWrap) this._pagingMaybeLoadMore();
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1025
1075
|
if (this._headerTableEl && this._tableEl) {
|
|
1026
1076
|
requestAnimationFrame(() => this._syncHeaderWidths());
|
|
1027
1077
|
if (this._resizeObserver) {
|
|
@@ -1884,13 +1934,234 @@ var DataTable = class {
|
|
|
1884
1934
|
btn.addEventListener("click", onClick);
|
|
1885
1935
|
return btn;
|
|
1886
1936
|
}
|
|
1937
|
+
// ── `paging` ────────────────────────────────────────────────────────────
|
|
1938
|
+
/** The `paging` config with its defaults, or null when it is not set. */
|
|
1939
|
+
_pagingConfig() {
|
|
1940
|
+
const p = this.config.paging;
|
|
1941
|
+
if (!p || typeof p !== "object") return null;
|
|
1942
|
+
const size = Number.isFinite(p.pageSize) && p.pageSize >= 1 ? Math.floor(p.pageSize) : this.config.pageSize;
|
|
1943
|
+
return {
|
|
1944
|
+
...p,
|
|
1945
|
+
pageSize: size,
|
|
1946
|
+
offset: Number.isFinite(p.offset) && p.offset > 0 ? Math.floor(p.offset) : 0,
|
|
1947
|
+
// What the table holds of the source. A consumer whose `rows` carry
|
|
1948
|
+
// a row the source does not (a blank line to type into) says so.
|
|
1949
|
+
held: Number.isFinite(p.held) ? p.held : this.config.rows?.length || 0,
|
|
1950
|
+
hasMore: !!p.hasMore,
|
|
1951
|
+
loadMorePx: Number.isFinite(p.loadMorePx) ? p.loadMorePx : PAGING_LOAD_MORE_PX
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
/**
|
|
1955
|
+
* Which way the rows are shown, and why — `resolvePaging` over this
|
|
1956
|
+
* table's facts. `null` without `paging`.
|
|
1957
|
+
*/
|
|
1958
|
+
get pagingState() {
|
|
1959
|
+
const p = this._pagingConfig();
|
|
1960
|
+
if (!p) return null;
|
|
1961
|
+
return resolvePaging({
|
|
1962
|
+
mode: p.mode,
|
|
1963
|
+
total: p.total,
|
|
1964
|
+
atLeast: p.atLeast,
|
|
1965
|
+
held: p.offset + p.held,
|
|
1966
|
+
hasMore: p.hasMore,
|
|
1967
|
+
softLimit: p.softLimit,
|
|
1968
|
+
hardLimit: p.hardLimit
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Change some `paging` facts — a count that arrived, a mode — and redraw
|
|
1973
|
+
* the STRIP only. The rows are not touched: a consumer with new rows
|
|
1974
|
+
* renders the table as it always has.
|
|
1975
|
+
*/
|
|
1976
|
+
setPaging(patch = {}) {
|
|
1977
|
+
if (!this.config.paging) return;
|
|
1978
|
+
this.config.paging = { ...this.config.paging, ...patch };
|
|
1979
|
+
this._refreshPagingStrip();
|
|
1980
|
+
this._pagingMaybeLoadMore();
|
|
1981
|
+
}
|
|
1982
|
+
_refreshPagingStrip() {
|
|
1983
|
+
if (!this._wrapperEl) return;
|
|
1984
|
+
const next = this._pagingConfig() ? this._createPagingStrip() : null;
|
|
1985
|
+
const prev = this._pagingEl;
|
|
1986
|
+
if (prev && prev.parentNode === this._wrapperEl) {
|
|
1987
|
+
if (next) prev.replaceWith(next);
|
|
1988
|
+
else prev.remove();
|
|
1989
|
+
} else if (next) {
|
|
1990
|
+
this._wrapperEl.appendChild(next);
|
|
1991
|
+
}
|
|
1992
|
+
this._pagingEl = next;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Call one of the consumer's paging callbacks. A Promise holds every other
|
|
1996
|
+
* paging call back until it settles, and the strip shows the table busy —
|
|
1997
|
+
* a second `onLoadMore` fired by the next scroll event would fetch the same
|
|
1998
|
+
* page twice.
|
|
1999
|
+
*/
|
|
2000
|
+
_pagingCall(kind, fn, ...args) {
|
|
2001
|
+
if (typeof fn !== "function" || this._pagingBusy) return;
|
|
2002
|
+
let result;
|
|
2003
|
+
try {
|
|
2004
|
+
result = fn(...args);
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
console.error(`[DataTable] paging ${kind} threw`, err);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (result && typeof result.then === "function") {
|
|
2010
|
+
this._pagingBusy = kind;
|
|
2011
|
+
this._refreshPagingStrip();
|
|
2012
|
+
const settle = () => {
|
|
2013
|
+
if (this._pagingBusy !== kind) return;
|
|
2014
|
+
this._pagingBusy = null;
|
|
2015
|
+
if (this._pagingEl?.isConnected) this._refreshPagingStrip();
|
|
2016
|
+
};
|
|
2017
|
+
result.then(settle, (err) => {
|
|
2018
|
+
console.error(`[DataTable] paging ${kind} failed`, err);
|
|
2019
|
+
settle();
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Scrolling: ask for the next rows when the body is near its end — or does
|
|
2025
|
+
* not fill its box, which no scroll can ever change. Never past the hard
|
|
2026
|
+
* limit, never in pages, never with no layout (a hidden table's box is 0px
|
|
2027
|
+
* tall, and "0 ≤ 0" would ask for the whole source).
|
|
2028
|
+
*/
|
|
2029
|
+
_pagingMaybeLoadMore() {
|
|
2030
|
+
const p = this._pagingConfig();
|
|
2031
|
+
if (!p || !p.hasMore || this._pagingBusy) return;
|
|
2032
|
+
const state = this.pagingState;
|
|
2033
|
+
if (state.mode !== "scroll" || p.held >= state.hardLimit) return;
|
|
2034
|
+
const wrap = this._tableWrapEl;
|
|
2035
|
+
if (!wrap?.isConnected || !(wrap.clientHeight > 0)) return;
|
|
2036
|
+
const remaining = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight;
|
|
2037
|
+
if (remaining >= p.loadMorePx) return;
|
|
2038
|
+
this._pagingCall("more", p.onLoadMore);
|
|
2039
|
+
}
|
|
2040
|
+
/** The strip under the body, or null when it would say nothing. */
|
|
2041
|
+
_createPagingStrip() {
|
|
2042
|
+
const p = this._pagingConfig();
|
|
2043
|
+
const state = this.pagingState;
|
|
2044
|
+
const { mode, forced, exact, known } = state;
|
|
2045
|
+
const busy = this._pagingBusy;
|
|
2046
|
+
const n = (x) => Number(x).toLocaleString();
|
|
2047
|
+
const pageSize = p.pageSize;
|
|
2048
|
+
const held = p.held;
|
|
2049
|
+
const total = exact ? p.total : null;
|
|
2050
|
+
const morePages = p.offset > 0 || p.hasMore || exact && total > held;
|
|
2051
|
+
if (!forced && mode === "scroll" && !p.hasMore && held <= pageSize) return null;
|
|
2052
|
+
if (!forced && mode === "pages" && !morePages) return null;
|
|
2053
|
+
const el = document.createElement("div");
|
|
2054
|
+
el.className = `twm-dt-paging twm-dt-paging--${mode}`;
|
|
2055
|
+
el.setAttribute("role", "navigation");
|
|
2056
|
+
el.setAttribute("aria-label", "Rows");
|
|
2057
|
+
if (busy) el.classList.add("twm-dt-paging--busy");
|
|
2058
|
+
const info = document.createElement("span");
|
|
2059
|
+
info.className = "twm-dt-paging__info";
|
|
2060
|
+
el.appendChild(info);
|
|
2061
|
+
if (mode === "pages") {
|
|
2062
|
+
const from = held ? p.offset + 1 : p.offset;
|
|
2063
|
+
const to = p.offset + held;
|
|
2064
|
+
let of = "";
|
|
2065
|
+
if (exact) of = ` of ${n(total)}`;
|
|
2066
|
+
else if (known > to) of = ` of more than ${n(known - 1)}`;
|
|
2067
|
+
info.textContent = held ? `Rows ${n(from)}\u2013${n(to)}${of}` : "No rows on this page";
|
|
2068
|
+
const hasPrev = p.offset > 0;
|
|
2069
|
+
const hasNext = exact ? p.offset + held < total : p.hasMore;
|
|
2070
|
+
const lastOffset = exact && total > 0 ? Math.floor((total - 1) / pageSize) * pageSize : null;
|
|
2071
|
+
const go = (offset) => (ev) => {
|
|
2072
|
+
if (!ev?.currentTarget?.disabled) this._pagingCall("page", p.onPage, offset, pageSize);
|
|
2073
|
+
};
|
|
2074
|
+
const nav = document.createElement("span");
|
|
2075
|
+
nav.className = "twm-dt-paging__nav";
|
|
2076
|
+
const off = !!busy || typeof p.onPage !== "function";
|
|
2077
|
+
nav.append(
|
|
2078
|
+
this._createPaginationBtn("first_page", "First page", off || !hasPrev, go(0)),
|
|
2079
|
+
this._createPaginationBtn(
|
|
2080
|
+
"chevron_left",
|
|
2081
|
+
"Previous page",
|
|
2082
|
+
off || !hasPrev,
|
|
2083
|
+
go(Math.max(0, p.offset - pageSize))
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2086
|
+
const page = document.createElement("span");
|
|
2087
|
+
page.className = "twm-dt-paging__page";
|
|
2088
|
+
const current = Math.floor(p.offset / pageSize) + 1;
|
|
2089
|
+
page.textContent = exact ? `Page ${n(current)} of ${n(Math.max(1, Math.ceil(total / pageSize)))}` : `Page ${n(current)}`;
|
|
2090
|
+
nav.append(
|
|
2091
|
+
page,
|
|
2092
|
+
this._createPaginationBtn(
|
|
2093
|
+
"chevron_right",
|
|
2094
|
+
"Next page",
|
|
2095
|
+
off || !hasNext,
|
|
2096
|
+
go(p.offset + pageSize)
|
|
2097
|
+
),
|
|
2098
|
+
// The last page needs the exact count: without it there is no
|
|
2099
|
+
// offset to go to, and the button says so by being off.
|
|
2100
|
+
this._createPaginationBtn(
|
|
2101
|
+
"last_page",
|
|
2102
|
+
lastOffset === null ? "Last page (the total is not known)" : "Last page",
|
|
2103
|
+
off || !hasNext || lastOffset === null,
|
|
2104
|
+
go(lastOffset)
|
|
2105
|
+
)
|
|
2106
|
+
);
|
|
2107
|
+
el.appendChild(nav);
|
|
2108
|
+
} else {
|
|
2109
|
+
if (exact && total > held) info.textContent = `${n(held)} of ${n(total)} rows`;
|
|
2110
|
+
else if (p.hasMore) info.textContent = `${n(held)} rows, more to come`;
|
|
2111
|
+
else info.textContent = `${n(held)} rows`;
|
|
2112
|
+
if (p.hasMore && typeof p.onLoadAll === "function") {
|
|
2113
|
+
const all = document.createElement("button");
|
|
2114
|
+
all.type = "button";
|
|
2115
|
+
all.className = "twm-dt-paging__all";
|
|
2116
|
+
all.textContent = busy === "all" ? "Loading\u2026" : exact ? `Load all ${n(total)} rows` : "Load all rows";
|
|
2117
|
+
all.disabled = !!busy;
|
|
2118
|
+
all.addEventListener("click", () => {
|
|
2119
|
+
if (!all.disabled) this._pagingCall("all", p.onLoadAll);
|
|
2120
|
+
});
|
|
2121
|
+
el.appendChild(all);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
const spacer = document.createElement("span");
|
|
2125
|
+
spacer.className = "twm-dt-paging__spacer";
|
|
2126
|
+
el.appendChild(spacer);
|
|
2127
|
+
const why = forced ? `More than ${n(state.softLimit)} rows, so they are shown in pages.` : null;
|
|
2128
|
+
if (why) {
|
|
2129
|
+
const note = document.createElement("span");
|
|
2130
|
+
note.className = "twm-dt-paging__note";
|
|
2131
|
+
note.textContent = why;
|
|
2132
|
+
el.appendChild(note);
|
|
2133
|
+
}
|
|
2134
|
+
if (typeof p.onModeChange === "function") {
|
|
2135
|
+
const group = document.createElement("span");
|
|
2136
|
+
group.className = "twm-dt-paging__modes";
|
|
2137
|
+
group.setAttribute("role", "group");
|
|
2138
|
+
group.setAttribute("aria-label", "Show rows");
|
|
2139
|
+
for (const [value, label] of [["pages", "Pages"], ["scroll", "Scroll"]]) {
|
|
2140
|
+
const b = document.createElement("button");
|
|
2141
|
+
b.type = "button";
|
|
2142
|
+
b.className = "twm-dt-paging__mode";
|
|
2143
|
+
b.dataset.mode = value;
|
|
2144
|
+
b.textContent = label;
|
|
2145
|
+
const pressed = mode === value;
|
|
2146
|
+
b.setAttribute("aria-pressed", String(pressed));
|
|
2147
|
+
b.disabled = !!busy || value === "scroll" && forced;
|
|
2148
|
+
if (value === "scroll" && forced) b.title = why;
|
|
2149
|
+
b.addEventListener("click", () => {
|
|
2150
|
+
if (!b.disabled && mode !== value) this._pagingCall("mode", p.onModeChange, value);
|
|
2151
|
+
});
|
|
2152
|
+
group.appendChild(b);
|
|
2153
|
+
}
|
|
2154
|
+
el.appendChild(group);
|
|
2155
|
+
}
|
|
2156
|
+
return el;
|
|
2157
|
+
}
|
|
1887
2158
|
_createTable() {
|
|
1888
2159
|
const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon, onPageChange, offset: configOffset } = this.config;
|
|
1889
2160
|
const { sortColumn, sortAscending } = this._state;
|
|
1890
2161
|
const isServerSide = typeof onPageChange === "function";
|
|
1891
2162
|
const processedRows = isServerSide ? rows : this._getProcessedRows();
|
|
1892
2163
|
const offset = isServerSide ? configOffset || 0 : this._state.offset;
|
|
1893
|
-
const pageRows = isServerSide || !this.config.pagination ? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
2164
|
+
const pageRows = isServerSide || !this.config.pagination || this._pagingConfig() ? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
1894
2165
|
const table = document.createElement("table");
|
|
1895
2166
|
table.className = this._tableClassName();
|
|
1896
2167
|
table.tabIndex = 0;
|
|
@@ -1953,6 +2224,8 @@ var DataTable = class {
|
|
|
1953
2224
|
return isServerSide ? (configOffset || 0) + localIdx : this._processedIndexMap?.[processedIdx] ?? processedIdx;
|
|
1954
2225
|
};
|
|
1955
2226
|
this._positionBase = isServerSide ? configOffset || 0 : this.config.pagination ? offset : 0;
|
|
2227
|
+
const paged = this._pagingConfig();
|
|
2228
|
+
if (paged && Number.isFinite(paged.offset)) this._positionBase += paged.offset;
|
|
1956
2229
|
const tbody = document.createElement("tbody");
|
|
1957
2230
|
table.appendChild(tbody);
|
|
1958
2231
|
this._tbodyEl = tbody;
|
|
@@ -2411,11 +2684,20 @@ var DataTable = class {
|
|
|
2411
2684
|
tr.appendChild(td);
|
|
2412
2685
|
}
|
|
2413
2686
|
const { cellClass, nullDisplay } = this.config;
|
|
2687
|
+
const diff = this._diffOf(row, globalIdx);
|
|
2688
|
+
const gap = diff?.status === "gap";
|
|
2689
|
+
const changedCells = diff?.cells ? diff.cells instanceof Set ? diff.cells : new Set(diff.cells) : null;
|
|
2414
2690
|
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
|
2415
2691
|
const td = document.createElement("td");
|
|
2416
2692
|
td.className = this._columnTypes[colIdx] || "text";
|
|
2417
2693
|
const value = row[colIdx];
|
|
2418
2694
|
let handled = false;
|
|
2695
|
+
if (gap) {
|
|
2696
|
+
td.classList.add("twm-dt-diff__gap");
|
|
2697
|
+
tr.appendChild(td);
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
if (changedCells?.has(colIdx)) td.classList.add("twm-dt-diff__cell--changed");
|
|
2419
2701
|
if (this.config.renderCell) {
|
|
2420
2702
|
handled = !!this.config.renderCell(td, value, colIdx, globalIdx, row);
|
|
2421
2703
|
if (!handled) {
|
|
@@ -2682,6 +2964,14 @@ var DataTable = class {
|
|
|
2682
2964
|
}
|
|
2683
2965
|
return null;
|
|
2684
2966
|
}
|
|
2967
|
+
/** 0.6.1. The `diff` answer for one row, or `null` when there is none or
|
|
2968
|
+
* its status is not one this component draws. */
|
|
2969
|
+
_diffOf(row, rowIdx) {
|
|
2970
|
+
if (typeof this.config.diff !== "function") return null;
|
|
2971
|
+
const answer = this.config.diff(row, rowIdx);
|
|
2972
|
+
if (!answer || !DIFF_STATUSES.has(answer.status)) return null;
|
|
2973
|
+
return answer;
|
|
2974
|
+
}
|
|
2685
2975
|
/** Everything `rowClass`, `rowAttrs` and the open mark put on a `<tr>`,
|
|
2686
2976
|
* taken off again first — so `updateRow` cannot leave last state's class
|
|
2687
2977
|
* behind. */
|
|
@@ -2696,6 +2986,11 @@ var DataTable = class {
|
|
|
2696
2986
|
const added = this._addClasses(tr, rowClass(row, rowIdx));
|
|
2697
2987
|
tr.__twmClasses = added.filter((c) => c !== "data-preview-row" && c !== "selected");
|
|
2698
2988
|
}
|
|
2989
|
+
for (const c of [...tr.classList]) {
|
|
2990
|
+
if (c === "twm-dt-diff" || c.startsWith("twm-dt-diff--")) tr.classList.remove(c);
|
|
2991
|
+
}
|
|
2992
|
+
const diff = this._diffOf(row, rowIdx);
|
|
2993
|
+
if (diff) tr.classList.add("twm-dt-diff", `twm-dt-diff--${diff.status}`);
|
|
2699
2994
|
if (tr.__twmAttrs) {
|
|
2700
2995
|
for (const name of tr.__twmAttrs) tr.removeAttribute(name);
|
|
2701
2996
|
tr.__twmAttrs = null;
|
|
@@ -3697,6 +3992,9 @@ export {
|
|
|
3697
3992
|
formatDuration,
|
|
3698
3993
|
matchDateFilter,
|
|
3699
3994
|
matchDurationFilter,
|
|
3995
|
+
PAGING_HARD_LIMIT,
|
|
3996
|
+
PAGING_SOFT_LIMIT,
|
|
3997
|
+
resolvePaging,
|
|
3700
3998
|
DataTable
|
|
3701
3999
|
};
|
|
3702
|
-
//# sourceMappingURL=chunk-
|
|
4000
|
+
//# sourceMappingURL=chunk-CPYBQN3Q.js.map
|