@xeplr/ui-table 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/src/CellZoom.jsx +77 -0
- package/src/XeplrTable.jsx +338 -22
- package/src/columnWidths.js +333 -0
- package/src/filters/FilterWrapper.jsx +79 -30
- package/src/index.js +14 -0
- package/src/tableStyles.js +236 -0
- package/src/useColumnWidths.js +280 -0
- package/src/useTableController.js +62 -6
- package/src/xeplr-table.css +151 -4
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-based column widths — the pure half.
|
|
3
|
+
*
|
|
4
|
+
* No React, no DOM: this module takes measured text widths in and gives
|
|
5
|
+
* assigned pixel widths out, so the decision it makes is testable without a
|
|
6
|
+
* browser. The DOM side (reading fonts, running canvas measureText, watching
|
|
7
|
+
* the container resize) lives in useColumnWidths.js.
|
|
8
|
+
*
|
|
9
|
+
* THE RULE, in one line: a column never gets less than it needs while some
|
|
10
|
+
* other part of the table is sitting on empty space.
|
|
11
|
+
*
|
|
12
|
+
* Concretely, given a container and a set of columns:
|
|
13
|
+
*
|
|
14
|
+
* 1. Everything fits -> every column gets its natural width, and whatever
|
|
15
|
+
* is left over stays empty. That leftover is the "blank column".
|
|
16
|
+
* 2. It doesn't fit -> there is NO blank column. Every column drops to its
|
|
17
|
+
* floor (the widest single word it must show without breaking mid-word),
|
|
18
|
+
* then the remaining space is handed back in proportion to how much each
|
|
19
|
+
* column actually wanted. A column that never wanted more than its floor
|
|
20
|
+
* -- a number under 100, say -- takes none of it, so all the slack goes
|
|
21
|
+
* to the column that needs it. Columns that end up short of natural wrap.
|
|
22
|
+
* 3. Not even the floors fit -> everyone gets their floor and the table
|
|
23
|
+
* scrolls horizontally.
|
|
24
|
+
*
|
|
25
|
+
* That ordering is what makes case 2 defensible: you can never see a narrow,
|
|
26
|
+
* wrapped column sitting next to blank space, because blank space only exists
|
|
27
|
+
* once every column is already satisfied.
|
|
28
|
+
*
|
|
29
|
+
* Every column comes back with a `reason`, so "why is this column this wide?"
|
|
30
|
+
* has an actual answer rather than a shrug.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Column got exactly what its content asked for. */
|
|
34
|
+
export var WIDTH_NATURAL = 'natural';
|
|
35
|
+
/** Column was squeezed below its natural width; its text wraps. */
|
|
36
|
+
export var WIDTH_SHRUNK = 'shrunk';
|
|
37
|
+
/** Column is at its floor and the table overflows; horizontal scroll. */
|
|
38
|
+
export var WIDTH_FLOOR = 'floor';
|
|
39
|
+
/** Column was given an explicit width by the caller; nothing was measured. */
|
|
40
|
+
export var WIDTH_PINNED = 'pinned';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A caller-supplied width for one column, BY POSITION, in PX.
|
|
44
|
+
*
|
|
45
|
+
* Px here even though the public API takes a percentage: this file returns
|
|
46
|
+
* pixel widths, so converting once at the boundary (useColumnWidths, which is
|
|
47
|
+
* the only place that knows the container) keeps every number below in one
|
|
48
|
+
* unit. A percentage this deep would mean two units in the same arithmetic.
|
|
49
|
+
*
|
|
50
|
+
* Positional on purpose, and it is worth saying why, because a key looks like
|
|
51
|
+
* the safer choice and is not.
|
|
52
|
+
*
|
|
53
|
+
* A pivot's columns ARE data: they are the distinct values of the pivot
|
|
54
|
+
* column, so today's `Q3 · Sum` is next quarter's `Q4 · Sum`. Key a width to
|
|
55
|
+
* that and it is lost on an ordinary refresh — nobody edited anything, the
|
|
56
|
+
* layout simply forgot. Position survives that, and only breaks when somebody
|
|
57
|
+
* deliberately restructures the report, where losing a width is a reasonable
|
|
58
|
+
* consequence of a deliberate act.
|
|
59
|
+
*
|
|
60
|
+
* Which is the real point: a width here belongs to the SLOT, not to whatever
|
|
61
|
+
* value lands in it. "The third column has to be wide" is a positional
|
|
62
|
+
* statement.
|
|
63
|
+
*
|
|
64
|
+
* Non-positive and non-finite entries are ignored rather than clamped — they
|
|
65
|
+
* mean the caller has a bug, and inventing a width would hide it.
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* { index: percent } -> { index: px }, against the width the columns divide.
|
|
69
|
+
*
|
|
70
|
+
* Lives here rather than in the hook so the conversion is testable without a
|
|
71
|
+
* browser, like every other decision in this file. The hook's only job is
|
|
72
|
+
* knowing what `available` is.
|
|
73
|
+
*
|
|
74
|
+
* `available` is the container MINUS the structural columns (expand arrow,
|
|
75
|
+
* checkbox, row actions), which is what makes 100 mean "all the room the
|
|
76
|
+
* columns actually have": two columns at 50 fill the table exactly whether or
|
|
77
|
+
* not there is a checkbox column in front of them. Against the raw container
|
|
78
|
+
* they would overflow by its width — not something anyone typing a percentage
|
|
79
|
+
* is thinking about.
|
|
80
|
+
*
|
|
81
|
+
* Junk is dropped rather than coerced, so a caller's bug stays visible as an
|
|
82
|
+
* unpinned (measured) column rather than as a silently invented width.
|
|
83
|
+
*/
|
|
84
|
+
export function resolvePercentWidths(overrides, available) {
|
|
85
|
+
if (!overrides || !(available > 0)) return null;
|
|
86
|
+
var out = {};
|
|
87
|
+
Object.keys(overrides).forEach(function(index) {
|
|
88
|
+
var percent = overrides[index];
|
|
89
|
+
if (typeof percent !== 'number' || !isFinite(percent) || percent <= 0) return;
|
|
90
|
+
out[index] = Math.round((percent / 100) * available);
|
|
91
|
+
});
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pinnedWidth(overrides, index) {
|
|
96
|
+
if (!overrides) return null;
|
|
97
|
+
var width = overrides[index];
|
|
98
|
+
if (typeof width !== 'number' || !isFinite(width) || width <= 0) return null;
|
|
99
|
+
return Math.ceil(width);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The longest run of characters with no break opportunity in it.
|
|
104
|
+
*
|
|
105
|
+
* This is a column's floor: text can wrap at spaces for free, so a column
|
|
106
|
+
* only *has* to be as wide as its widest single word. "New York City" can live
|
|
107
|
+
* in the width of "York"; a 60-character URL cannot be narrowed at all, which
|
|
108
|
+
* is why the caller caps this (see maxFloor).
|
|
109
|
+
*/
|
|
110
|
+
export function longestToken(text) {
|
|
111
|
+
if (!text) return '';
|
|
112
|
+
var parts = String(text).split(/\s+/);
|
|
113
|
+
var longest = '';
|
|
114
|
+
for (var i = 0; i < parts.length; i++) {
|
|
115
|
+
if (parts[i].length > longest.length) longest = parts[i];
|
|
116
|
+
}
|
|
117
|
+
return longest;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function clamp(value, low, high) {
|
|
121
|
+
if (value < low) return low;
|
|
122
|
+
if (value > high) return high;
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Turn text into per-column width demands.
|
|
128
|
+
*
|
|
129
|
+
* Deliberately takes `measure` as an argument rather than reaching for a
|
|
130
|
+
* canvas itself — that keeps this file pure, and lets tests substitute a
|
|
131
|
+
* monospace measurer whose numbers can be reasoned about by hand.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {Array<object>} opts.columns - Column descriptors (opaque here)
|
|
135
|
+
* @param {Array<object>} opts.rows - Sample rows to measure
|
|
136
|
+
* @param {Function} opts.textOf - (row, column) => displayed string.
|
|
137
|
+
* MUST return what the user actually sees: '₹36,260.80' is far wider than
|
|
138
|
+
* 36260.8, so measuring the raw value would size currency columns wrong.
|
|
139
|
+
* @param {Function} opts.headerOf - (column) => header string
|
|
140
|
+
* @param {Function} opts.keyOf - (column) => stable key
|
|
141
|
+
* @param {Function} opts.measure - (text, font) => px
|
|
142
|
+
* @param {string} opts.bodyFont - CSS font shorthand for cells
|
|
143
|
+
* @param {string} opts.headerFont - CSS font shorthand for headers
|
|
144
|
+
* @param {number} opts.cellPadding - Horizontal chrome around cell text
|
|
145
|
+
* @param {number} opts.headerPadding - Horizontal chrome around header text
|
|
146
|
+
* @param {number} opts.minWidth - No column is ever narrower than this
|
|
147
|
+
* @param {number} opts.maxWidth - Caps natural, so one essay column
|
|
148
|
+
* can't demand 4000px and push everything else off screen
|
|
149
|
+
* @param {number} opts.maxFloor - Caps the floor, so a single
|
|
150
|
+
* unbreakable 200-char token can't force horizontal scroll on its own; past
|
|
151
|
+
* this the browser breaks mid-word (overflow-wrap: anywhere)
|
|
152
|
+
* @param {object} [opts.overrides] - { columnIndex: px } — explicit
|
|
153
|
+
* widths. These columns are NOT measured, and NOT clamped to min/max: an
|
|
154
|
+
* instruction outranks a guess, including the guess about what a sensible
|
|
155
|
+
* minimum is.
|
|
156
|
+
* @returns {Array<{key, natural, floor, pinned?}>}
|
|
157
|
+
*/
|
|
158
|
+
export function measureColumnDemand(opts) {
|
|
159
|
+
var columns = opts.columns || [];
|
|
160
|
+
var rows = opts.rows || [];
|
|
161
|
+
var measure = opts.measure;
|
|
162
|
+
var cellPad = opts.cellPadding || 0;
|
|
163
|
+
var headPad = opts.headerPadding || 0;
|
|
164
|
+
var minWidth = opts.minWidth || 0;
|
|
165
|
+
var maxWidth = opts.maxWidth || Infinity;
|
|
166
|
+
var maxFloor = opts.maxFloor || maxWidth;
|
|
167
|
+
|
|
168
|
+
return columns.map(function(column, index) {
|
|
169
|
+
// PINNED COLUMNS ARE NEVER MEASURED. Not measured-then-overridden: the
|
|
170
|
+
// measuring is the expensive part (every sampled row, twice, through the
|
|
171
|
+
// caller's formatter), and its answer would be discarded. natural ===
|
|
172
|
+
// floor is what tells the allocator this column does not negotiate.
|
|
173
|
+
var pinned = pinnedWidth(opts.overrides, index);
|
|
174
|
+
if (pinned !== null) {
|
|
175
|
+
return { key: opts.keyOf(column), natural: pinned, floor: pinned, pinned: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var header = opts.headerOf(column) || '';
|
|
179
|
+
var natural = measure(header, opts.headerFont) + headPad;
|
|
180
|
+
var floor = measure(longestToken(header), opts.headerFont) + headPad;
|
|
181
|
+
|
|
182
|
+
for (var i = 0; i < rows.length; i++) {
|
|
183
|
+
var text = opts.textOf(rows[i], column);
|
|
184
|
+
if (text === null || text === undefined || text === '') continue;
|
|
185
|
+
text = String(text);
|
|
186
|
+
var full = measure(text, opts.bodyFont) + cellPad;
|
|
187
|
+
if (full > natural) natural = full;
|
|
188
|
+
// Skip the second measure when the text is a single word — then the
|
|
189
|
+
// longest token IS the whole string and we already have its width.
|
|
190
|
+
var token = longestToken(text);
|
|
191
|
+
var tokenWidth = token.length === text.length ? full : measure(token, opts.bodyFont) + cellPad;
|
|
192
|
+
if (tokenWidth > floor) floor = tokenWidth;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
natural = clamp(Math.ceil(natural), minWidth, maxWidth);
|
|
196
|
+
// The floor can never exceed natural — a column asking for less room than
|
|
197
|
+
// its own minimum is nonsense, and would break the surplus maths below.
|
|
198
|
+
floor = clamp(Math.ceil(floor), minWidth, Math.min(maxFloor, natural));
|
|
199
|
+
|
|
200
|
+
return { key: opts.keyOf(column), natural: natural, floor: floor };
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Hand out the container's width.
|
|
206
|
+
*
|
|
207
|
+
* @param {Array<{key, natural, floor, pinned}>} demands - `pinned` columns are
|
|
208
|
+
* held at their width and excluded from the negotiation entirely.
|
|
209
|
+
* @param {number} containerWidth - Available px. 0/undefined means "unknown",
|
|
210
|
+
* which is treated as unlimited: everyone gets natural and nothing is blank.
|
|
211
|
+
* @returns {{widths: number[], tableWidth: number, blankWidth: number,
|
|
212
|
+
* overflow: boolean, decisions: Array<{key, natural, floor, width, reason}>}}
|
|
213
|
+
*/
|
|
214
|
+
export function allocateColumnWidths(demands, containerWidth) {
|
|
215
|
+
demands = demands || [];
|
|
216
|
+
if (!demands.length) {
|
|
217
|
+
return { widths: [], tableWidth: 0, blankWidth: Math.max(0, containerWidth || 0), overflow: false, decisions: [] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Pinned columns come out of the negotiation entirely ──
|
|
221
|
+
//
|
|
222
|
+
// A pinned width is an instruction, so it must not be shrunk in case 2 or
|
|
223
|
+
// dropped to a floor in case 3. Taking them out FIRST and running the
|
|
224
|
+
// existing three cases over what remains is what keeps that true without
|
|
225
|
+
// threading a special case through the surplus maths — the allocator below
|
|
226
|
+
// simply never sees them.
|
|
227
|
+
var pinnedTotal = 0;
|
|
228
|
+
var free = [];
|
|
229
|
+
var freeIndex = [];
|
|
230
|
+
for (var p = 0; p < demands.length; p++) {
|
|
231
|
+
if (demands[p].pinned) { pinnedTotal += demands[p].natural; continue; }
|
|
232
|
+
free.push(demands[p]);
|
|
233
|
+
freeIndex.push(p);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (pinnedTotal > 0) {
|
|
237
|
+
var known = containerWidth > 0;
|
|
238
|
+
var remaining = known ? containerWidth - pinnedTotal : containerWidth;
|
|
239
|
+
|
|
240
|
+
// THE TRAP THIS AVOIDS: `remaining` can now go to zero or negative, and
|
|
241
|
+
// 0 is already this function's word for "container unknown, treat as
|
|
242
|
+
// unlimited". Falling through would hand every free column its natural
|
|
243
|
+
// width — the widest possible table — at exactly the moment there is no
|
|
244
|
+
// room at all. So it is answered here instead of reaching that branch.
|
|
245
|
+
if (known && remaining <= 0) {
|
|
246
|
+
var floored = demands.map(function(d) { return d.pinned ? d.natural : d.floor; });
|
|
247
|
+
return finish(demands, floored, containerWidth, true);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
var inner = allocateColumnWidths(free, remaining);
|
|
251
|
+
var merged = new Array(demands.length);
|
|
252
|
+
for (var m = 0; m < demands.length; m++) if (demands[m].pinned) merged[m] = demands[m].natural;
|
|
253
|
+
for (var f = 0; f < freeIndex.length; f++) merged[freeIndex[f]] = inner.widths[f];
|
|
254
|
+
return finish(demands, merged, containerWidth, inner.overflow);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
var naturalTotal = 0;
|
|
258
|
+
var floorTotal = 0;
|
|
259
|
+
for (var i = 0; i < demands.length; i++) {
|
|
260
|
+
naturalTotal += demands[i].natural;
|
|
261
|
+
floorTotal += demands[i].floor;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
var widths;
|
|
265
|
+
var overflow = false;
|
|
266
|
+
|
|
267
|
+
if (!containerWidth || containerWidth <= 0 || naturalTotal <= containerWidth) {
|
|
268
|
+
// Case 1 — everything fits. This is the ONLY branch that leaves blank space.
|
|
269
|
+
widths = demands.map(function(d) { return d.natural; });
|
|
270
|
+
} else if (floorTotal >= containerWidth) {
|
|
271
|
+
// Case 3 — not even the floors fit. Scroll rather than break words.
|
|
272
|
+
widths = demands.map(function(d) { return d.floor; });
|
|
273
|
+
overflow = true;
|
|
274
|
+
} else {
|
|
275
|
+
// Case 2 — squeeze. Start everyone at their floor, then redistribute the
|
|
276
|
+
// surplus in proportion to unmet demand, so a column that only ever wanted
|
|
277
|
+
// 46px keeps 46px and the wide column absorbs all the slack.
|
|
278
|
+
var surplus = containerWidth - floorTotal;
|
|
279
|
+
var demandTotal = naturalTotal - floorTotal; // > surplus > 0 in this branch
|
|
280
|
+
var used = 0;
|
|
281
|
+
widths = demands.map(function(d) {
|
|
282
|
+
var width = Math.floor(d.floor + ((d.natural - d.floor) / demandTotal) * surplus);
|
|
283
|
+
if (width > d.natural) width = d.natural; // float-rounding guard
|
|
284
|
+
used += width;
|
|
285
|
+
return width;
|
|
286
|
+
});
|
|
287
|
+
// Flooring each share loses a few px; give them to the hungriest column
|
|
288
|
+
// so the table lands exactly on the container edge rather than a pixel shy.
|
|
289
|
+
var remainder = containerWidth - used;
|
|
290
|
+
if (remainder > 0) {
|
|
291
|
+
var hungriest = 0;
|
|
292
|
+
for (var j = 1; j < demands.length; j++) {
|
|
293
|
+
if (demands[j].natural - widths[j] > demands[hungriest].natural - widths[hungriest]) hungriest = j;
|
|
294
|
+
}
|
|
295
|
+
widths[hungriest] += remainder;
|
|
296
|
+
if (widths[hungriest] > demands[hungriest].natural) widths[hungriest] = demands[hungriest].natural;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return finish(demands, widths, containerWidth, overflow);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The shared tail: totals, blank space, and a `reason` per column.
|
|
305
|
+
*
|
|
306
|
+
* One place, so the pinned path and the negotiated path cannot disagree about
|
|
307
|
+
* what a decision record looks like.
|
|
308
|
+
*/
|
|
309
|
+
function finish(demands, widths, containerWidth, overflow) {
|
|
310
|
+
var tableWidth = 0;
|
|
311
|
+
for (var k = 0; k < widths.length; k++) tableWidth += widths[k];
|
|
312
|
+
|
|
313
|
+
var decisions = demands.map(function(d, idx) {
|
|
314
|
+
var width = widths[idx];
|
|
315
|
+
var reason = WIDTH_NATURAL;
|
|
316
|
+
// Checked FIRST: a pinned column has natural === floor, so every test
|
|
317
|
+
// below would read it as "got what it asked for" and report `natural`.
|
|
318
|
+
// True, but it hides the one thing worth knowing — that this width was
|
|
319
|
+
// given, not measured, and is the caller's to change.
|
|
320
|
+
if (d.pinned) reason = WIDTH_PINNED;
|
|
321
|
+
else if (overflow && width <= d.floor && d.floor < d.natural) reason = WIDTH_FLOOR;
|
|
322
|
+
else if (width < d.natural) reason = WIDTH_SHRUNK;
|
|
323
|
+
return { key: d.key, natural: d.natural, floor: d.floor, width: width, reason: reason };
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
widths: widths,
|
|
328
|
+
tableWidth: tableWidth,
|
|
329
|
+
blankWidth: containerWidth > 0 ? Math.max(0, containerWidth - tableWidth) : 0,
|
|
330
|
+
overflow: overflow,
|
|
331
|
+
decisions: decisions
|
|
332
|
+
};
|
|
333
|
+
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Common filter popover shell.
|
|
5
6
|
* Renders a trigger icon and a dropdown panel that closes on outside click.
|
|
6
7
|
*
|
|
8
|
+
* PORTALLED TO document.body, and positioned from the trigger's own rect.
|
|
9
|
+
*
|
|
10
|
+
* It used to be `position: absolute; right: 0` inside the header cell, which
|
|
11
|
+
* put it at the mercy of every ancestor: a table scrolls (`overflow: auto`),
|
|
12
|
+
* so the panel was CLIPPED by the table it belongs to. On the first, narrow
|
|
13
|
+
* column a 220px panel anchored to the cell's right edge opened leftward past
|
|
14
|
+
* the table's left edge and had its labels sliced off — a filter list you
|
|
15
|
+
* cannot read is a filter you cannot use.
|
|
16
|
+
*
|
|
17
|
+
* A portal has no ancestors to be clipped by. The cost is that it no longer
|
|
18
|
+
* moves with the page on its own, so it closes on scroll and resize rather
|
|
19
|
+
* than hanging in the wrong place.
|
|
20
|
+
*
|
|
7
21
|
* @param {object} props
|
|
8
22
|
* @param {boolean} props.isActive - Whether filter is currently applied
|
|
9
23
|
* @param {React.ReactNode} props.children - Filter UI content
|
|
@@ -11,21 +25,77 @@ import React, { useState, useRef, useEffect } from 'react';
|
|
|
11
25
|
*/
|
|
12
26
|
export default function FilterWrapper({ isActive, children, onClear }) {
|
|
13
27
|
var [open, setOpen] = useState(false);
|
|
28
|
+
var [rect, setRect] = useState(null);
|
|
14
29
|
var wrapperRef = useRef(null);
|
|
30
|
+
var panelRef = useRef(null);
|
|
31
|
+
|
|
32
|
+
// Where the panel should sit, in VIEWPORT coordinates (position: fixed).
|
|
33
|
+
var place = useCallback(function() {
|
|
34
|
+
var el = wrapperRef.current;
|
|
35
|
+
if (!el) return;
|
|
36
|
+
var r = el.getBoundingClientRect();
|
|
37
|
+
var W = 240; // matches min-width in the stylesheet
|
|
38
|
+
var margin = 8;
|
|
39
|
+
// Right-aligned to the trigger by preference, because that reads as
|
|
40
|
+
// belonging to this column — but never off either edge of the window.
|
|
41
|
+
var left = Math.min(
|
|
42
|
+
Math.max(margin, r.right - W),
|
|
43
|
+
Math.max(margin, window.innerWidth - W - margin)
|
|
44
|
+
);
|
|
45
|
+
setRect({ top: r.bottom + 4, left: left, width: W });
|
|
46
|
+
}, []);
|
|
15
47
|
|
|
16
|
-
// Close on outside click
|
|
17
48
|
useEffect(function() {
|
|
18
|
-
if (!open) return;
|
|
49
|
+
if (!open) return undefined;
|
|
50
|
+
place();
|
|
19
51
|
|
|
20
52
|
function handleClick(e) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
53
|
+
var inTrigger = wrapperRef.current && wrapperRef.current.contains(e.target);
|
|
54
|
+
var inPanel = panelRef.current && panelRef.current.contains(e.target);
|
|
55
|
+
if (!inTrigger && !inPanel) setOpen(false);
|
|
24
56
|
}
|
|
57
|
+
// A portalled panel does not travel with what it is anchored to, so
|
|
58
|
+
// rather than let it hang somewhere wrong it closes. `true` catches
|
|
59
|
+
// scrolling in the TABLE, not just the window — the scroll event does not
|
|
60
|
+
// bubble, so a listener without capture never hears it.
|
|
61
|
+
function handleScroll() { setOpen(false); }
|
|
25
62
|
|
|
26
63
|
document.addEventListener('mousedown', handleClick);
|
|
27
|
-
|
|
28
|
-
|
|
64
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
65
|
+
window.addEventListener('resize', handleScroll);
|
|
66
|
+
return function() {
|
|
67
|
+
document.removeEventListener('mousedown', handleClick);
|
|
68
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
69
|
+
window.removeEventListener('resize', handleScroll);
|
|
70
|
+
};
|
|
71
|
+
}, [open, place]);
|
|
72
|
+
|
|
73
|
+
var panel = open && rect ? createPortal(
|
|
74
|
+
<div
|
|
75
|
+
ref={panelRef}
|
|
76
|
+
className="xeplr-table-filter-panel"
|
|
77
|
+
style={{ position: 'fixed', top: rect.top, left: rect.left, width: rect.width }}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
<div className="xeplr-table-filter-actions">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
className="xeplr-table-filter-clear-btn"
|
|
84
|
+
onClick={function() { onClear(); setOpen(false); }}
|
|
85
|
+
>
|
|
86
|
+
Clear
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
className="xeplr-table-filter-close-btn"
|
|
91
|
+
onClick={function() { setOpen(false); }}
|
|
92
|
+
>
|
|
93
|
+
Done
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>,
|
|
97
|
+
document.body
|
|
98
|
+
) : null;
|
|
29
99
|
|
|
30
100
|
return (
|
|
31
101
|
<div className="xeplr-table-filter-wrapper" ref={wrapperRef}>
|
|
@@ -39,28 +109,7 @@ export default function FilterWrapper({ isActive, children, onClear }) {
|
|
|
39
109
|
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
40
110
|
</svg>
|
|
41
111
|
</button>
|
|
42
|
-
|
|
43
|
-
{open && (
|
|
44
|
-
<div className="xeplr-table-filter-panel">
|
|
45
|
-
{children}
|
|
46
|
-
<div className="xeplr-table-filter-actions">
|
|
47
|
-
<button
|
|
48
|
-
type="button"
|
|
49
|
-
className="xeplr-table-filter-clear-btn"
|
|
50
|
-
onClick={function() { onClear(); setOpen(false); }}
|
|
51
|
-
>
|
|
52
|
-
Clear
|
|
53
|
-
</button>
|
|
54
|
-
<button
|
|
55
|
-
type="button"
|
|
56
|
-
className="xeplr-table-filter-close-btn"
|
|
57
|
-
onClick={function() { setOpen(false); }}
|
|
58
|
-
>
|
|
59
|
-
Done
|
|
60
|
-
</button>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
112
|
+
{panel}
|
|
64
113
|
</div>
|
|
65
114
|
);
|
|
66
115
|
}
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { default as RecordModal } from './actions/RecordModal.jsx';
|
|
|
21
21
|
export { default as ChildTable } from './actions/ChildTable.jsx';
|
|
22
22
|
export { default as RecordDetail } from './actions/RecordDetail.jsx';
|
|
23
23
|
export { CHILD_DISPLAY } from './XeplrTable.jsx';
|
|
24
|
+
export { default as CellZoom } from './CellZoom.jsx';
|
|
24
25
|
|
|
25
26
|
// Change set builder
|
|
26
27
|
export { buildChangeSet } from './buildChangeSet.js';
|
|
@@ -31,6 +32,19 @@ export { resolve as resolveOperator, resolveString, resolveNumber, resolveDate,
|
|
|
31
32
|
// Conditional formatting
|
|
32
33
|
export { resolveCellStyle } from './resolveCellStyle.js';
|
|
33
34
|
|
|
35
|
+
// Style vocabulary + layer resolution (theme -> overrides)
|
|
36
|
+
export {
|
|
37
|
+
STYLE_GROUPS, STYLE_FIELDS, BORDER_SIDES, styleField, cssLength,
|
|
38
|
+
flattenStyle, resolveStyle, toCssProperties, resolveCellCss
|
|
39
|
+
} from './tableStyles.js';
|
|
40
|
+
|
|
41
|
+
// Content-based column widths
|
|
42
|
+
export { default as useColumnWidths } from './useColumnWidths.js';
|
|
43
|
+
export {
|
|
44
|
+
measureColumnDemand, allocateColumnWidths, resolvePercentWidths, longestToken,
|
|
45
|
+
WIDTH_NATURAL, WIDTH_SHRUNK, WIDTH_FLOOR
|
|
46
|
+
} from './columnWidths.js';
|
|
47
|
+
|
|
34
48
|
// Cell renderers (config-driven)
|
|
35
49
|
export { default as renderers } from './renderers/index.js';
|
|
36
50
|
export { avatarName, tags, currency, memberChips, twoLine, statusBadge, dateDisplay, link } from './renderers/index.js';
|