flexdesk 0.4.5 → 0.4.6
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/css/base.css +57 -12
- package/css/flexdesk.css +75 -18
- package/css/overrides.css +15 -5
- package/dist/charts.js +2 -2
- package/dist/{chunk-BIAOGX6K.js → chunk-CYTWMP2Y.js} +569 -43
- package/dist/chunk-CYTWMP2Y.js.map +7 -0
- package/dist/{chunk-SWB5ARKN.js → chunk-HWCX5I4R.js} +2 -2
- package/dist/{chunk-QNQHQ24V.js → chunk-NLTOY57M.js} +72 -1
- package/dist/chunk-NLTOY57M.js.map +7 -0
- package/dist/editor.js +1 -1
- package/dist/flexdesk.css +75 -18
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +1 -1
- package/dist/wm.js +1 -1
- package/package.json +1 -1
- package/src/editor/notebook_tab_bar.js +89 -0
- package/src/ui/components/data_table.js +635 -48
- package/dist/chunk-BIAOGX6K.js.map +0 -7
- package/dist/chunk-QNQHQ24V.js.map +0 -7
- /package/dist/{chunk-SWB5ARKN.js.map → chunk-HWCX5I4R.js.map} +0 -0
|
@@ -65,8 +65,16 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
65
65
|
* @property {Object} [host] - A Host (see ui/js/host/host.js). Its `dialogs` capability backs `downloadCSV()`'s native save dialog; without it CSV export falls back to a Blob download.
|
|
66
66
|
* @property {Object} [stateStore] - `{ ready(), get(key), set(key, state) }` — REQUIRED when `persistKey` is set. Built by `createTableStateStore()`.
|
|
67
67
|
* @property {'container'|'content'} [columnFit='container'] - How the automatic fit pass sizes a column the user has NOT dragged. 'container' squeezes every column into the wrap: a 360px cap, a more generous one for the first column, and water-fill-shrink toward the 40px floor when they do not fit. 'content' sizes each column to its own content through the same `_autoWidth` a grip double-click uses (520px cap) and lets the body scroll sideways rather than squeezing. A pinned column is honoured verbatim under both.
|
|
68
|
+
* @property {boolean|{overscan?: number, rowHeight?: number}} [virtualize=false] - C10. Draw only the rows near the viewport, between a top and a bottom spacer row, and give the body table a `<colgroup>` so its column widths no longer come from whichever row happens to be first. OFF by default: nothing about a table that does not ask changes. `overscan` (rows drawn beyond each edge, default 20) and `rowHeight` (the estimate used until a real row has been measured, default 24) tune it. See `_virtUpdate` for the whole contract.
|
|
69
|
+
* @property {Function} [virtualRowHeight] - `(rowIndex, baseHeight) => number`. Under `virtualize`, the height a row occupies, including anything the embedder injects in front of it (a group header) — and 0 for a row the embedder hides, which is then not drawn at all. Absent: every row is `baseHeight`, which is measured from a real row.
|
|
70
|
+
* @property {Function} [onRowsRendered] - `({ rows, removed }) => void`. Under `virtualize`, called whenever the window changes after `render()` returned: `rows` are the `<tr>`s the virtualiser ADDED — the rows an embedder that decorates rows after a render would otherwise never see — and `removed` the ones it took OUT, already detached (an embedder that injected rows of its own beside them, or holds an editor in one, has to hear that they went). The rows present when `render()` returns are not reported: they are the render's, exactly as without `virtualize`.
|
|
68
71
|
*/
|
|
69
72
|
|
|
73
|
+
/** C10. When the scroll box has no layout — mounted hidden, or a DOM with no
|
|
74
|
+
* layout engine — there is no viewport to measure, so the window is sized as if
|
|
75
|
+
* it were this tall. A ResizeObserver corrects it the moment a real size exists. */
|
|
76
|
+
const VIRTUAL_FALLBACK_VIEWPORT_PX = 1000;
|
|
77
|
+
|
|
70
78
|
export class DataTable {
|
|
71
79
|
/**
|
|
72
80
|
* Create a DataTable instance
|
|
@@ -122,9 +130,19 @@ export class DataTable {
|
|
|
122
130
|
// promise that adding the key changed no existing layout by a
|
|
123
131
|
// pixel. Only a consumer that asks for 'content' sees anything new.
|
|
124
132
|
columnFit: 'container',
|
|
133
|
+
// C10. Off unless asked for — see the typedef.
|
|
134
|
+
virtualize: false,
|
|
135
|
+
virtualRowHeight: null,
|
|
136
|
+
onRowsRendered: null,
|
|
125
137
|
...config,
|
|
126
138
|
};
|
|
127
139
|
|
|
140
|
+
// C10. The render window, or null when this table draws every row.
|
|
141
|
+
this._virt = null;
|
|
142
|
+
// The measured row height outlives a render, so a re-render does not
|
|
143
|
+
// start again from the estimate and jump once it has re-measured.
|
|
144
|
+
this._virtBase = null;
|
|
145
|
+
|
|
128
146
|
// A persistKey with nowhere to persist is a wiring bug, not a
|
|
129
147
|
// degraded mode. Fail loud at construction: a silent no-op here
|
|
130
148
|
// reads as "my filters randomly stopped sticking" months later.
|
|
@@ -418,9 +436,18 @@ export class DataTable {
|
|
|
418
436
|
// navigation silently died the first time the store moved.
|
|
419
437
|
const doc = this.container.ownerDocument;
|
|
420
438
|
const hadTableFocus = !!this._tableEl && doc?.activeElement === this._tableEl;
|
|
439
|
+
// SCROLL POSITION SURVIVES A RE-RENDER, for the same reason: the scroll
|
|
440
|
+
// box is rebuilt too, and a new one starts at the top. Folding a tree
|
|
441
|
+
// row, or any refresh, threw a reader scrolled halfway down back to row
|
|
442
|
+
// one. Only on the SAME page — turning the page should start at its top.
|
|
443
|
+
const prevWrap = this._tableWrapEl;
|
|
444
|
+
const prevScroll = prevWrap?.isConnected && this._renderedOffset === this._state.offset
|
|
445
|
+
? { top: prevWrap.scrollTop, left: prevWrap.scrollLeft } : null;
|
|
421
446
|
|
|
422
447
|
this._cleanup();
|
|
423
448
|
this.container.innerHTML = '';
|
|
449
|
+
// C10. A window belongs to one tbody; `_createTable` builds a new one.
|
|
450
|
+
this._virt = null;
|
|
424
451
|
|
|
425
452
|
const { rows, pagination, emptyMessage } = this.config;
|
|
426
453
|
|
|
@@ -552,6 +579,24 @@ export class DataTable {
|
|
|
552
579
|
|
|
553
580
|
this._tableWrapEl = tableWrap;
|
|
554
581
|
this.container.appendChild(this._wrapperEl);
|
|
582
|
+
if (prevScroll && this._state.offset === this._renderedOffset) {
|
|
583
|
+
tableWrap.scrollTop = prevScroll.top;
|
|
584
|
+
tableWrap.scrollLeft = prevScroll.left;
|
|
585
|
+
}
|
|
586
|
+
this._renderedOffset = this._state.offset;
|
|
587
|
+
|
|
588
|
+
// C10. NOW THERE IS A VIEWPORT. `_createTable` drew a window sized from
|
|
589
|
+
// an estimate, before the scroll box existed; measure a real row, and
|
|
590
|
+
// redraw the window for the real height and the restored scroll offset.
|
|
591
|
+
// Synchronously, and not reported through `onRowsRendered`: a row present
|
|
592
|
+
// when `render()` returns is the render's, as it is without `virtualize`.
|
|
593
|
+
if (this._virt) {
|
|
594
|
+
const onScroll = () => this._virtUpdate();
|
|
595
|
+
tableWrap.addEventListener('scroll', onScroll, { passive: true });
|
|
596
|
+
this._disposers.push(() => tableWrap.removeEventListener('scroll', onScroll));
|
|
597
|
+
this._virtMeasure();
|
|
598
|
+
this._virtUpdate({ force: true, notify: false });
|
|
599
|
+
}
|
|
555
600
|
|
|
556
601
|
// Sync header column widths to body after layout. Two-pass: the
|
|
557
602
|
// first frame lets the browser settle natural widths from the
|
|
@@ -566,8 +611,11 @@ export class DataTable {
|
|
|
566
611
|
try { this._resizeObserver.disconnect(); } catch (_) {}
|
|
567
612
|
}
|
|
568
613
|
if (typeof ResizeObserver !== 'undefined') {
|
|
569
|
-
|
|
570
|
-
|
|
614
|
+
// C10: a new height is a new window, as well as new widths.
|
|
615
|
+
this._resizeObserver = createRafResizeObserver(() => {
|
|
616
|
+
this._syncHeaderWidths();
|
|
617
|
+
this._virtUpdate();
|
|
618
|
+
});
|
|
571
619
|
this._resizeObserver.observe(this._wrapperEl);
|
|
572
620
|
}
|
|
573
621
|
// The header lives in its own non-scrolling wrap (so it can't
|
|
@@ -725,6 +773,8 @@ export class DataTable {
|
|
|
725
773
|
this._tableWrapEl = null;
|
|
726
774
|
this._processedRows = null;
|
|
727
775
|
this._processedIndexMap = null;
|
|
776
|
+
this._virt = null;
|
|
777
|
+
this._colgroupEl = null;
|
|
728
778
|
}
|
|
729
779
|
|
|
730
780
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -1470,7 +1520,7 @@ export class DataTable {
|
|
|
1470
1520
|
|
|
1471
1521
|
_createTable() {
|
|
1472
1522
|
const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon, onPageChange, offset: configOffset } = this.config;
|
|
1473
|
-
const {
|
|
1523
|
+
const { sortColumn, sortAscending } = this._state;
|
|
1474
1524
|
|
|
1475
1525
|
// Server-side pagination: rows already represent current page, use config offset for row numbering
|
|
1476
1526
|
// Client-side pagination: use processed (filtered+sorted) rows, then slice for current page
|
|
@@ -1550,46 +1600,486 @@ export class DataTable {
|
|
|
1550
1600
|
thead.appendChild(filterRow);
|
|
1551
1601
|
}
|
|
1552
1602
|
|
|
1603
|
+
// C10. THE COLUMN TRACK IS A `<colgroup>`, NOT THE FIRST ROW. Under
|
|
1604
|
+
// `table-layout: fixed` a `<col>` with a width decides its column, and a
|
|
1605
|
+
// first-row cell only decides one whose `<col>` is `auto` — so once the
|
|
1606
|
+
// widths live here, it stops mattering which row is first. A recycling
|
|
1607
|
+
// window changes that row on every scroll; without this the columns
|
|
1608
|
+
// would jitter each time it did. Before the `thead`, where the content
|
|
1609
|
+
// model puts it; the `thead` leaves for the header table in `render()`
|
|
1610
|
+
// and the colgroup stays with the body.
|
|
1611
|
+
const virtual = this._isVirtual();
|
|
1612
|
+
if (virtual) {
|
|
1613
|
+
table.classList.add('twm-dt--virtual');
|
|
1614
|
+
table.appendChild(this._createColgroup());
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1553
1617
|
table.appendChild(thead);
|
|
1554
1618
|
|
|
1555
|
-
//
|
|
1556
|
-
const
|
|
1557
|
-
pageRows.forEach((row, localIdx) => {
|
|
1619
|
+
// Map back to original config.rows index for selection tracking
|
|
1620
|
+
const globalOf = (localIdx) => {
|
|
1558
1621
|
const processedIdx = offset + localIdx;
|
|
1559
|
-
|
|
1560
|
-
const globalIdx = isServerSide
|
|
1622
|
+
return isServerSide
|
|
1561
1623
|
? (configOffset || 0) + localIdx
|
|
1562
1624
|
: (this._processedIndexMap?.[processedIdx] ?? processedIdx);
|
|
1563
|
-
|
|
1564
|
-
tr.__rowIndex = globalIdx;
|
|
1565
|
-
tr.className = 'data-preview-row';
|
|
1625
|
+
};
|
|
1566
1626
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1627
|
+
// Body
|
|
1628
|
+
const tbody = document.createElement('tbody');
|
|
1629
|
+
table.appendChild(tbody);
|
|
1630
|
+
this._tbodyEl = tbody;
|
|
1570
1631
|
|
|
1571
|
-
|
|
1632
|
+
if (virtual) {
|
|
1633
|
+
this._virtInit(tbody, pageRows, globalOf, {
|
|
1634
|
+
serverSide: isServerSide, offset, configOffset: configOffset || 0,
|
|
1635
|
+
});
|
|
1636
|
+
} else {
|
|
1637
|
+
pageRows.forEach((row, localIdx) => {
|
|
1638
|
+
tbody.appendChild(this._buildBodyRow(row, globalOf(localIdx), localIdx));
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1572
1641
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1642
|
+
return table;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/** One body row, fully built. The ONE place a `<tr>` for a row is made, so
|
|
1646
|
+
* the full render and the virtual window cannot drift apart: a row drawn
|
|
1647
|
+
* on scroll is the same row a render would have drawn. */
|
|
1648
|
+
_buildBodyRow(row, globalIdx, localIdx) {
|
|
1649
|
+
const { showRowNumbers } = this.config;
|
|
1650
|
+
const tr = document.createElement('tr');
|
|
1651
|
+
tr.__rowIndex = globalIdx;
|
|
1652
|
+
tr.className = 'data-preview-row';
|
|
1653
|
+
|
|
1654
|
+
if (this._state.selected.has(globalIdx)) {
|
|
1655
|
+
tr.classList.add('selected');
|
|
1656
|
+
}
|
|
1657
|
+
// C10. The stripe is `tr:nth-child(even)`, which is a question about a
|
|
1658
|
+
// row's POSITION among its siblings — and a window that drops rows off
|
|
1659
|
+
// its top changes every answer. `_virtSpacers` keeps the position's
|
|
1660
|
+
// parity equal to the row's own, so the stripe needs nothing here; the
|
|
1661
|
+
// class is for an embedder that wants to ask about the row itself.
|
|
1662
|
+
if (this._virt) tr.classList.toggle('twm-dt-row--alt', localIdx % 2 === 1);
|
|
1663
|
+
|
|
1664
|
+
this._fillRowCells(tr, row, globalIdx, showRowNumbers);
|
|
1665
|
+
|
|
1666
|
+
// Row-level click + right-click hooks (P2). Bound after
|
|
1667
|
+
// cells so per-cell handlers run first.
|
|
1668
|
+
if (this.config.onRowClick) {
|
|
1669
|
+
tr.addEventListener('click', (ev) => {
|
|
1670
|
+
this.config.onRowClick(globalIdx, row, ev);
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
if (this.config.onRowContextMenu) {
|
|
1674
|
+
tr.addEventListener('contextmenu', (ev) => {
|
|
1675
|
+
this.config.onRowContextMenu(globalIdx, row, ev);
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
return tr;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1682
|
+
// C10 — the virtual window
|
|
1683
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1684
|
+
//
|
|
1685
|
+
// ══ WHAT IT IS ═══════════════════════════════════════════════════
|
|
1686
|
+
//
|
|
1687
|
+
// Only the rows near the viewport are in the DOM: a TOP SPACER row whose
|
|
1688
|
+
// height is every row above the window, the window's rows, and a BOTTOM
|
|
1689
|
+
// SPACER for everything below it. The scrollbar is therefore sized to the
|
|
1690
|
+
// whole table and every row is reachable, while the DOM stays bounded by
|
|
1691
|
+
// the viewport — a thousand-row page (or fifty of them) costs the same
|
|
1692
|
+
// few dozen `<tr>`s as one screenful.
|
|
1693
|
+
//
|
|
1694
|
+
// ══ WHAT IT PROMISES ═════════════════════════════════════════════
|
|
1695
|
+
//
|
|
1696
|
+
// - A row that stays in the window is NEVER MOVED OR REBUILT when the
|
|
1697
|
+
// window slides. Rows leave from the edges and arrive at the edges; the
|
|
1698
|
+
// ones in the middle are the same nodes. An open editor, a focused
|
|
1699
|
+
// cell, a hover — anything living in a row that is still on screen —
|
|
1700
|
+
// survives a scroll. (Moving a node that holds focus blurs it, which in
|
|
1701
|
+
// an editable grid is a commit nobody asked for.)
|
|
1702
|
+
// - Column widths come from a `<colgroup>`, so the first row changing
|
|
1703
|
+
// moves nothing (see `_createTable`).
|
|
1704
|
+
// - A spacer is in the DOM only while it has a height. A table that fits
|
|
1705
|
+
// in its window has exactly the DOM it had without `virtualize`.
|
|
1706
|
+
// - `onRowsRendered` hears about every row added after `render()`, and
|
|
1707
|
+
// every row taken out.
|
|
1708
|
+
//
|
|
1709
|
+
// ══ WHAT IT ASSUMES ══════════════════════════════════════════════
|
|
1710
|
+
//
|
|
1711
|
+
// That a row's height is knowable without drawing it: `baseHeight`,
|
|
1712
|
+
// measured from a real row, or what `virtualRowHeight` says. A row that
|
|
1713
|
+
// wraps to two lines is drawn correctly and is simply mis-counted in the
|
|
1714
|
+
// spacer arithmetic by the difference — the overscan absorbs that.
|
|
1715
|
+
|
|
1716
|
+
_isVirtual() {
|
|
1717
|
+
return !!this.config.virtualize;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
_virtOptions() {
|
|
1721
|
+
const v = this.config.virtualize;
|
|
1722
|
+
const o = v && typeof v === 'object' ? v : {};
|
|
1723
|
+
return {
|
|
1724
|
+
overscan: Number.isFinite(o.overscan) && o.overscan >= 0 ? Math.floor(o.overscan) : 20,
|
|
1725
|
+
rowHeight: Number.isFinite(o.rowHeight) && o.rowHeight > 0 ? o.rowHeight : 24,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/** One `<col>` per DOM column (the row-number column included), carrying
|
|
1730
|
+
* any width already pinned for it, so a re-render starts at the widths it
|
|
1731
|
+
* had rather than at a content-sized frame that then snaps. */
|
|
1732
|
+
_createColgroup() {
|
|
1733
|
+
const n = (this.config.headers?.length || 0) + (this.config.showRowNumbers ? 1 : 0);
|
|
1734
|
+
const colgroup = document.createElement('colgroup');
|
|
1735
|
+
for (let i = 0; i < n; i++) {
|
|
1736
|
+
const col = document.createElement('col');
|
|
1737
|
+
const w = this._colWidths?.[i];
|
|
1738
|
+
if (w != null && this._colWidthsSig === this._colSig()) col.style.width = `${w}px`;
|
|
1739
|
+
colgroup.appendChild(col);
|
|
1740
|
+
}
|
|
1741
|
+
this._colgroupEl = colgroup;
|
|
1742
|
+
return colgroup;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/** The `<col>`s, when this table has them. */
|
|
1746
|
+
_cols() {
|
|
1747
|
+
return this._virt && this._colgroupEl ? this._colgroupEl.children : null;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
_virtInit(tbody, rows, globalOf, { serverSide, offset, configOffset }) {
|
|
1751
|
+
const { overscan, rowHeight } = this._virtOptions();
|
|
1752
|
+
const spacer = (edge) => {
|
|
1753
|
+
const tr = document.createElement('tr');
|
|
1754
|
+
tr.className = `twm-dt-spacer twm-dt-spacer--${edge}`;
|
|
1755
|
+
tr.setAttribute('aria-hidden', 'true');
|
|
1756
|
+
const td = document.createElement('td');
|
|
1757
|
+
td.colSpan = Math.max(1, (this.config.headers?.length || 0)
|
|
1758
|
+
+ (this.config.showRowNumbers ? 1 : 0));
|
|
1759
|
+
// INLINE, because it must beat every consumer's cell padding and
|
|
1760
|
+
// border at any specificity: a spacer that is 1px taller than its
|
|
1761
|
+
// arithmetic moves every row below it by that pixel.
|
|
1762
|
+
td.style.cssText = 'padding:0;border:0;height:0;min-width:0;max-width:none;';
|
|
1763
|
+
tr.appendChild(td);
|
|
1764
|
+
return tr;
|
|
1765
|
+
};
|
|
1766
|
+
// The parity filler: see `_virtSpacers`.
|
|
1767
|
+
const filler = document.createElement('tr');
|
|
1768
|
+
filler.className = 'twm-dt-spacer twm-dt-spacer--parity';
|
|
1769
|
+
filler.setAttribute('aria-hidden', 'true');
|
|
1770
|
+
filler.hidden = true;
|
|
1771
|
+
this._virt = {
|
|
1772
|
+
tbody, rows, globalOf, serverSide, offset, configOffset,
|
|
1773
|
+
overscan,
|
|
1774
|
+
base: this._virtBase || rowHeight,
|
|
1775
|
+
heights: null, offsets: null,
|
|
1776
|
+
start: 0, end: 0,
|
|
1777
|
+
map: new Map(),
|
|
1778
|
+
top: spacer('top'), bottom: spacer('bottom'), filler,
|
|
1779
|
+
};
|
|
1780
|
+
this._virtComputeOffsets();
|
|
1781
|
+
// A first window from an ESTIMATED viewport: the scroll box does not
|
|
1782
|
+
// exist yet. `render()` redraws it for the real one once it does.
|
|
1783
|
+
const guess = this.container?.clientHeight || 0;
|
|
1784
|
+
this._virtUpdate({ force: true, notify: false, viewport: guess || undefined, top: 0 });
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/** Heights and their running sum. `offsets[i]` is where row `i` starts;
|
|
1788
|
+
* `offsets[n]` is the whole table. O(n), and rerun only when a height can
|
|
1789
|
+
* have changed — a new measurement, or `refreshVirtualLayout()`. */
|
|
1790
|
+
_virtComputeOffsets() {
|
|
1791
|
+
const v = this._virt;
|
|
1792
|
+
if (!v) return;
|
|
1793
|
+
const n = v.rows.length;
|
|
1794
|
+
const heights = new Float64Array(n);
|
|
1795
|
+
const offsets = new Float64Array(n + 1);
|
|
1796
|
+
const fn = typeof this.config.virtualRowHeight === 'function'
|
|
1797
|
+
? this.config.virtualRowHeight : null;
|
|
1798
|
+
let y = 0;
|
|
1799
|
+
for (let i = 0; i < n; i++) {
|
|
1800
|
+
let h = v.base;
|
|
1801
|
+
if (fn) {
|
|
1802
|
+
const asked = fn(v.globalOf(i), v.base);
|
|
1803
|
+
h = Number.isFinite(asked) && asked > 0 ? asked : 0;
|
|
1579
1804
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1805
|
+
heights[i] = h;
|
|
1806
|
+
offsets[i] = y;
|
|
1807
|
+
y += h;
|
|
1808
|
+
}
|
|
1809
|
+
offsets[n] = y;
|
|
1810
|
+
v.heights = heights;
|
|
1811
|
+
v.offsets = offsets;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
/** The row whose span contains `y`: the LAST `i` with `offsets[i] <= y`,
|
|
1815
|
+
* which steps over rows of zero height at the same offset. */
|
|
1816
|
+
_virtIndexAt(y) {
|
|
1817
|
+
const v = this._virt;
|
|
1818
|
+
const n = v.rows.length;
|
|
1819
|
+
if (n === 0) return 0;
|
|
1820
|
+
let lo = 0;
|
|
1821
|
+
let hi = n - 1;
|
|
1822
|
+
while (lo < hi) {
|
|
1823
|
+
const mid = (lo + hi + 1) >> 1;
|
|
1824
|
+
if (v.offsets[mid] <= y) lo = mid; else hi = mid - 1;
|
|
1825
|
+
}
|
|
1826
|
+
return lo;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/** Measure one real row. Client pixels, so the zoom is divided back out —
|
|
1830
|
+
* `_measureNaturalWidths` explains why — and NOT `offsetHeight`, which is
|
|
1831
|
+
* rounded: 21 for a 21.19px row is a 1,100px error over 6,000 rows. */
|
|
1832
|
+
_virtMeasure() {
|
|
1833
|
+
const v = this._virt;
|
|
1834
|
+
if (!v) return false;
|
|
1835
|
+
for (const tr of v.map.values()) {
|
|
1836
|
+
if (!tr.isConnected || tr.hidden) continue;
|
|
1837
|
+
const zoom = this._tableEl?.currentCSSZoom || 1;
|
|
1838
|
+
const h = tr.getBoundingClientRect().height / zoom;
|
|
1839
|
+
if (!(h > 0)) return false;
|
|
1840
|
+
if (Math.abs(h - v.base) > 0.01) {
|
|
1841
|
+
v.base = h;
|
|
1842
|
+
this._virtBase = h;
|
|
1843
|
+
this._virtComputeOffsets();
|
|
1844
|
+
return true;
|
|
1584
1845
|
}
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
return false;
|
|
1849
|
+
}
|
|
1585
1850
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1851
|
+
/**
|
|
1852
|
+
* Slide the window to wherever the scroll box now is.
|
|
1853
|
+
*
|
|
1854
|
+
* HYSTERESIS, so a scroll of one pixel does not rebuild anything: the
|
|
1855
|
+
* window is redrawn only when the visible rows come within a quarter of the
|
|
1856
|
+
* overscan of either end of what is drawn, and it is then redrawn with the
|
|
1857
|
+
* full overscan on both sides.
|
|
1858
|
+
*
|
|
1859
|
+
* @param {object} [o]
|
|
1860
|
+
* @param {boolean} [o.force] redraw even inside the hysteresis band.
|
|
1861
|
+
* @param {boolean} [o.notify] report added and removed rows to `onRowsRendered`.
|
|
1862
|
+
* @param {number} [o.top] the scroll offset to draw for, instead of
|
|
1863
|
+
* reading one back — `scrollToRow` passes the offset it has just set,
|
|
1864
|
+
* which a DOM with no layout would read back as 0.
|
|
1865
|
+
* @param {number} [o.viewport] the viewport height, likewise.
|
|
1866
|
+
* @returns {HTMLElement[]} the rows added.
|
|
1867
|
+
*/
|
|
1868
|
+
_virtUpdate({ force = false, notify = true, top = null, viewport = null } = {}) {
|
|
1869
|
+
const v = this._virt;
|
|
1870
|
+
if (!v || !v.offsets) return [];
|
|
1871
|
+
const n = v.rows.length;
|
|
1872
|
+
const wrap = this._tableWrapEl;
|
|
1873
|
+
const view = viewport || wrap?.clientHeight || VIRTUAL_FALLBACK_VIEWPORT_PX;
|
|
1874
|
+
const scrollTop = top ?? (wrap?.scrollTop || 0);
|
|
1875
|
+
const vs = this._virtIndexAt(scrollTop);
|
|
1876
|
+
const ve = Math.min(n, this._virtIndexAt(scrollTop + view) + 1);
|
|
1877
|
+
if (!force && v.end > v.start) {
|
|
1878
|
+
const margin = Math.max(1, Math.floor(v.overscan / 4));
|
|
1879
|
+
const lowOk = v.start === 0 || vs >= v.start + margin;
|
|
1880
|
+
const highOk = v.end === n || ve <= v.end - margin;
|
|
1881
|
+
if (lowOk && highOk) return [];
|
|
1882
|
+
}
|
|
1883
|
+
const start = Math.max(0, vs - v.overscan);
|
|
1884
|
+
const end = Math.min(n, ve + v.overscan);
|
|
1885
|
+
const removed = [];
|
|
1886
|
+
const added = this._virtRender(start, end, removed);
|
|
1887
|
+
if (added.length || removed.length) {
|
|
1888
|
+
// Only once the columns are fixed: before that the first sync has
|
|
1889
|
+
// not run, and it tooltips every cell itself.
|
|
1890
|
+
if (added[0]?.isConnected && this._tableEl?.style.tableLayout === 'fixed') {
|
|
1891
|
+
this._updateCellTooltips(added);
|
|
1892
|
+
}
|
|
1893
|
+
if (notify) {
|
|
1894
|
+
try { this.config.onRowsRendered?.({ rows: added, removed }); } catch (err) {
|
|
1895
|
+
console.error('[DataTable] onRowsRendered threw', err);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
return added;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
/** Make the DOM hold exactly the drawable rows of `[start, end)`, without
|
|
1903
|
+
* moving a row that is already there. See the promises above. */
|
|
1904
|
+
_virtRender(start, end, removed = null) {
|
|
1905
|
+
const v = this._virt;
|
|
1906
|
+
const tbody = v.tbody;
|
|
1907
|
+
const want = [];
|
|
1908
|
+
for (let i = start; i < end; i++) if (v.heights[i] > 0) want.push(i);
|
|
1909
|
+
const wanted = new Set(want);
|
|
1910
|
+
for (const [i, tr] of v.map) {
|
|
1911
|
+
if (!wanted.has(i)) { tr.remove(); v.map.delete(i); removed?.push(tr); }
|
|
1912
|
+
}
|
|
1913
|
+
v.start = start;
|
|
1914
|
+
v.end = end;
|
|
1915
|
+
this._virtSpacers();
|
|
1916
|
+
const added = [];
|
|
1917
|
+
// Right to left, so each new row goes in front of the one after it —
|
|
1918
|
+
// which is either a row that stayed (and is not moved) or the bottom
|
|
1919
|
+
// spacer (or the end of the body).
|
|
1920
|
+
let ref = v.bottom.isConnected ? v.bottom : null;
|
|
1921
|
+
for (let k = want.length - 1; k >= 0; k--) {
|
|
1922
|
+
const i = want[k];
|
|
1923
|
+
const have = v.map.get(i);
|
|
1924
|
+
if (have) { ref = have; continue; }
|
|
1925
|
+
const tr = this._buildBodyRow(v.rows[i], v.globalOf(i), i);
|
|
1926
|
+
v.map.set(i, tr);
|
|
1927
|
+
tbody.insertBefore(tr, ref);
|
|
1928
|
+
ref = tr;
|
|
1929
|
+
added.push(tr);
|
|
1930
|
+
}
|
|
1931
|
+
added.reverse();
|
|
1932
|
+
return added;
|
|
1933
|
+
}
|
|
1588
1934
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1935
|
+
/**
|
|
1936
|
+
* Size the two spacers, and put each in the DOM only while it has a height.
|
|
1937
|
+
*
|
|
1938
|
+
* AND KEEP THE STRIPE. `tr:nth-child(even)` stripes by POSITION, and in a
|
|
1939
|
+
* full render row `i` is child `i + 1`. Here the first drawn row is preceded
|
|
1940
|
+
* by the top spacer, so its position is off by one — and by a different one
|
|
1941
|
+
* each time the window's start changes parity, which strobes every stripe
|
|
1942
|
+
* on the screen as you scroll. A hidden filler row after the spacer, present
|
|
1943
|
+
* exactly when `start` is even, puts the parity back: a `display: none` row
|
|
1944
|
+
* is still a child, and so still counted by `nth-child`.
|
|
1945
|
+
*/
|
|
1946
|
+
_virtSpacers() {
|
|
1947
|
+
const v = this._virt;
|
|
1948
|
+
const n = v.rows.length;
|
|
1949
|
+
const above = v.offsets[v.start];
|
|
1950
|
+
const below = v.offsets[n] - v.offsets[v.end];
|
|
1951
|
+
const tbody = v.tbody;
|
|
1952
|
+
if (above > 0) {
|
|
1953
|
+
v.top.firstChild.style.height = `${above}px`;
|
|
1954
|
+
if (tbody.firstChild !== v.top) tbody.insertBefore(v.top, tbody.firstChild);
|
|
1955
|
+
const needFiller = v.start % 2 === 0;
|
|
1956
|
+
if (needFiller && v.top.nextSibling !== v.filler) v.top.after(v.filler);
|
|
1957
|
+
else if (!needFiller) v.filler.remove();
|
|
1958
|
+
} else {
|
|
1959
|
+
v.top.remove();
|
|
1960
|
+
v.filler.remove();
|
|
1961
|
+
}
|
|
1962
|
+
if (below > 0) {
|
|
1963
|
+
v.bottom.firstChild.style.height = `${below}px`;
|
|
1964
|
+
if (tbody.lastChild !== v.bottom) tbody.appendChild(v.bottom);
|
|
1965
|
+
} else {
|
|
1966
|
+
v.bottom.remove();
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1591
1969
|
|
|
1592
|
-
|
|
1970
|
+
/**
|
|
1971
|
+
* Recompute every row's height and redraw the window. For an embedder whose
|
|
1972
|
+
* `virtualRowHeight` answer has changed — a group folded, a header added.
|
|
1973
|
+
* A no-op on a table that is not virtual.
|
|
1974
|
+
*/
|
|
1975
|
+
refreshVirtualLayout() {
|
|
1976
|
+
if (!this._virt) return;
|
|
1977
|
+
this._virtMeasure();
|
|
1978
|
+
this._virtComputeOffsets();
|
|
1979
|
+
this._virtUpdate({ force: true });
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/** The element that scrolls the body. Read-only; the table owns it. */
|
|
1983
|
+
get scrollElement() {
|
|
1984
|
+
return this._tableWrapEl || null;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/** The `<tr>` drawing row `rowIndex` (the `__rowIndex` space), or null when
|
|
1988
|
+
* it is not in the DOM — outside the window, on another page, or hidden. */
|
|
1989
|
+
getRowElement(rowIndex) {
|
|
1990
|
+
const v = this._virt;
|
|
1991
|
+
if (v) {
|
|
1992
|
+
const local = this._virtLocalOf(rowIndex);
|
|
1993
|
+
return local < 0 ? null : (v.map.get(local) || null);
|
|
1994
|
+
}
|
|
1995
|
+
const tbody = this._tbodyEl;
|
|
1996
|
+
if (!tbody) return null;
|
|
1997
|
+
for (const tr of tbody.children) if (tr.__rowIndex === rowIndex) return tr;
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/** Keep the window's row list and `config.rows` in step for one row. They
|
|
2002
|
+
* are the same array in server-side mode; in client mode the window reads
|
|
2003
|
+
* the filtered-and-sorted copy, which holds its own reference. */
|
|
2004
|
+
_virtSetRow(rowIndex, row) {
|
|
2005
|
+
const v = this._virt;
|
|
2006
|
+
if (!v) return;
|
|
2007
|
+
if (Array.isArray(this.config.rows) && rowIndex >= 0
|
|
2008
|
+
&& rowIndex < this.config.rows.length) {
|
|
2009
|
+
this.config.rows[rowIndex] = row;
|
|
2010
|
+
}
|
|
2011
|
+
const local = this._virtLocalOf(rowIndex);
|
|
2012
|
+
if (local >= 0) v.rows[local] = row;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
/** `__rowIndex` → position in the window's row list, or -1. */
|
|
2016
|
+
_virtLocalOf(rowIndex) {
|
|
2017
|
+
const v = this._virt;
|
|
2018
|
+
if (!v) return -1;
|
|
2019
|
+
let local;
|
|
2020
|
+
if (v.serverSide) local = rowIndex - v.configOffset;
|
|
2021
|
+
else {
|
|
2022
|
+
const map = this._processedIndexMap;
|
|
2023
|
+
const processed = map ? map.indexOf(rowIndex) : rowIndex;
|
|
2024
|
+
local = processed < 0 ? -1 : processed - v.offset;
|
|
2025
|
+
}
|
|
2026
|
+
return local >= 0 && local < v.rows.length ? local : -1;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Scroll the body so row `rowIndex` is on screen, drawing it if it was not,
|
|
2031
|
+
* and return its `<tr>` (null when it cannot be shown: another page, or a
|
|
2032
|
+
* row the embedder hides). Works with or without `virtualize`.
|
|
2033
|
+
*
|
|
2034
|
+
* @param {number} rowIndex in the `__rowIndex` space.
|
|
2035
|
+
* @param {{block?: 'nearest'|'center'|'start'}} [opts]
|
|
2036
|
+
*/
|
|
2037
|
+
scrollToRow(rowIndex, { block = 'nearest' } = {}) {
|
|
2038
|
+
const wrap = this._tableWrapEl;
|
|
2039
|
+
const v = this._virt;
|
|
2040
|
+
if (!v) {
|
|
2041
|
+
const tr = this.getRowElement(rowIndex);
|
|
2042
|
+
if (!tr || !wrap) return tr;
|
|
2043
|
+
const zoom = this._tableEl?.currentCSSZoom || 1;
|
|
2044
|
+
const box = wrap.getBoundingClientRect();
|
|
2045
|
+
const r = tr.getBoundingClientRect();
|
|
2046
|
+
const rowTop = (r.top - box.top) / zoom + wrap.scrollTop;
|
|
2047
|
+
const rowH = r.height / zoom;
|
|
2048
|
+
const target = this._scrollTarget(rowTop, rowH, wrap.scrollTop,
|
|
2049
|
+
wrap.clientHeight, block);
|
|
2050
|
+
if (target !== wrap.scrollTop) wrap.scrollTop = target;
|
|
2051
|
+
return tr;
|
|
2052
|
+
}
|
|
2053
|
+
const local = this._virtLocalOf(rowIndex);
|
|
2054
|
+
if (local < 0 || !(v.heights[local] > 0)) return null;
|
|
2055
|
+
const view = wrap?.clientHeight || VIRTUAL_FALLBACK_VIEWPORT_PX;
|
|
2056
|
+
const current = wrap?.scrollTop || 0;
|
|
2057
|
+
let target = this._scrollTarget(v.offsets[local], v.heights[local],
|
|
2058
|
+
current, view, block);
|
|
2059
|
+
target = Math.max(0, Math.min(target, Math.max(0, v.offsets[v.rows.length] - view)));
|
|
2060
|
+
if (wrap && Math.abs(target - current) >= 1) wrap.scrollTop = target;
|
|
2061
|
+
this._virtUpdate({ top: Math.abs(target - current) >= 1 ? target : current });
|
|
2062
|
+
return v.map.get(local) || null;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
_scrollTarget(rowTop, rowH, current, view, block) {
|
|
2066
|
+
if (block === 'start') return rowTop;
|
|
2067
|
+
if (block === 'center') return rowTop - Math.max(0, (view - rowH) / 2);
|
|
2068
|
+
if (rowTop < current) return rowTop;
|
|
2069
|
+
if (rowTop + rowH > current + view) return rowTop + rowH - view;
|
|
2070
|
+
return current;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/** The first body row that draws a row of data — never a spacer, and never
|
|
2074
|
+
* a row an embedder injected (a group header spans every column with ONE
|
|
2075
|
+
* cell, so measuring it would report one column). */
|
|
2076
|
+
_firstBodyRow() {
|
|
2077
|
+
const tbody = this._tableEl?.querySelector?.('tbody');
|
|
2078
|
+
if (!tbody) return null;
|
|
2079
|
+
for (const tr of tbody.children) {
|
|
2080
|
+
if (tr.__rowIndex !== undefined) return tr;
|
|
2081
|
+
}
|
|
2082
|
+
return null;
|
|
1593
2083
|
}
|
|
1594
2084
|
|
|
1595
2085
|
/** Build (or rebuild) one row's cells in place.
|
|
@@ -1674,7 +2164,15 @@ export class DataTable {
|
|
|
1674
2164
|
for (const candidate of tbody.children) {
|
|
1675
2165
|
if (candidate.__rowIndex === index) { tr = candidate; break; }
|
|
1676
2166
|
}
|
|
1677
|
-
|
|
2167
|
+
// C10. OUTSIDE THE WINDOW IS NOT OFF THE PAGE. The row will be drawn the
|
|
2168
|
+
// moment it scrolls back in, from the window's own row list — so the
|
|
2169
|
+
// data has to change now, or it comes back showing the value it had
|
|
2170
|
+
// before this update. Without `virtualize` a row that is not in the DOM
|
|
2171
|
+
// is on another page, and that path is unchanged.
|
|
2172
|
+
if (!tr) {
|
|
2173
|
+
if (this._virt) this._virtSetRow(index, row);
|
|
2174
|
+
return false;
|
|
2175
|
+
}
|
|
1678
2176
|
|
|
1679
2177
|
// Keep the caller's data in step, so a later full render agrees with
|
|
1680
2178
|
// what is on screen. Server-side paging means config.rows may hold only
|
|
@@ -1682,6 +2180,7 @@ export class DataTable {
|
|
|
1682
2180
|
if (Array.isArray(this.config.rows) && this.config.rows[index]) {
|
|
1683
2181
|
this.config.rows[index] = row;
|
|
1684
2182
|
}
|
|
2183
|
+
if (this._virt) this._virtSetRow(index, row);
|
|
1685
2184
|
|
|
1686
2185
|
// Record focus BEFORE the replace: `replaceChildren` detaches the
|
|
1687
2186
|
// focused cell and the browser moves focus to <body>.
|
|
@@ -1756,14 +2255,26 @@ export class DataTable {
|
|
|
1756
2255
|
* never visible. Clearing inline widths first stops the last sync's forced
|
|
1757
2256
|
* widths from constraining the measure.
|
|
1758
2257
|
*
|
|
2258
|
+
* C10 — UNDER `virtualize` ONLY THE WINDOW IS IN THE DOM, so `max-content`
|
|
2259
|
+
* spans the eighty-odd rows drawn rather than every row loaded. The fit
|
|
2260
|
+
* pass that runs on every render is content with that (it is fitting what
|
|
2261
|
+
* is on screen). An EXPLICIT auto-size is not: it is a promise to fit the
|
|
2262
|
+
* column's content, and the longest value in a 6,000-row table is usually
|
|
2263
|
+
* not in the window. So `sample` adds, for the length of this one reflow,
|
|
2264
|
+
* the row holding each column's longest value — see `_virtSampleRows`.
|
|
2265
|
+
*
|
|
2266
|
+
* @param {{sample?: boolean}} [o]
|
|
1759
2267
|
* @returns {number[]|null} width per DOM column index, or null when there
|
|
1760
2268
|
* is nothing laid out to measure.
|
|
1761
2269
|
*/
|
|
1762
|
-
_measureNaturalWidths() {
|
|
2270
|
+
_measureNaturalWidths({ sample = false } = {}) {
|
|
1763
2271
|
const headerTable = this._headerTableEl;
|
|
1764
2272
|
const bodyTable = this._tableEl;
|
|
1765
2273
|
if (!headerTable || !bodyTable) return null;
|
|
1766
|
-
|
|
2274
|
+
// A ROW OF DATA, not merely the first `<tr>`: under C10 that is a
|
|
2275
|
+
// spacer, and an embedder may inject rows of its own (a group header is
|
|
2276
|
+
// one cell spanning every column, which would measure as ONE column).
|
|
2277
|
+
const firstRow = this._firstBodyRow();
|
|
1767
2278
|
if (!firstRow) return null;
|
|
1768
2279
|
const bodyCells = firstRow.children;
|
|
1769
2280
|
if (!bodyCells.length) return null;
|
|
@@ -1772,10 +2283,17 @@ export class DataTable {
|
|
|
1772
2283
|
for (const th of tr.children) this._clearCellWidth(th);
|
|
1773
2284
|
});
|
|
1774
2285
|
for (const td of bodyCells) this._clearCellWidth(td);
|
|
2286
|
+
// C10: the `<col>`s are what hold the widths, so they are what has to
|
|
2287
|
+
// let go for the measure (`.twm-dt-measuring col` also overrides them).
|
|
2288
|
+
for (const col of this._cols() || []) col.style.width = '';
|
|
1775
2289
|
headerTable.style.width = '';
|
|
1776
2290
|
bodyTable.style.width = '';
|
|
1777
2291
|
headerTable.classList.add('twm-dt-measuring');
|
|
1778
2292
|
bodyTable.classList.add('twm-dt-measuring');
|
|
2293
|
+
// Appended at the end, where they move no drawn row, and taken out
|
|
2294
|
+
// again below before anything paints.
|
|
2295
|
+
const sampled = sample ? this._virtSampleRows() : [];
|
|
2296
|
+
for (const tr of sampled) this._virt.tbody.appendChild(tr);
|
|
1779
2297
|
// Force layout flush so measurements are current.
|
|
1780
2298
|
// eslint-disable-next-line no-unused-expressions
|
|
1781
2299
|
bodyTable.offsetWidth;
|
|
@@ -1811,13 +2329,60 @@ export class DataTable {
|
|
|
1811
2329
|
natural[i] = Math.max(body, head);
|
|
1812
2330
|
}
|
|
1813
2331
|
|
|
2332
|
+
for (const tr of sampled) tr.remove();
|
|
1814
2333
|
headerTable.classList.remove('twm-dt-measuring');
|
|
1815
2334
|
bodyTable.classList.remove('twm-dt-measuring');
|
|
1816
2335
|
return natural;
|
|
1817
2336
|
}
|
|
1818
2337
|
|
|
1819
2338
|
/**
|
|
1820
|
-
*
|
|
2339
|
+
* The rows an explicit auto-size must measure that the window has not
|
|
2340
|
+
* drawn: for each column, the drawable row whose value is LONGEST as a
|
|
2341
|
+
* string, plus the last row (the widest row number). Built with
|
|
2342
|
+
* `_buildBodyRow` — so a consumer's `renderCell` draws them exactly as it
|
|
2343
|
+
* would on screen — and handed back detached, for `_measureNaturalWidths`
|
|
2344
|
+
* to put in and take out around its one reflow.
|
|
2345
|
+
*
|
|
2346
|
+
* The string length is a proxy, not a measurement: in a proportional font
|
|
2347
|
+
* forty `i`s are narrower than thirty `W`s. It is the proxy a spreadsheet's
|
|
2348
|
+
* own autofit falls short by, and it is right about the case that matters
|
|
2349
|
+
* here — a value five times longer than anything in the window. One pass
|
|
2350
|
+
* over the loaded rows, on a click and never on a scroll or a resize.
|
|
2351
|
+
*
|
|
2352
|
+
* @returns {HTMLTableRowElement[]}
|
|
2353
|
+
*/
|
|
2354
|
+
_virtSampleRows() {
|
|
2355
|
+
const v = this._virt;
|
|
2356
|
+
if (!v || !v.rows.length) return [];
|
|
2357
|
+
const longest = [];
|
|
2358
|
+
const at = [];
|
|
2359
|
+
let last = -1;
|
|
2360
|
+
for (let i = 0; i < v.rows.length; i++) {
|
|
2361
|
+
if (!(v.heights[i] > 0)) continue; // hidden: not drawn, not fitted
|
|
2362
|
+
last = i;
|
|
2363
|
+
const row = v.rows[i];
|
|
2364
|
+
if (!Array.isArray(row)) continue;
|
|
2365
|
+
for (let c = 0; c < row.length; c++) {
|
|
2366
|
+
const value = row[c];
|
|
2367
|
+
const len = value == null ? 0 : String(value).length;
|
|
2368
|
+
if (len > (longest[c] ?? -1)) { longest[c] = len; at[c] = i; }
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
const want = new Set(at.filter((i) => i !== undefined));
|
|
2372
|
+
if (last >= 0) want.add(last);
|
|
2373
|
+
const rows = [];
|
|
2374
|
+
for (const i of want) {
|
|
2375
|
+
if (v.map.has(i)) continue; // drawn already; measured anyway
|
|
2376
|
+
const tr = this._buildBodyRow(v.rows[i], v.globalOf(i), i);
|
|
2377
|
+
tr.setAttribute('aria-hidden', 'true');
|
|
2378
|
+
rows.push(tr);
|
|
2379
|
+
}
|
|
2380
|
+
return rows;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Fit one column to its widest value and pin it there — every loaded
|
|
2385
|
+
* row's, under `virtualize` (see `_virtSampleRows`), not only the window's.
|
|
1821
2386
|
*
|
|
1822
2387
|
* The gesture is a double-click on the column's resize grip, which is where
|
|
1823
2388
|
* every spreadsheet has put it — and the grip is this component's, which is
|
|
@@ -1832,7 +2397,7 @@ export class DataTable {
|
|
|
1832
2397
|
* @returns {boolean} whether it had a layout to measure.
|
|
1833
2398
|
*/
|
|
1834
2399
|
autoSizeColumn(domIdx, opts = {}) {
|
|
1835
|
-
const natural = this._measureNaturalWidths();
|
|
2400
|
+
const natural = this._measureNaturalWidths({ sample: true });
|
|
1836
2401
|
if (!natural || domIdx < 0 || domIdx >= natural.length) return false;
|
|
1837
2402
|
this._colWidths[domIdx] = this._autoWidth(natural[domIdx], opts);
|
|
1838
2403
|
this._colWidthsSig = this._colSig();
|
|
@@ -1857,7 +2422,7 @@ export class DataTable {
|
|
|
1857
2422
|
* already, so a button that did that would be a button that does nothing.
|
|
1858
2423
|
*/
|
|
1859
2424
|
autoSizeColumns(opts = {}) {
|
|
1860
|
-
const natural = this._measureNaturalWidths();
|
|
2425
|
+
const natural = this._measureNaturalWidths({ sample: true });
|
|
1861
2426
|
if (!natural) return false;
|
|
1862
2427
|
this._colWidths = {};
|
|
1863
2428
|
for (let i = 0; i < natural.length; i++) {
|
|
@@ -1909,10 +2474,15 @@ export class DataTable {
|
|
|
1909
2474
|
const headerTable = this._headerTableEl;
|
|
1910
2475
|
const bodyTable = this._tableEl;
|
|
1911
2476
|
if (!headerTable || !bodyTable) return;
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2477
|
+
// C10. UNDER `virtualize` THE COLUMN TRACK IS THE `<colgroup>`, and the
|
|
2478
|
+
// column count is ITS length — not the first row's, which is whatever
|
|
2479
|
+
// row the window happens to start at (or a spacer). Without it, the
|
|
2480
|
+
// first ROW OF DATA; see `_firstBodyRow` for why not merely the first `<tr>`.
|
|
2481
|
+
const cols = this._cols();
|
|
2482
|
+
const firstRow = this._firstBodyRow();
|
|
2483
|
+
const bodyCells = cols ? [] : firstRow?.children;
|
|
2484
|
+
const n = cols ? cols.length : (bodyCells?.length || 0);
|
|
2485
|
+
if (!n) return;
|
|
1916
2486
|
|
|
1917
2487
|
const headerRows = headerTable.querySelectorAll('thead > tr');
|
|
1918
2488
|
// DO NOT MEASURE WHAT NOTHING WILL READ.
|
|
@@ -1930,8 +2500,8 @@ export class DataTable {
|
|
|
1930
2500
|
// only thing left that the array supplies is `n`, which is its length.
|
|
1931
2501
|
// So zeroes of the right length are not an approximation of the
|
|
1932
2502
|
// measurement, they are indistinguishable from it.
|
|
1933
|
-
const natural = this._allColumnsPinned(
|
|
1934
|
-
? new Array(
|
|
2503
|
+
const natural = this._allColumnsPinned(n)
|
|
2504
|
+
? new Array(n).fill(0)
|
|
1935
2505
|
: this._measureNaturalWidths();
|
|
1936
2506
|
if (!natural) return;
|
|
1937
2507
|
|
|
@@ -1941,7 +2511,7 @@ export class DataTable {
|
|
|
1941
2511
|
const wrap = this._tableWrapEl;
|
|
1942
2512
|
const headerWrap = this._headerWrapEl;
|
|
1943
2513
|
const avail = wrap ? wrap.clientWidth : 0;
|
|
1944
|
-
const firstIdx = this.config.showRowNumbers &&
|
|
2514
|
+
const firstIdx = this.config.showRowNumbers && n > 1 ? 1 : 0;
|
|
1945
2515
|
const widths = this._fitColumnWidths(natural, avail, {
|
|
1946
2516
|
firstIdx,
|
|
1947
2517
|
overrides: this._colWidths,
|
|
@@ -1959,8 +2529,14 @@ export class DataTable {
|
|
|
1959
2529
|
this._setCellWidth(tr.children[i], widths[i]);
|
|
1960
2530
|
}
|
|
1961
2531
|
});
|
|
1962
|
-
|
|
1963
|
-
|
|
2532
|
+
if (cols) {
|
|
2533
|
+
for (let i = 0; i < cols.length && i < widths.length; i++) {
|
|
2534
|
+
cols[i].style.width = `${widths[i]}px`;
|
|
2535
|
+
}
|
|
2536
|
+
} else {
|
|
2537
|
+
for (let i = 0; i < bodyCells.length && i < widths.length; i++) {
|
|
2538
|
+
this._setCellWidth(bodyCells[i], widths[i]);
|
|
2539
|
+
}
|
|
1964
2540
|
}
|
|
1965
2541
|
|
|
1966
2542
|
// Lock both tables so the widths stick even when content changes.
|
|
@@ -2361,7 +2937,13 @@ export class DataTable {
|
|
|
2361
2937
|
headerTable.querySelectorAll('thead > tr').forEach((tr) => {
|
|
2362
2938
|
if (tr.children[i]) this._setCellWidth(tr.children[i], w);
|
|
2363
2939
|
});
|
|
2364
|
-
|
|
2940
|
+
// C10: the `<col>` is the track; without one, the first row of data.
|
|
2941
|
+
const cols = this._cols();
|
|
2942
|
+
if (cols) {
|
|
2943
|
+
if (cols[i]) cols[i].style.width = `${w}px`;
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
const bodyRow = bodyTable && this._firstBodyRow();
|
|
2365
2947
|
if (bodyRow && bodyRow.children[i]) this._setCellWidth(bodyRow.children[i], w);
|
|
2366
2948
|
}
|
|
2367
2949
|
|
|
@@ -2417,10 +2999,15 @@ export class DataTable {
|
|
|
2417
2999
|
/** Add a native `title` tooltip to any body cell whose text is
|
|
2418
3000
|
* clipped by its column width; remove ours once it fits again.
|
|
2419
3001
|
* Leaves caller-supplied titles (e.g. from renderCell) untouched. */
|
|
2420
|
-
_updateCellTooltips() {
|
|
3002
|
+
_updateCellTooltips(rows = null) {
|
|
2421
3003
|
const bodyTable = this._tableEl;
|
|
2422
3004
|
if (!bodyTable) return;
|
|
2423
|
-
|
|
3005
|
+
// `rows`: only these (C10 — the rows a window slide just added).
|
|
3006
|
+
const cells = rows
|
|
3007
|
+
? rows.flatMap((tr) => [...tr.children])
|
|
3008
|
+
: bodyTable.querySelectorAll('tbody td');
|
|
3009
|
+
cells.forEach((td) => {
|
|
3010
|
+
if (td.parentElement?.classList.contains('twm-dt-spacer')) return;
|
|
2424
3011
|
const clipped = td.scrollWidth > td.clientWidth + 1;
|
|
2425
3012
|
if (clipped) {
|
|
2426
3013
|
if (!td.title) td.title = td.textContent;
|