flexdesk 0.6.1 → 0.6.3
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 +44 -0
- package/css/base.css +43 -0
- package/css/flexdesk.css +43 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-ZMCRDOCF.js → chunk-WAVLVXYV.js} +2 -2
- package/dist/{chunk-IYV5JVYB.js → chunk-WLASPRGP.js} +328 -20
- package/dist/chunk-WLASPRGP.js.map +7 -0
- package/dist/flexdesk.css +43 -0
- package/dist/flow.js +1 -1
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +8 -2
- package/dist/widgets.js.map +1 -1
- package/package.json +1 -1
- package/src/ui/components/data_table.js +392 -19
- package/dist/chunk-IYV5JVYB.js.map +0 -7
- /package/dist/{chunk-ZMCRDOCF.js.map → chunk-WAVLVXYV.js.map} +0 -0
package/package.json
CHANGED
|
@@ -40,6 +40,67 @@ const DEFAULT_PAGE_SIZE = 100;
|
|
|
40
40
|
/** 0.6.1 — the row statuses `diff` draws. */
|
|
41
41
|
const DIFF_STATUSES = new Set(['added', 'removed', 'changed', 'same', 'gap']);
|
|
42
42
|
|
|
43
|
+
// ── `paging`: a server's rows, in pages or as one scrolling list ────────────
|
|
44
|
+
//
|
|
45
|
+
// THE TWO LIMITS. A table that scrolls holds every row it has loaded, in one
|
|
46
|
+
// array and (through `virtualize`) one scroll range, so how many rows it may
|
|
47
|
+
// hold is a decision rather than an accident of how far somebody scrolled.
|
|
48
|
+
// The SOFT limit is the consumer's (a person's preference, say): a source
|
|
49
|
+
// with more rows than that is shown in pages, whatever was asked for. The HARD
|
|
50
|
+
// limit is the ceiling no soft limit may exceed, and no scrolling table ever
|
|
51
|
+
// holds more rows than it.
|
|
52
|
+
|
|
53
|
+
/** The most rows a scrolling table holds, and the ceiling of every soft limit. */
|
|
54
|
+
export const PAGING_HARD_LIMIT = 100_000;
|
|
55
|
+
/** The soft limit when the consumer names none. */
|
|
56
|
+
export const PAGING_SOFT_LIMIT = 10_000;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* WHICH WAY A TABLE IS SHOWN — pure, so a consumer deciding how to FETCH and
|
|
60
|
+
* the table deciding how to DRAW get the same answer from the same facts.
|
|
61
|
+
*
|
|
62
|
+
* Scroll is allowed only while the source is known to hold no more rows than
|
|
63
|
+
* the soft limit. What is known is the exact `total` when there is one, and
|
|
64
|
+
* otherwise the most the facts prove: a lower bound (`atLeast`, a capped
|
|
65
|
+
* count) or the rows already held plus one when the source says it has more.
|
|
66
|
+
* An unknown total therefore does not force pages until the rows themselves
|
|
67
|
+
* prove the source is larger than the limit.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} [o]
|
|
70
|
+
* @param {'pages'|'scroll'} [o.mode='scroll'] what was asked for
|
|
71
|
+
* @param {number|null} [o.total] the source's exact row count, if known
|
|
72
|
+
* @param {number} [o.atLeast=0] a lower bound on it (a capped count)
|
|
73
|
+
* @param {number} [o.held=0] rows the table holds now
|
|
74
|
+
* @param {boolean} [o.hasMore=false] the source has rows past those held
|
|
75
|
+
* @param {number} [o.softLimit=PAGING_SOFT_LIMIT]
|
|
76
|
+
* @param {number} [o.hardLimit=PAGING_HARD_LIMIT]
|
|
77
|
+
* @returns {{mode: 'pages'|'scroll', requested: 'pages'|'scroll', forced: boolean,
|
|
78
|
+
* softLimit: number, hardLimit: number, known: number, exact: boolean}}
|
|
79
|
+
*/
|
|
80
|
+
export function resolvePaging({
|
|
81
|
+
mode = 'scroll', total = null, atLeast = 0, held = 0, hasMore = false,
|
|
82
|
+
softLimit = PAGING_SOFT_LIMIT, hardLimit = PAGING_HARD_LIMIT,
|
|
83
|
+
} = {}) {
|
|
84
|
+
const requested = mode === 'pages' ? 'pages' : 'scroll';
|
|
85
|
+
const hard = Number.isFinite(hardLimit) && hardLimit >= 1
|
|
86
|
+
? Math.floor(hardLimit) : PAGING_HARD_LIMIT;
|
|
87
|
+
// A soft limit above the hard one is the hard one: nothing the consumer
|
|
88
|
+
// names can let a table hold more than the ceiling.
|
|
89
|
+
const soft = Math.min(hard, Number.isFinite(softLimit) && softLimit >= 1
|
|
90
|
+
? Math.floor(softLimit) : PAGING_SOFT_LIMIT);
|
|
91
|
+
const exact = Number.isFinite(total) && total >= 0;
|
|
92
|
+
const known = exact ? total
|
|
93
|
+
: Math.max(Number.isFinite(atLeast) ? atLeast : 0, (held || 0) + (hasMore ? 1 : 0));
|
|
94
|
+
const forced = requested === 'scroll' && known > soft;
|
|
95
|
+
return {
|
|
96
|
+
mode: forced ? 'pages' : requested, requested, forced,
|
|
97
|
+
softLimit: soft, hardLimit: hard, known, exact,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** How near the bottom (px) a scrolling table asks for its next rows. */
|
|
102
|
+
const PAGING_LOAD_MORE_PX = 400;
|
|
103
|
+
|
|
43
104
|
/** 0.5.0 — the column types that sort and filter by a value rather than by
|
|
44
105
|
* the text drawn for it. */
|
|
45
106
|
const TYPED_COLUMNS = new Set(['date', 'datetime', 'duration']);
|
|
@@ -232,6 +293,15 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
232
293
|
* @property {string|Function|Object|Array} [dateTimeFormat='YYYY-MM-DD HH:mm'] - The same for a 'datetime' column.
|
|
233
294
|
* @property {'auto'|'clock'|Function|Object|Array} [durationFormat='auto'] - How a 'duration' column (milliseconds) is drawn: '850 ms', '3.2 s', '2m 5s' (auto) or '0:02:05' (clock).
|
|
234
295
|
* @property {'local'|'UTC'} [dateTimeZone='local'] - The zone typed date columns are read and drawn in.
|
|
296
|
+
*
|
|
297
|
+
* ── `paging`, off unless asked for ────────────────────────────────────────
|
|
298
|
+
* @property {Object} [paging] - A server's rows shown IN PAGES or as ONE SCROLLING LIST, and a strip under the body that says which and lets the reader change it. The consumer holds the rows and fetches them; the table decides which way they are shown (`resolvePaging`), asks for more, and draws the strip. Set it and the old pager (`pagination`) is not drawn. Its keys:
|
|
299
|
+
* `mode` ('pages'|'scroll', default 'scroll') — what was asked for. Scroll is refused, and pages shown, while the source holds more than `softLimit` rows.
|
|
300
|
+
* `pageSize` — rows per page, and per load while scrolling (default `pageSize`).
|
|
301
|
+
* `softLimit` (default 10,000) and `hardLimit` (default 100,000) — see `resolvePaging`; the soft one is clamped to the hard one, and a scrolling table never asks for rows past the hard one.
|
|
302
|
+
* `total` — the source's exact row count, or null. `atLeast` — a lower bound when the exact count is not known (a capped count). `hasMore` — the source has rows past those held.
|
|
303
|
+
* `offset` — in pages, where the first row held stands in the source: the strip's "Rows 1,001–2,000" and `showRowNumbers: 'position'` read it. The rows' indexes (`__rowIndex`, selection, `onRowClick`) stay those of `rows`.
|
|
304
|
+
* `onPage(offset, limit)` — pages: show the page at `offset`. `onLoadMore()` — scroll: called when the body is scrolled within 400px of its end (`loadMorePx`), or does not fill its box, while `hasMore`. `onLoadAll()` — scroll: the strip's *Load all rows* button. `onModeChange(mode)` — the strip's Pages / Scroll switch; without it there is no switch. Each may return a Promise; until it settles no second call is made and its button shows the table is busy.
|
|
235
305
|
*/
|
|
236
306
|
|
|
237
307
|
/** C10. When the scroll box has no layout — mounted hidden, or a DOM with no
|
|
@@ -299,6 +369,9 @@ export class DataTable {
|
|
|
299
369
|
// promise that adding the key changed no existing layout by a
|
|
300
370
|
// pixel. Only a consumer that asks for 'content' sees anything new.
|
|
301
371
|
columnFit: 'container',
|
|
372
|
+
// Off unless asked for — see the typedef. With it the old pager is
|
|
373
|
+
// not drawn: the paging strip says the same and more.
|
|
374
|
+
paging: null,
|
|
302
375
|
// C10. Off unless asked for — see the typedef.
|
|
303
376
|
virtualize: false,
|
|
304
377
|
virtualRowHeight: null,
|
|
@@ -839,8 +912,9 @@ export class DataTable {
|
|
|
839
912
|
this._loading = false;
|
|
840
913
|
|
|
841
914
|
// Pagination (top). `_createPagination` returns null when the
|
|
842
|
-
// strip would be uninformative (single page, no active filter).
|
|
843
|
-
|
|
915
|
+
// strip would be uninformative (single page, no active filter). Not
|
|
916
|
+
// with `paging`, whose own strip (under the body) replaces it.
|
|
917
|
+
if (pagination && rows.length > 0 && !this._pagingConfig()) {
|
|
844
918
|
this._paginationEl = this._createPagination();
|
|
845
919
|
if (this._paginationEl) this._wrapperEl.appendChild(this._paginationEl);
|
|
846
920
|
}
|
|
@@ -962,6 +1036,13 @@ export class DataTable {
|
|
|
962
1036
|
}
|
|
963
1037
|
|
|
964
1038
|
this._tableWrapEl = tableWrap;
|
|
1039
|
+
// `paging`: the strip goes UNDER the body, where the reader's eye is
|
|
1040
|
+
// when they reach the end of what is held.
|
|
1041
|
+
this._pagingEl = null;
|
|
1042
|
+
if (this._pagingConfig()) {
|
|
1043
|
+
this._pagingEl = this._createPagingStrip();
|
|
1044
|
+
if (this._pagingEl) this._wrapperEl.appendChild(this._pagingEl);
|
|
1045
|
+
}
|
|
965
1046
|
this.container.appendChild(this._wrapperEl);
|
|
966
1047
|
// 0.6.1 `scrollLock`: THIS render's body box joins the group, and leaves
|
|
967
1048
|
// it with the next render's cleanup or the dispose.
|
|
@@ -990,12 +1071,29 @@ export class DataTable {
|
|
|
990
1071
|
this._virtUpdate({ force: true, notify: false });
|
|
991
1072
|
}
|
|
992
1073
|
|
|
1074
|
+
// `paging`, scrolling: ask for the next rows near the end — and once
|
|
1075
|
+
// the layout has settled, when the rows held do not fill the box at all,
|
|
1076
|
+
// since then nothing can ever be scrolled to ask for them.
|
|
1077
|
+
if (this._pagingConfig()) {
|
|
1078
|
+
const onScroll = () => this._pagingMaybeLoadMore();
|
|
1079
|
+
tableWrap.addEventListener('scroll', onScroll, { passive: true });
|
|
1080
|
+
this._disposers.push(() => tableWrap.removeEventListener('scroll', onScroll));
|
|
1081
|
+
requestAnimationFrame(() => {
|
|
1082
|
+
if (this._tableWrapEl === tableWrap) this._pagingMaybeLoadMore();
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
993
1086
|
// Sync header column widths to body after layout. Two-pass: the
|
|
994
1087
|
// first frame lets the browser settle natural widths from the
|
|
995
1088
|
// body's auto layout, then we lock both tables to those widths
|
|
996
1089
|
// via table-layout:fixed + explicit cell widths.
|
|
997
1090
|
if (this._headerTableEl && this._tableEl) {
|
|
998
|
-
|
|
1091
|
+
// 0.6.3: and once the widths are fixed the rows may be another
|
|
1092
|
+
// height than they were measured at — so the window measures again.
|
|
1093
|
+
requestAnimationFrame(() => {
|
|
1094
|
+
this._syncHeaderWidths();
|
|
1095
|
+
this._virtRemeasure();
|
|
1096
|
+
});
|
|
999
1097
|
// Re-sync on container resize so columns stay aligned when
|
|
1000
1098
|
// the parent flex / grid layout changes (window resize,
|
|
1001
1099
|
// splitter drag, etc.).
|
|
@@ -2046,6 +2144,238 @@ export class DataTable {
|
|
|
2046
2144
|
return btn;
|
|
2047
2145
|
}
|
|
2048
2146
|
|
|
2147
|
+
// ── `paging` ────────────────────────────────────────────────────────────
|
|
2148
|
+
|
|
2149
|
+
/** The `paging` config with its defaults, or null when it is not set. */
|
|
2150
|
+
_pagingConfig() {
|
|
2151
|
+
const p = this.config.paging;
|
|
2152
|
+
if (!p || typeof p !== 'object') return null;
|
|
2153
|
+
const size = Number.isFinite(p.pageSize) && p.pageSize >= 1
|
|
2154
|
+
? Math.floor(p.pageSize) : this.config.pageSize;
|
|
2155
|
+
return {
|
|
2156
|
+
...p,
|
|
2157
|
+
pageSize: size,
|
|
2158
|
+
offset: Number.isFinite(p.offset) && p.offset > 0 ? Math.floor(p.offset) : 0,
|
|
2159
|
+
// What the table holds of the source. A consumer whose `rows` carry
|
|
2160
|
+
// a row the source does not (a blank line to type into) says so.
|
|
2161
|
+
held: Number.isFinite(p.held) ? p.held : (this.config.rows?.length || 0),
|
|
2162
|
+
hasMore: !!p.hasMore,
|
|
2163
|
+
loadMorePx: Number.isFinite(p.loadMorePx) ? p.loadMorePx : PAGING_LOAD_MORE_PX,
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/**
|
|
2168
|
+
* Which way the rows are shown, and why — `resolvePaging` over this
|
|
2169
|
+
* table's facts. `null` without `paging`.
|
|
2170
|
+
*/
|
|
2171
|
+
get pagingState() {
|
|
2172
|
+
const p = this._pagingConfig();
|
|
2173
|
+
if (!p) return null;
|
|
2174
|
+
// A page's rows prove the source holds every row before them too, so
|
|
2175
|
+
// what is PROVEN is the offset plus the rows held.
|
|
2176
|
+
return resolvePaging({
|
|
2177
|
+
mode: p.mode, total: p.total, atLeast: p.atLeast, held: p.offset + p.held,
|
|
2178
|
+
hasMore: p.hasMore, softLimit: p.softLimit, hardLimit: p.hardLimit,
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Change some `paging` facts — a count that arrived, a mode — and redraw
|
|
2184
|
+
* the STRIP only. The rows are not touched: a consumer with new rows
|
|
2185
|
+
* renders the table as it always has.
|
|
2186
|
+
*/
|
|
2187
|
+
setPaging(patch = {}) {
|
|
2188
|
+
if (!this.config.paging) return;
|
|
2189
|
+
this.config.paging = { ...this.config.paging, ...patch };
|
|
2190
|
+
this._refreshPagingStrip();
|
|
2191
|
+
this._pagingMaybeLoadMore();
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
_refreshPagingStrip() {
|
|
2195
|
+
if (!this._wrapperEl) return;
|
|
2196
|
+
const next = this._pagingConfig() ? this._createPagingStrip() : null;
|
|
2197
|
+
const prev = this._pagingEl;
|
|
2198
|
+
if (prev && prev.parentNode === this._wrapperEl) {
|
|
2199
|
+
if (next) prev.replaceWith(next); else prev.remove();
|
|
2200
|
+
} else if (next) {
|
|
2201
|
+
this._wrapperEl.appendChild(next);
|
|
2202
|
+
}
|
|
2203
|
+
this._pagingEl = next;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Call one of the consumer's paging callbacks. A Promise holds every other
|
|
2208
|
+
* paging call back until it settles, and the strip shows the table busy —
|
|
2209
|
+
* a second `onLoadMore` fired by the next scroll event would fetch the same
|
|
2210
|
+
* page twice.
|
|
2211
|
+
*/
|
|
2212
|
+
_pagingCall(kind, fn, ...args) {
|
|
2213
|
+
if (typeof fn !== 'function' || this._pagingBusy) return;
|
|
2214
|
+
let result;
|
|
2215
|
+
try {
|
|
2216
|
+
result = fn(...args);
|
|
2217
|
+
} catch (err) {
|
|
2218
|
+
console.error(`[DataTable] paging ${kind} threw`, err);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (result && typeof result.then === 'function') {
|
|
2222
|
+
this._pagingBusy = kind;
|
|
2223
|
+
this._refreshPagingStrip();
|
|
2224
|
+
const settle = () => {
|
|
2225
|
+
if (this._pagingBusy !== kind) return;
|
|
2226
|
+
this._pagingBusy = null;
|
|
2227
|
+
if (this._pagingEl?.isConnected) this._refreshPagingStrip();
|
|
2228
|
+
};
|
|
2229
|
+
result.then(settle, (err) => {
|
|
2230
|
+
console.error(`[DataTable] paging ${kind} failed`, err);
|
|
2231
|
+
settle();
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Scrolling: ask for the next rows when the body is near its end — or does
|
|
2238
|
+
* not fill its box, which no scroll can ever change. Never past the hard
|
|
2239
|
+
* limit, never in pages, never with no layout (a hidden table's box is 0px
|
|
2240
|
+
* tall, and "0 ≤ 0" would ask for the whole source).
|
|
2241
|
+
*/
|
|
2242
|
+
_pagingMaybeLoadMore() {
|
|
2243
|
+
const p = this._pagingConfig();
|
|
2244
|
+
if (!p || !p.hasMore || this._pagingBusy) return;
|
|
2245
|
+
const state = this.pagingState;
|
|
2246
|
+
if (state.mode !== 'scroll' || p.held >= state.hardLimit) return;
|
|
2247
|
+
const wrap = this._tableWrapEl;
|
|
2248
|
+
if (!wrap?.isConnected || !(wrap.clientHeight > 0)) return;
|
|
2249
|
+
const remaining = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight;
|
|
2250
|
+
if (remaining >= p.loadMorePx) return;
|
|
2251
|
+
this._pagingCall('more', p.onLoadMore);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
/** The strip under the body, or null when it would say nothing. */
|
|
2255
|
+
_createPagingStrip() {
|
|
2256
|
+
const p = this._pagingConfig();
|
|
2257
|
+
const state = this.pagingState;
|
|
2258
|
+
const { mode, forced, exact, known } = state;
|
|
2259
|
+
const busy = this._pagingBusy;
|
|
2260
|
+
const n = (x) => Number(x).toLocaleString();
|
|
2261
|
+
const pageSize = p.pageSize;
|
|
2262
|
+
const held = p.held;
|
|
2263
|
+
const total = exact ? p.total : null;
|
|
2264
|
+
|
|
2265
|
+
const morePages = p.offset > 0 || p.hasMore || (exact && total > held);
|
|
2266
|
+
if (!forced && mode === 'scroll' && !p.hasMore && held <= pageSize) return null;
|
|
2267
|
+
if (!forced && mode === 'pages' && !morePages) return null;
|
|
2268
|
+
|
|
2269
|
+
const el = document.createElement('div');
|
|
2270
|
+
el.className = `twm-dt-paging twm-dt-paging--${mode}`;
|
|
2271
|
+
el.setAttribute('role', 'navigation');
|
|
2272
|
+
el.setAttribute('aria-label', 'Rows');
|
|
2273
|
+
if (busy) el.classList.add('twm-dt-paging--busy');
|
|
2274
|
+
|
|
2275
|
+
const info = document.createElement('span');
|
|
2276
|
+
info.className = 'twm-dt-paging__info';
|
|
2277
|
+
el.appendChild(info);
|
|
2278
|
+
|
|
2279
|
+
if (mode === 'pages') {
|
|
2280
|
+
const from = held ? p.offset + 1 : p.offset;
|
|
2281
|
+
const to = p.offset + held;
|
|
2282
|
+
let of = '';
|
|
2283
|
+
if (exact) of = ` of ${n(total)}`;
|
|
2284
|
+
else if (known > to) of = ` of more than ${n(known - 1)}`;
|
|
2285
|
+
info.textContent = held ? `Rows ${n(from)}–${n(to)}${of}` : 'No rows on this page';
|
|
2286
|
+
|
|
2287
|
+
const hasPrev = p.offset > 0;
|
|
2288
|
+
const hasNext = exact ? p.offset + held < total : p.hasMore;
|
|
2289
|
+
const lastOffset = exact && total > 0
|
|
2290
|
+
? Math.floor((total - 1) / pageSize) * pageSize : null;
|
|
2291
|
+
// A disabled button's click is ignored here too: a browser sends none,
|
|
2292
|
+
// but a synthetic one (a test, a script) still reaches the listener.
|
|
2293
|
+
const go = (offset) => (ev) => {
|
|
2294
|
+
if (!ev?.currentTarget?.disabled) this._pagingCall('page', p.onPage, offset, pageSize);
|
|
2295
|
+
};
|
|
2296
|
+
const nav = document.createElement('span');
|
|
2297
|
+
nav.className = 'twm-dt-paging__nav';
|
|
2298
|
+
const off = !!busy || typeof p.onPage !== 'function';
|
|
2299
|
+
nav.append(
|
|
2300
|
+
this._createPaginationBtn('first_page', 'First page', off || !hasPrev, go(0)),
|
|
2301
|
+
this._createPaginationBtn('chevron_left', 'Previous page', off || !hasPrev,
|
|
2302
|
+
go(Math.max(0, p.offset - pageSize))),
|
|
2303
|
+
);
|
|
2304
|
+
const page = document.createElement('span');
|
|
2305
|
+
page.className = 'twm-dt-paging__page';
|
|
2306
|
+
const current = Math.floor(p.offset / pageSize) + 1;
|
|
2307
|
+
page.textContent = exact
|
|
2308
|
+
? `Page ${n(current)} of ${n(Math.max(1, Math.ceil(total / pageSize)))}`
|
|
2309
|
+
: `Page ${n(current)}`;
|
|
2310
|
+
nav.append(
|
|
2311
|
+
page,
|
|
2312
|
+
this._createPaginationBtn('chevron_right', 'Next page', off || !hasNext,
|
|
2313
|
+
go(p.offset + pageSize)),
|
|
2314
|
+
// The last page needs the exact count: without it there is no
|
|
2315
|
+
// offset to go to, and the button says so by being off.
|
|
2316
|
+
this._createPaginationBtn('last_page',
|
|
2317
|
+
lastOffset === null ? 'Last page (the total is not known)' : 'Last page',
|
|
2318
|
+
off || !hasNext || lastOffset === null, go(lastOffset)),
|
|
2319
|
+
);
|
|
2320
|
+
el.appendChild(nav);
|
|
2321
|
+
} else {
|
|
2322
|
+
if (exact && total > held) info.textContent = `${n(held)} of ${n(total)} rows`;
|
|
2323
|
+
else if (p.hasMore) info.textContent = `${n(held)} rows, more to come`;
|
|
2324
|
+
else info.textContent = `${n(held)} rows`;
|
|
2325
|
+
if (p.hasMore && typeof p.onLoadAll === 'function') {
|
|
2326
|
+
const all = document.createElement('button');
|
|
2327
|
+
all.type = 'button';
|
|
2328
|
+
all.className = 'twm-dt-paging__all';
|
|
2329
|
+
all.textContent = busy === 'all' ? 'Loading…'
|
|
2330
|
+
: exact ? `Load all ${n(total)} rows` : 'Load all rows';
|
|
2331
|
+
all.disabled = !!busy;
|
|
2332
|
+
all.addEventListener('click', () => {
|
|
2333
|
+
if (!all.disabled) this._pagingCall('all', p.onLoadAll);
|
|
2334
|
+
});
|
|
2335
|
+
el.appendChild(all);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const spacer = document.createElement('span');
|
|
2340
|
+
spacer.className = 'twm-dt-paging__spacer';
|
|
2341
|
+
el.appendChild(spacer);
|
|
2342
|
+
|
|
2343
|
+
// WHY IT IS PAGED, said on the strip — a disabled Scroll alone reads as
|
|
2344
|
+
// a switch that does not work.
|
|
2345
|
+
const why = forced
|
|
2346
|
+
? `More than ${n(state.softLimit)} rows, so they are shown in pages.` : null;
|
|
2347
|
+
if (why) {
|
|
2348
|
+
const note = document.createElement('span');
|
|
2349
|
+
note.className = 'twm-dt-paging__note';
|
|
2350
|
+
note.textContent = why;
|
|
2351
|
+
el.appendChild(note);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
if (typeof p.onModeChange === 'function') {
|
|
2355
|
+
const group = document.createElement('span');
|
|
2356
|
+
group.className = 'twm-dt-paging__modes';
|
|
2357
|
+
group.setAttribute('role', 'group');
|
|
2358
|
+
group.setAttribute('aria-label', 'Show rows');
|
|
2359
|
+
for (const [value, label] of [['pages', 'Pages'], ['scroll', 'Scroll']]) {
|
|
2360
|
+
const b = document.createElement('button');
|
|
2361
|
+
b.type = 'button';
|
|
2362
|
+
b.className = 'twm-dt-paging__mode';
|
|
2363
|
+
b.dataset.mode = value;
|
|
2364
|
+
b.textContent = label;
|
|
2365
|
+
const pressed = mode === value;
|
|
2366
|
+
b.setAttribute('aria-pressed', String(pressed));
|
|
2367
|
+
b.disabled = !!busy || (value === 'scroll' && forced);
|
|
2368
|
+
if (value === 'scroll' && forced) b.title = why;
|
|
2369
|
+
b.addEventListener('click', () => {
|
|
2370
|
+
if (!b.disabled && mode !== value) this._pagingCall('mode', p.onModeChange, value);
|
|
2371
|
+
});
|
|
2372
|
+
group.appendChild(b);
|
|
2373
|
+
}
|
|
2374
|
+
el.appendChild(group);
|
|
2375
|
+
}
|
|
2376
|
+
return el;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2049
2379
|
_createTable() {
|
|
2050
2380
|
const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon, onPageChange, offset: configOffset } = this.config;
|
|
2051
2381
|
const { sortColumn, sortAscending } = this._state;
|
|
@@ -2058,7 +2388,10 @@ export class DataTable {
|
|
|
2058
2388
|
// NO PAGINATION MEANS NO PAGES. This used to slice to `pageSize` either
|
|
2059
2389
|
// way, so a table built with `pagination: false` silently showed its
|
|
2060
2390
|
// first hundred rows and offered no control to reach the rest.
|
|
2061
|
-
|
|
2391
|
+
// And `paging` rows ARE the page (or everything scrolled to so far):
|
|
2392
|
+
// the consumer fetched them, and slicing them again would draw a
|
|
2393
|
+
// hundred of a thousand.
|
|
2394
|
+
const pageRows = isServerSide || !this.config.pagination || this._pagingConfig()
|
|
2062
2395
|
? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
2063
2396
|
|
|
2064
2397
|
const table = document.createElement('table');
|
|
@@ -2156,6 +2489,10 @@ export class DataTable {
|
|
|
2156
2489
|
// stands in the order SHOWN (the page's first row on a later page).
|
|
2157
2490
|
this._positionBase = isServerSide ? (configOffset || 0)
|
|
2158
2491
|
: (this.config.pagination ? offset : 0);
|
|
2492
|
+
// `paging`: a page's rows stand at its offset in the SOURCE, which only
|
|
2493
|
+
// the numbering reads — their indexes stay those of `rows`.
|
|
2494
|
+
const paged = this._pagingConfig();
|
|
2495
|
+
if (paged && Number.isFinite(paged.offset)) this._positionBase += paged.offset;
|
|
2159
2496
|
|
|
2160
2497
|
// Body
|
|
2161
2498
|
const tbody = document.createElement('tbody');
|
|
@@ -2363,26 +2700,52 @@ export class DataTable {
|
|
|
2363
2700
|
return lo;
|
|
2364
2701
|
}
|
|
2365
2702
|
|
|
2366
|
-
/**
|
|
2367
|
-
*
|
|
2368
|
-
*
|
|
2703
|
+
/**
|
|
2704
|
+
* Measure the rows that are DRAWN — the median of up to 64 of them. Client
|
|
2705
|
+
* pixels, so the zoom is divided back out (`_measureNaturalWidths` explains
|
|
2706
|
+
* why), and NOT `offsetHeight`, which is rounded: 21 for a 21.19px row is a
|
|
2707
|
+
* 1,100px error over 6,000 rows.
|
|
2708
|
+
*
|
|
2709
|
+
* 0.6.3: IT USED TO MEASURE ONE ROW, ONCE, AT RENDER, and trust it for the
|
|
2710
|
+
* life of the table. A row measured at a bad moment — a box not yet laid
|
|
2711
|
+
* out at its width, a font not yet loaded — set every row's height: a
|
|
2712
|
+
* Tables grid drawing 22.8px rows was found holding a 134px estimate, so the
|
|
2713
|
+
* window took a 650px viewport for five rows, drew rows 1–35 and left the
|
|
2714
|
+
* rest of the box blank spacer while the reader scrolled. Now it is asked
|
|
2715
|
+
* again on every window update (`_virtUpdate`), on a resize and once the
|
|
2716
|
+
* column widths are fixed, and corrects itself the first time real rows
|
|
2717
|
+
* are on screen. The first measure is exact; after that a change has to be
|
|
2718
|
+
* larger than half a pixel, so rows of mixed heights cannot make the
|
|
2719
|
+
* offsets move under the reader on every scroll.
|
|
2720
|
+
*
|
|
2721
|
+
* @returns {boolean} whether the heights changed (the offsets are recomputed).
|
|
2722
|
+
*/
|
|
2369
2723
|
_virtMeasure() {
|
|
2370
2724
|
const v = this._virt;
|
|
2371
2725
|
if (!v) return false;
|
|
2726
|
+
const zoom = this._tableEl?.currentCSSZoom || 1;
|
|
2727
|
+
const heights = [];
|
|
2372
2728
|
for (const tr of v.map.values()) {
|
|
2373
2729
|
if (!tr.isConnected || tr.hidden) continue;
|
|
2374
|
-
const zoom = this._tableEl?.currentCSSZoom || 1;
|
|
2375
2730
|
const h = tr.getBoundingClientRect().height / zoom;
|
|
2376
|
-
if (
|
|
2377
|
-
if (
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2731
|
+
if (h > 0) heights.push(h);
|
|
2732
|
+
if (heights.length >= 64) break;
|
|
2733
|
+
}
|
|
2734
|
+
if (!heights.length) return false;
|
|
2735
|
+
heights.sort((a, b) => a - b);
|
|
2736
|
+
const h = heights[heights.length >> 1];
|
|
2737
|
+
const tolerance = v.measured ? 0.5 : 0.01;
|
|
2738
|
+
v.measured = true;
|
|
2739
|
+
if (Math.abs(h - v.base) <= tolerance) return false;
|
|
2740
|
+
v.base = h;
|
|
2741
|
+
this._virtBase = h;
|
|
2742
|
+
this._virtComputeOffsets();
|
|
2743
|
+
return true;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
/** Measure again, and redraw the window when the heights moved. */
|
|
2747
|
+
_virtRemeasure() {
|
|
2748
|
+
if (this._virtMeasure()) this._virtUpdate({ force: true });
|
|
2386
2749
|
}
|
|
2387
2750
|
|
|
2388
2751
|
/**
|
|
@@ -2402,9 +2765,12 @@ export class DataTable {
|
|
|
2402
2765
|
* @param {number} [o.viewport] the viewport height, likewise.
|
|
2403
2766
|
* @returns {HTMLElement[]} the rows added.
|
|
2404
2767
|
*/
|
|
2405
|
-
_virtUpdate({ force = false, notify = true, top = null, viewport = null } = {}) {
|
|
2768
|
+
_virtUpdate({ force = false, notify = true, top = null, viewport = null, measure = true } = {}) {
|
|
2406
2769
|
const v = this._virt;
|
|
2407
2770
|
if (!v || !v.offsets) return [];
|
|
2771
|
+
// The rows on screen say how tall a row is — before the window is
|
|
2772
|
+
// placed, so a stale estimate is not used to place it (`_virtMeasure`).
|
|
2773
|
+
if (measure && this._virtMeasure()) force = true;
|
|
2408
2774
|
const n = v.rows.length;
|
|
2409
2775
|
const wrap = this._tableWrapEl;
|
|
2410
2776
|
const view = viewport || wrap?.clientHeight || VIRTUAL_FALLBACK_VIEWPORT_PX;
|
|
@@ -2432,6 +2798,13 @@ export class DataTable {
|
|
|
2432
2798
|
console.error('[DataTable] onRowsRendered threw', err);
|
|
2433
2799
|
}
|
|
2434
2800
|
}
|
|
2801
|
+
// And the rows just drawn: if they disagree with the estimate the
|
|
2802
|
+
// window was placed by, place it once more by what they say.
|
|
2803
|
+
if (measure && added.length && this._virtMeasure()) {
|
|
2804
|
+
return added.concat(this._virtUpdate({
|
|
2805
|
+
force: true, notify, top, viewport, measure: false,
|
|
2806
|
+
}));
|
|
2807
|
+
}
|
|
2435
2808
|
}
|
|
2436
2809
|
return added;
|
|
2437
2810
|
}
|