flexdesk 0.6.0 → 0.6.1
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 +33 -0
- package/css/base.css +33 -0
- package/css/flexdesk.css +33 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-KH2GJTKR.js → chunk-IYV5JVYB.js} +29 -1
- package/dist/chunk-IYV5JVYB.js.map +7 -0
- package/dist/{chunk-CNXP3SIN.js → chunk-ZMCRDOCF.js} +2 -2
- package/dist/flexdesk.css +33 -0
- package/dist/flow.js +1 -1
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +81 -1
- package/dist/widgets.js.map +3 -3
- package/package.json +1 -1
- package/src/ui/components/data_table.js +40 -0
- package/src/ui/utils/scroll_lock.js +111 -0
- package/dist/chunk-KH2GJTKR.js.map +0 -7
- /package/dist/{chunk-CNXP3SIN.js.map → chunk-ZMCRDOCF.js.map} +0 -0
package/package.json
CHANGED
|
@@ -37,6 +37,9 @@ export {
|
|
|
37
37
|
|
|
38
38
|
const DEFAULT_PAGE_SIZE = 100;
|
|
39
39
|
|
|
40
|
+
/** 0.6.1 — the row statuses `diff` draws. */
|
|
41
|
+
const DIFF_STATUSES = new Set(['added', 'removed', 'changed', 'same', 'gap']);
|
|
42
|
+
|
|
40
43
|
/** 0.5.0 — the column types that sort and filter by a value rather than by
|
|
41
44
|
* the text drawn for it. */
|
|
42
45
|
const TYPED_COLUMNS = new Set(['date', 'datetime', 'duration']);
|
|
@@ -218,6 +221,8 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
218
221
|
* @property {Function} [rowClass] - `(row, rowIdx) => string|string[]|null`. Classes for the row's `<tr>`.
|
|
219
222
|
* @property {Function} [rowAttrs] - `(row, rowIdx) => {name: value}`. Attributes for the row's `<tr>` (`null`/`false` leaves one off, `true` sets it empty).
|
|
220
223
|
* @property {Function} [cellClass] - `(value, colIdx, row, rowIdx) => string|string[]|null`. Classes for a cell, after its type class.
|
|
224
|
+
* @property {Function} [diff] - 0.6.1. `(row, rowIdx) => {status, cells?}|null`. One side of a COMPARISON: `status` is 'added', 'removed', 'changed', 'same' or 'gap' and draws the row `twm-dt-diff twm-dt-diff--<status>`; `cells` (an array or Set of column indices) marks the cells that differ `twm-dt-diff__cell--changed`. A 'gap' row is the place a row holds on the OTHER side: its cells are drawn empty, whatever its values and `renderCell`, so two tables drawn from one aligned row list keep their rows level. `null` (or an unknown status) draws the row as without `diff`.
|
|
225
|
+
* @property {object} [scrollLock] - 0.6.1. A group from `createScrollLock()`: the table's body box joins it on every render and leaves it on the next render and on dispose, so tables in the same group scroll as one.
|
|
221
226
|
* @property {Function} [rowIcon] - `(row, rowIdx) => string|{icon, title?, tone?}|null`. A Material Symbols icon at the start of the row's first cell; `tone` adds `twm-dt-row-icon--<tone>`. Drawn by CSS, so it is in neither the cell's text, its tooltip nor a copy.
|
|
222
227
|
* @property {string|Node|Function} [emptyState] - What the box says when there are no rows: text, a node, or a function returning either (called on each render). Replaces `emptyMessage`, and is hidden while `setLoading(true)` — an empty table that is still loading is not empty.
|
|
223
228
|
* @property {string} [nullDisplay] - Draw `null`/`undefined` as this text (and copy it so), with `twm-dt-cell--null` on the cell, instead of '-'.
|
|
@@ -313,6 +318,8 @@ export class DataTable {
|
|
|
313
318
|
rowClass: null,
|
|
314
319
|
rowAttrs: null,
|
|
315
320
|
cellClass: null,
|
|
321
|
+
diff: null,
|
|
322
|
+
scrollLock: null,
|
|
316
323
|
rowIcon: null,
|
|
317
324
|
emptyState: undefined,
|
|
318
325
|
nullDisplay: null,
|
|
@@ -956,6 +963,11 @@ export class DataTable {
|
|
|
956
963
|
|
|
957
964
|
this._tableWrapEl = tableWrap;
|
|
958
965
|
this.container.appendChild(this._wrapperEl);
|
|
966
|
+
// 0.6.1 `scrollLock`: THIS render's body box joins the group, and leaves
|
|
967
|
+
// it with the next render's cleanup or the dispose.
|
|
968
|
+
if (this.config.scrollLock && typeof this.config.scrollLock.attach === 'function') {
|
|
969
|
+
this._disposers.push(this.config.scrollLock.attach(tableWrap));
|
|
970
|
+
}
|
|
959
971
|
// 0.5.0. Watched from here, so a table rendered again after an automatic
|
|
960
972
|
// dispose (a `setData` into a re-attached host) is watched again.
|
|
961
973
|
if (this.config.autoDispose) autoDisposeWatch(this);
|
|
@@ -2628,6 +2640,12 @@ export class DataTable {
|
|
|
2628
2640
|
}
|
|
2629
2641
|
|
|
2630
2642
|
const { cellClass, nullDisplay } = this.config;
|
|
2643
|
+
// 0.6.1 `diff`.
|
|
2644
|
+
const diff = this._diffOf(row, globalIdx);
|
|
2645
|
+
const gap = diff?.status === 'gap';
|
|
2646
|
+
const changedCells = diff?.cells
|
|
2647
|
+
? (diff.cells instanceof Set ? diff.cells : new Set(diff.cells))
|
|
2648
|
+
: null;
|
|
2631
2649
|
for (let colIdx = 0; colIdx < row.length; colIdx++) {
|
|
2632
2650
|
const td = document.createElement('td');
|
|
2633
2651
|
td.className = this._columnTypes[colIdx] || 'text';
|
|
@@ -2635,6 +2653,13 @@ export class DataTable {
|
|
|
2635
2653
|
// Use custom cell renderer if provided
|
|
2636
2654
|
const value = row[colIdx];
|
|
2637
2655
|
let handled = false;
|
|
2656
|
+
if (gap) {
|
|
2657
|
+
// The other side's row: a place, not a value.
|
|
2658
|
+
td.classList.add('twm-dt-diff__gap');
|
|
2659
|
+
tr.appendChild(td);
|
|
2660
|
+
continue;
|
|
2661
|
+
}
|
|
2662
|
+
if (changedCells?.has(colIdx)) td.classList.add('twm-dt-diff__cell--changed');
|
|
2638
2663
|
if (this.config.renderCell) {
|
|
2639
2664
|
handled = !!this.config.renderCell(td, value, colIdx, globalIdx, row);
|
|
2640
2665
|
if (!handled) {
|
|
@@ -2961,6 +2986,15 @@ export class DataTable {
|
|
|
2961
2986
|
return null;
|
|
2962
2987
|
}
|
|
2963
2988
|
|
|
2989
|
+
/** 0.6.1. The `diff` answer for one row, or `null` when there is none or
|
|
2990
|
+
* its status is not one this component draws. */
|
|
2991
|
+
_diffOf(row, rowIdx) {
|
|
2992
|
+
if (typeof this.config.diff !== 'function') return null;
|
|
2993
|
+
const answer = this.config.diff(row, rowIdx);
|
|
2994
|
+
if (!answer || !DIFF_STATUSES.has(answer.status)) return null;
|
|
2995
|
+
return answer;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2964
2998
|
/** Everything `rowClass`, `rowAttrs` and the open mark put on a `<tr>`,
|
|
2965
2999
|
* taken off again first — so `updateRow` cannot leave last state's class
|
|
2966
3000
|
* behind. */
|
|
@@ -2977,6 +3011,12 @@ export class DataTable {
|
|
|
2977
3011
|
// Never take away a class this component owns.
|
|
2978
3012
|
tr.__twmClasses = added.filter((c) => c !== 'data-preview-row' && c !== 'selected');
|
|
2979
3013
|
}
|
|
3014
|
+
// 0.6.1 `diff`: the row's status, taken off again first like `rowClass`.
|
|
3015
|
+
for (const c of [...tr.classList]) {
|
|
3016
|
+
if (c === 'twm-dt-diff' || c.startsWith('twm-dt-diff--')) tr.classList.remove(c);
|
|
3017
|
+
}
|
|
3018
|
+
const diff = this._diffOf(row, rowIdx);
|
|
3019
|
+
if (diff) tr.classList.add('twm-dt-diff', `twm-dt-diff--${diff.status}`);
|
|
2980
3020
|
if (tr.__twmAttrs) {
|
|
2981
3021
|
for (const name of tr.__twmAttrs) tr.removeAttribute(name);
|
|
2982
3022
|
tr.__twmAttrs = null;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scroll_lock.js — 0.6.1. Several scroll boxes that move as one.
|
|
3
|
+
*
|
|
4
|
+
* A side-by-side comparison (two DataTables in two tiles, "now" and "then") is
|
|
5
|
+
* only readable if row 40 on the left stays beside row 40 on the right. A lock
|
|
6
|
+
* is a GROUP: every box attached to it copies the scroll position of whichever
|
|
7
|
+
* member the person scrolled, on the axes the lock names.
|
|
8
|
+
*
|
|
9
|
+
* const lock = createScrollLock({ axis: 'y' });
|
|
10
|
+
* new DataTable(left, { …, scrollLock: lock });
|
|
11
|
+
* new DataTable(right, { …, scrollLock: lock });
|
|
12
|
+
* lock.setEnabled(false); // the two scroll freely; true locks them again
|
|
13
|
+
*
|
|
14
|
+
* Any element can join with `lock.attach(el)`, which returns its detach. A
|
|
15
|
+
* DataTable given `scrollLock` attaches its own body box on every render and
|
|
16
|
+
* detaches it on the next render and on dispose, so a re-render (a sort, new
|
|
17
|
+
* data) never strands a dead box in the group.
|
|
18
|
+
*
|
|
19
|
+
* THE ECHO. Setting `scrollTop` on a member fires that member's own `scroll`
|
|
20
|
+
* event a frame later, and a naive group would then copy it back to the first
|
|
21
|
+
* box, which by then may have moved on — the two fight and the wheel stutters.
|
|
22
|
+
* So a position this lock WROTE is remembered per box, and the scroll event that
|
|
23
|
+
* reports exactly that position is the echo and is ignored.
|
|
24
|
+
*
|
|
25
|
+
* A box that joins is moved to the group's last position, so a pane opened
|
|
26
|
+
* beside a scrolled one starts level with it. Positions are copied as pixels:
|
|
27
|
+
* the lock aligns rows only when the members draw rows of the same height,
|
|
28
|
+
* which is the comparison's own contract (both sides drawn from one aligned
|
|
29
|
+
* row list, a gap row where a side has none).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} [options]
|
|
34
|
+
* @param {'x'|'y'|'both'} [options.axis='y'] which positions are copied
|
|
35
|
+
* @param {boolean} [options.enabled=true]
|
|
36
|
+
*/
|
|
37
|
+
export function createScrollLock({ axis = 'y', enabled = true } = {}) {
|
|
38
|
+
const doY = axis === 'y' || axis === 'both';
|
|
39
|
+
const doX = axis === 'x' || axis === 'both';
|
|
40
|
+
const members = new Map(); // element -> { wrote: {top, left} | null, off }
|
|
41
|
+
let last = { top: 0, left: 0 };
|
|
42
|
+
let on = !!enabled;
|
|
43
|
+
|
|
44
|
+
function copyFrom(source) {
|
|
45
|
+
last = { top: source.scrollTop, left: source.scrollLeft };
|
|
46
|
+
if (!on) return;
|
|
47
|
+
for (const [el, state] of members) {
|
|
48
|
+
if (el === source) continue;
|
|
49
|
+
const top = doY ? last.top : el.scrollTop;
|
|
50
|
+
const left = doX ? last.left : el.scrollLeft;
|
|
51
|
+
if (el.scrollTop === top && el.scrollLeft === left) continue;
|
|
52
|
+
state.wrote = { top, left };
|
|
53
|
+
if (doY) el.scrollTop = top;
|
|
54
|
+
if (doX) el.scrollLeft = left;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onScroll(event) {
|
|
59
|
+
const el = event.currentTarget;
|
|
60
|
+
const state = members.get(el);
|
|
61
|
+
if (!state) return;
|
|
62
|
+
const wrote = state.wrote;
|
|
63
|
+
state.wrote = null;
|
|
64
|
+
// THE ECHO of a position this lock set: not a person scrolling.
|
|
65
|
+
if (wrote && Math.abs(el.scrollTop - wrote.top) < 1
|
|
66
|
+
&& Math.abs(el.scrollLeft - wrote.left) < 1) return;
|
|
67
|
+
copyFrom(el);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lock = {
|
|
71
|
+
/** Join the group. Returns the function that leaves it. */
|
|
72
|
+
attach(el) {
|
|
73
|
+
if (!el || typeof el.addEventListener !== 'function') return () => {};
|
|
74
|
+
if (members.has(el)) return () => lock.detach(el);
|
|
75
|
+
const state = { wrote: null };
|
|
76
|
+
members.set(el, state);
|
|
77
|
+
el.addEventListener('scroll', onScroll, { passive: true });
|
|
78
|
+
if (on && (last.top || last.left)) {
|
|
79
|
+
state.wrote = { top: doY ? last.top : el.scrollTop,
|
|
80
|
+
left: doX ? last.left : el.scrollLeft };
|
|
81
|
+
if (doY) el.scrollTop = last.top;
|
|
82
|
+
if (doX) el.scrollLeft = last.left;
|
|
83
|
+
}
|
|
84
|
+
return () => lock.detach(el);
|
|
85
|
+
},
|
|
86
|
+
detach(el) {
|
|
87
|
+
if (!members.has(el)) return;
|
|
88
|
+
members.delete(el);
|
|
89
|
+
el.removeEventListener('scroll', onScroll);
|
|
90
|
+
},
|
|
91
|
+
/** Lock (true) or let the members scroll freely (false). Locking again
|
|
92
|
+
* brings every member level with the one scrolled last. */
|
|
93
|
+
setEnabled(value) {
|
|
94
|
+
const was = on;
|
|
95
|
+
on = !!value;
|
|
96
|
+
if (on && !was) {
|
|
97
|
+
const source = [...members.keys()].find(
|
|
98
|
+
(el) => el.scrollTop === last.top && el.scrollLeft === last.left)
|
|
99
|
+
|| members.keys().next().value;
|
|
100
|
+
if (source) copyFrom(source);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
get enabled() { return on; },
|
|
104
|
+
/** How many boxes are in the group (for a consumer's own checks). */
|
|
105
|
+
get size() { return members.size; },
|
|
106
|
+
dispose() {
|
|
107
|
+
for (const el of [...members.keys()]) lock.detach(el);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
return lock;
|
|
111
|
+
}
|