flexdesk 0.4.3 → 0.4.4
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 +12 -0
- package/dist/widgets.js +99 -0
- package/dist/widgets.js.map +4 -4
- package/package.json +1 -1
- package/src/ui/components/column_fit.js +170 -0
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* column_fit.js — how wide each column of a table should be.
|
|
3
|
+
*
|
|
4
|
+
* A pure function over measurements, so the policy can be read and tested
|
|
5
|
+
* without a DOM. A table measures its columns, hands them here, and applies
|
|
6
|
+
* the widths it gets back.
|
|
7
|
+
*
|
|
8
|
+
* ── The policy ─────────────────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* Columns are not all alike, and treating them alike is what went wrong
|
|
11
|
+
* before. An id, a date, a status, a person: SHORT columns, whose values are
|
|
12
|
+
* all about as wide as each other, and which are useless the moment they are
|
|
13
|
+
* cut ("2026-09-…", "review-q…"). A summary or a title: TEXT columns, whose
|
|
14
|
+
* values run as long as people write them, and which are readable cut.
|
|
15
|
+
*
|
|
16
|
+
* The old fit spread spare room EVENLY, so a 40px "BUG" chip got 90px while
|
|
17
|
+
* the summary beside it was clipped in nine rows out of ten, and it shrank the
|
|
18
|
+
* WIDEST column first, so a narrow tile crushed the title to 85px while every
|
|
19
|
+
* short column kept its width.
|
|
20
|
+
*
|
|
21
|
+
* 1. Short columns get their full content width. Text columns start at a
|
|
22
|
+
* readable width.
|
|
23
|
+
* 2. Room to spare goes to the text columns, in proportion to what each
|
|
24
|
+
* still needs, and past their content so the table fills its box. With no
|
|
25
|
+
* text column it goes to the last column, so no column in the middle
|
|
26
|
+
* opens a gap.
|
|
27
|
+
* 3. Too little room: shrink in order of harm. Short columns first give up
|
|
28
|
+
* only their rare longest values (down to `typical`, what most values
|
|
29
|
+
* fit). Then text columns go down to a floor. Then short columns go down
|
|
30
|
+
* to theirs, and text columns once more. What still does not fit scrolls
|
|
31
|
+
* sideways rather than being crushed further.
|
|
32
|
+
*
|
|
33
|
+
* Within each step the widest columns give way first and the narrow ones keep
|
|
34
|
+
* their width (a water level), so one long column never forces every other
|
|
35
|
+
* column down with it.
|
|
36
|
+
*
|
|
37
|
+
* Columns the user dragged are PINNED: they keep their width and sit out every
|
|
38
|
+
* step.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
export const COLUMN_FIT = Object.freeze({
|
|
42
|
+
/** Absolute floor, and the drag-resize minimum. */
|
|
43
|
+
FLOOR: 40,
|
|
44
|
+
/** Sub-pixel safety against an ellipsis on content that exactly fits. */
|
|
45
|
+
PAD: 2,
|
|
46
|
+
/** A column whose full width reaches this is free text. */
|
|
47
|
+
TEXT_AT: 240,
|
|
48
|
+
/** A text column's starting share when room allows it. */
|
|
49
|
+
TEXT_MIN: 200,
|
|
50
|
+
/** How far a text column shrinks before short columns go below `typical`. */
|
|
51
|
+
TEXT_FLOOR: 120,
|
|
52
|
+
/** How far a short column shrinks before the table scrolls. */
|
|
53
|
+
SHORT_FLOOR: 56,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {object} ColumnMeasure
|
|
58
|
+
* @property {number} natural widest cell, padding included (px)
|
|
59
|
+
* @property {number} [typical] width most cells fit, e.g. the 95th percentile (px);
|
|
60
|
+
* defaults to `natural`
|
|
61
|
+
* @property {number} [header] the header label's width (px)
|
|
62
|
+
* @property {number} [pinned] a user-dragged width, honoured exactly
|
|
63
|
+
* @property {boolean} [text] force (true) or forbid (false) text treatment;
|
|
64
|
+
* omitted, it is decided from the width
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {ColumnMeasure[]} columns
|
|
69
|
+
* @param {number} avail the width the table has (px); 0 or less means "not laid out"
|
|
70
|
+
* @returns {number[]} a width per column (px, possibly fractional). The sum
|
|
71
|
+
* equals `avail` whenever the floors fit, and exceeds it only when they do
|
|
72
|
+
* not, in which case the table should scroll.
|
|
73
|
+
*/
|
|
74
|
+
export function fitColumns(columns, avail) {
|
|
75
|
+
const { FLOOR, PAD, TEXT_AT, TEXT_MIN, TEXT_FLOOR, SHORT_FLOOR } = COLUMN_FIT;
|
|
76
|
+
const n = columns.length;
|
|
77
|
+
const cols = columns.map((c) => {
|
|
78
|
+
const header = Math.max(0, Number(c.header) || 0);
|
|
79
|
+
const natural = Math.max(0, Number(c.natural) || 0);
|
|
80
|
+
const full = Math.max(FLOOR, Math.ceil(Math.max(natural, header) + PAD));
|
|
81
|
+
const typical = Math.min(full, Math.max(FLOOR, Math.ceil(Math.max(
|
|
82
|
+
Number.isFinite(c.typical) ? c.typical : natural, header) + PAD)));
|
|
83
|
+
const pinned = Number.isFinite(c.pinned) ? Math.max(FLOOR, Math.round(c.pinned)) : null;
|
|
84
|
+
const text = pinned == null && (typeof c.text === 'boolean' ? c.text : full >= TEXT_AT);
|
|
85
|
+
return { full, typical, pinned, text };
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const text = [];
|
|
89
|
+
const short = [];
|
|
90
|
+
cols.forEach((c, i) => { if (c.pinned == null) (c.text ? text : short).push(i); });
|
|
91
|
+
if (!(avail > 1)) return cols.map((c) => (c.pinned != null ? c.pinned : c.full));
|
|
92
|
+
|
|
93
|
+
// 1. The starting point: short columns at their full content width, text
|
|
94
|
+
// columns at a readable width (or their content, if shorter).
|
|
95
|
+
const widths = cols.map((c) => {
|
|
96
|
+
if (c.pinned != null) return c.pinned;
|
|
97
|
+
return c.text ? Math.min(c.full, TEXT_MIN) : c.full;
|
|
98
|
+
});
|
|
99
|
+
const total = () => widths.reduce((a, b) => a + b, 0);
|
|
100
|
+
let slack = avail - total();
|
|
101
|
+
|
|
102
|
+
// 2. Room to spare goes to the text columns, in proportion to what each
|
|
103
|
+
// still needs, then beyond their content so the table fills its box.
|
|
104
|
+
// Widths are not rounded: CSS takes fractional pixels, and rounding is
|
|
105
|
+
// what made the total miss the box by a pixel and a column jitter by one
|
|
106
|
+
// as its tile was resized.
|
|
107
|
+
if (slack >= 0) {
|
|
108
|
+
slack = raise(widths, text, (i) => cols[i].full, slack);
|
|
109
|
+
if (slack > 0) {
|
|
110
|
+
if (text.length) text.forEach((i) => { widths[i] += slack / text.length; });
|
|
111
|
+
else widths[short.length ? short[short.length - 1] : n - 1] += slack;
|
|
112
|
+
}
|
|
113
|
+
return widths;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 3. Not enough room: shrink in order of harm until it fits.
|
|
117
|
+
const steps = [
|
|
118
|
+
[short, (i) => cols[i].typical],
|
|
119
|
+
[text, () => TEXT_FLOOR],
|
|
120
|
+
[short, (i) => Math.min(cols[i].typical, SHORT_FLOOR)],
|
|
121
|
+
[text, () => FLOOR * 2],
|
|
122
|
+
];
|
|
123
|
+
for (const [idxs, low] of steps) {
|
|
124
|
+
const over = total() - avail;
|
|
125
|
+
if (over <= 1e-6) break;
|
|
126
|
+
lowerTo(widths, idxs, low, over);
|
|
127
|
+
}
|
|
128
|
+
return widths;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Give up to `slack` px to the columns in `idxs`, each toward `target(i)`, in
|
|
133
|
+
* proportion to how far each still is from it. Returns what is left.
|
|
134
|
+
*/
|
|
135
|
+
function raise(widths, idxs, target, slack) {
|
|
136
|
+
if (!(slack > 0)) return slack;
|
|
137
|
+
const wants = idxs.map((i) => Math.max(0, target(i) - widths[i]));
|
|
138
|
+
const need = wants.reduce((a, b) => a + b, 0);
|
|
139
|
+
if (!(need > 0)) return slack;
|
|
140
|
+
const give = Math.min(slack, need);
|
|
141
|
+
idxs.forEach((i, k) => { widths[i] += give * wants[k] / need; });
|
|
142
|
+
return slack - give;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Take up to `over` px off the columns in `idxs`, widest first, none below
|
|
147
|
+
* `low(i)`. A water level: every column above the level is cut down to it, and
|
|
148
|
+
* the level is solved so the total cut is exactly `over`, or everything the
|
|
149
|
+
* lows allow when that is less. Solved on real numbers: the result is exact,
|
|
150
|
+
* and a wider box can never make any column narrower.
|
|
151
|
+
*/
|
|
152
|
+
function lowerTo(widths, idxs, low, over) {
|
|
153
|
+
const floorOf = (i) => Math.min(widths[i], Math.max(0, low(i)));
|
|
154
|
+
const cand = idxs.filter((i) => widths[i] > floorOf(i));
|
|
155
|
+
if (!cand.length || over <= 0) return;
|
|
156
|
+
const floors = new Map(cand.map((i) => [i, floorOf(i)]));
|
|
157
|
+
const capacity = cand.reduce((a, i) => a + widths[i] - floors.get(i), 0);
|
|
158
|
+
if (capacity <= over) {
|
|
159
|
+
for (const i of cand) widths[i] = floors.get(i);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const cutAt = (level) => cand.reduce((a, i) => a + Math.max(0, widths[i] - Math.max(floors.get(i), level)), 0);
|
|
163
|
+
let lo = 0;
|
|
164
|
+
let hi = Math.max(...cand.map((i) => widths[i]));
|
|
165
|
+
for (let k = 0; k < 64; k++) {
|
|
166
|
+
const mid = (lo + hi) / 2;
|
|
167
|
+
if (cutAt(mid) > over) lo = mid; else hi = mid;
|
|
168
|
+
}
|
|
169
|
+
for (const i of cand) widths[i] = Math.min(widths[i], Math.max(floors.get(i), hi));
|
|
170
|
+
}
|