@freegridjs/core 0.8.1 → 1.0.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/LICENSE +3 -1
- package/README.md +14 -11
- package/dist/cdn/freegrid.css +49 -0
- package/dist/cdn/freegrid.esm.js +17 -8
- package/dist/cdn/freegrid.min.js +17 -8
- package/dist/index.d.ts +56 -39
- package/dist/index.js +2117 -396
- package/dist/locales/de.d.ts +1 -1
- package/dist/locales/de.js +8 -0
- package/dist/locales/en.d.ts +1 -1
- package/dist/locales/en.js +8 -0
- package/dist/locales/es.d.ts +1 -1
- package/dist/locales/es.js +8 -0
- package/dist/locales/fr.d.ts +1 -1
- package/dist/locales/fr.js +8 -0
- package/dist/locales/id.d.ts +5 -0
- package/dist/locales/id.js +113 -0
- package/dist/locales/it.d.ts +5 -0
- package/dist/locales/it.js +113 -0
- package/dist/locales/ja.d.ts +1 -1
- package/dist/locales/ja.js +8 -0
- package/dist/locales/ko.d.ts +1 -1
- package/dist/locales/ko.js +8 -0
- package/dist/locales/pt-BR.d.ts +5 -0
- package/dist/locales/pt-BR.js +113 -0
- package/dist/locales/ru.d.ts +5 -0
- package/dist/locales/ru.js +113 -0
- package/dist/locales/th.d.ts +5 -0
- package/dist/locales/th.js +113 -0
- package/dist/locales/vi.d.ts +5 -0
- package/dist/locales/vi.js +113 -0
- package/dist/locales/zh-CN.d.ts +1 -1
- package/dist/locales/zh-CN.js +8 -0
- package/dist/locales/zh-TW.d.ts +5 -0
- package/dist/locales/zh-TW.js +113 -0
- package/dist/styles.css +49 -0
- package/dist/{types-e45suTSB.d.ts → types-bFz-Q6ac.d.ts} +226 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,7 +32,20 @@ var DRAG_COLUMN = {
|
|
|
32
32
|
filter: false,
|
|
33
33
|
chooser: false
|
|
34
34
|
};
|
|
35
|
-
var
|
|
35
|
+
var ROWNUM_COL_ID = "__fg_rownum__";
|
|
36
|
+
var rowNumberColumn = (width = 52, header2 = "#") => ({
|
|
37
|
+
id: ROWNUM_COL_ID,
|
|
38
|
+
header: header2,
|
|
39
|
+
width,
|
|
40
|
+
minWidth: width,
|
|
41
|
+
maxWidth: width,
|
|
42
|
+
pinned: "left",
|
|
43
|
+
sortable: false,
|
|
44
|
+
resizable: false,
|
|
45
|
+
filter: false,
|
|
46
|
+
chooser: false
|
|
47
|
+
});
|
|
48
|
+
var isSystemCol = (id) => id === SELECT_COL_ID || id === DRAG_COL_ID || id === ROWNUM_COL_ID;
|
|
36
49
|
var DEFAULT_WIDTH = 120;
|
|
37
50
|
var DEFAULT_MIN_WIDTH = 40;
|
|
38
51
|
var REGIONS = ["left", "center", "right"];
|
|
@@ -107,7 +120,8 @@ var ColumnModel = class {
|
|
|
107
120
|
width: clampWidth(def, def.width ?? DEFAULT_WIDTH),
|
|
108
121
|
flex: def.flex && def.flex > 0 ? def.flex : null,
|
|
109
122
|
hide: def.hide ?? false,
|
|
110
|
-
pinned: def.pinned ?? parent?.pinned ?? null
|
|
123
|
+
pinned: def.pinned ?? parent?.pinned ?? null,
|
|
124
|
+
open: !!def.openByDefault
|
|
111
125
|
};
|
|
112
126
|
this.byId.set(id, node);
|
|
113
127
|
if (isGroup) node.children = valid(def.children).map((c) => build(c, node, depth + 1));
|
|
@@ -228,7 +242,7 @@ var ColumnModel = class {
|
|
|
228
242
|
return cells;
|
|
229
243
|
}
|
|
230
244
|
getState() {
|
|
231
|
-
return this.leafNodes().filter((n) => !isSystemCol(n.id)).map((n) => ({ colId: n.id, width: this.leafById.get(n.id)?.width ?? n.width, hide: n.hide, pinned: n.pinned }));
|
|
245
|
+
return this.leafNodes().filter((n) => !isSystemCol(n.id)).map((n) => ({ colId: n.id, width: this.leafById.get(n.id)?.width ?? n.width, hide: !!n.hide, pinned: n.pinned ?? null }));
|
|
232
246
|
}
|
|
233
247
|
applyState(state) {
|
|
234
248
|
const rank = /* @__PURE__ */ new Map();
|
|
@@ -236,11 +250,14 @@ var ColumnModel = class {
|
|
|
236
250
|
rank.set(s.colId, i);
|
|
237
251
|
const node = this.byId.get(s.colId);
|
|
238
252
|
if (!node || node.children || isSystemCol(s.colId)) return;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
253
|
+
if (typeof s.width === "number" && Number.isFinite(s.width)) {
|
|
254
|
+
node.flex = null;
|
|
255
|
+
node.width = clampWidth(node.def, s.width);
|
|
256
|
+
}
|
|
257
|
+
if (typeof s.hide === "boolean") node.hide = s.hide;
|
|
258
|
+
if ("pinned" in s) node.pinned = s.pinned === "left" || s.pinned === "right" ? s.pinned : null;
|
|
243
259
|
});
|
|
260
|
+
rank.set(ROWNUM_COL_ID, -3);
|
|
244
261
|
rank.set(DRAG_COL_ID, -2);
|
|
245
262
|
rank.set(SELECT_COL_ID, -1);
|
|
246
263
|
const nodeRank = (n) => n.children ? Math.min(Infinity, ...n.children.map(nodeRank)) : rank.get(n.id) ?? Infinity;
|
|
@@ -259,8 +276,33 @@ var ColumnModel = class {
|
|
|
259
276
|
this.roots_.forEach(walk);
|
|
260
277
|
return out;
|
|
261
278
|
}
|
|
279
|
+
/** A group whose children use columnGroupShow, so its header gets an open/close toggle. */
|
|
280
|
+
isCollapsible(node) {
|
|
281
|
+
return !!node.children?.some((c) => c.def.columnGroupShow === "open" || c.def.columnGroupShow === "closed");
|
|
282
|
+
}
|
|
283
|
+
setGroupOpen(id, open) {
|
|
284
|
+
const node = this.byId.get(id);
|
|
285
|
+
if (!node?.children || node.open === open) return false;
|
|
286
|
+
node.open = open;
|
|
287
|
+
this.relayout();
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
/** Every collapsible group and whether it is open. */
|
|
291
|
+
groupOpenState() {
|
|
292
|
+
const out = {};
|
|
293
|
+
for (const n of this.byId.values()) if (this.isCollapsible(n)) out[n.id] = n.open;
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
/** columnGroupShow: hidden while its group (or any group above) is in the other state. */
|
|
297
|
+
shownByGroups(n) {
|
|
298
|
+
for (let c = n; c?.parent; c = c.parent) {
|
|
299
|
+
const show = c.def.columnGroupShow;
|
|
300
|
+
if (show === "open" && !c.parent.open || show === "closed" && c.parent.open) return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
262
304
|
relayout() {
|
|
263
|
-
const visible = this.leafNodes().filter((n) => !n.hide);
|
|
305
|
+
const visible = this.leafNodes().filter((n) => !n.hide && this.shownByGroups(n));
|
|
264
306
|
const fixed = visible.reduce((s, n) => s + (n.flex ? 0 : n.width), 0);
|
|
265
307
|
const totalFlex = visible.reduce((s, n) => s + (n.flex ?? 0), 0);
|
|
266
308
|
const free = Math.max(0, this.containerWidth - fixed);
|
|
@@ -282,215 +324,6 @@ var ColumnModel = class {
|
|
|
282
324
|
}
|
|
283
325
|
};
|
|
284
326
|
|
|
285
|
-
// src/rows/filters.ts
|
|
286
|
-
function isBlank(v) {
|
|
287
|
-
return v == null || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
|
|
288
|
-
}
|
|
289
|
-
var pad = (n) => String(n).padStart(2, "0");
|
|
290
|
-
function toDateKey(v) {
|
|
291
|
-
if (isBlank(v)) return null;
|
|
292
|
-
if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
|
|
293
|
-
const d = v instanceof Date ? v : new Date(v);
|
|
294
|
-
if (Number.isNaN(d.getTime())) return null;
|
|
295
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
296
|
-
}
|
|
297
|
-
function setKey(v) {
|
|
298
|
-
if (isBlank(v)) return "";
|
|
299
|
-
if (v instanceof Date) return toDateKey(v) ?? "";
|
|
300
|
-
return String(v);
|
|
301
|
-
}
|
|
302
|
-
var all = () => true;
|
|
303
|
-
function buildPredicate(model) {
|
|
304
|
-
switch (model.type) {
|
|
305
|
-
case "text": {
|
|
306
|
-
const q = (model.value ?? "").toLowerCase();
|
|
307
|
-
const s = (v) => isBlank(v) ? "" : String(v).toLowerCase();
|
|
308
|
-
switch (model.op) {
|
|
309
|
-
case "contains":
|
|
310
|
-
return (v) => s(v).includes(q);
|
|
311
|
-
case "equals":
|
|
312
|
-
return (v) => s(v) === q;
|
|
313
|
-
case "notEquals":
|
|
314
|
-
return (v) => s(v) !== q;
|
|
315
|
-
case "startsWith":
|
|
316
|
-
return (v) => s(v).startsWith(q);
|
|
317
|
-
case "endsWith":
|
|
318
|
-
return (v) => s(v).endsWith(q);
|
|
319
|
-
case "blank":
|
|
320
|
-
return isBlank;
|
|
321
|
-
case "notBlank":
|
|
322
|
-
return (v) => !isBlank(v);
|
|
323
|
-
}
|
|
324
|
-
return all;
|
|
325
|
-
}
|
|
326
|
-
case "number": {
|
|
327
|
-
const n = (v) => isBlank(v) ? NaN : Number(v);
|
|
328
|
-
if (model.op === "blank") return (v) => Number.isNaN(n(v));
|
|
329
|
-
if (model.op === "notBlank") return (v) => !Number.isNaN(n(v));
|
|
330
|
-
const a = model.value;
|
|
331
|
-
if (a == null || Number.isNaN(a)) return all;
|
|
332
|
-
switch (model.op) {
|
|
333
|
-
case "eq":
|
|
334
|
-
return (v) => n(v) === a;
|
|
335
|
-
case "ne":
|
|
336
|
-
return (v) => n(v) !== a;
|
|
337
|
-
case "lt":
|
|
338
|
-
return (v) => n(v) < a;
|
|
339
|
-
case "lte":
|
|
340
|
-
return (v) => n(v) <= a;
|
|
341
|
-
case "gt":
|
|
342
|
-
return (v) => n(v) > a;
|
|
343
|
-
case "gte":
|
|
344
|
-
return (v) => n(v) >= a;
|
|
345
|
-
case "between": {
|
|
346
|
-
const b = model.valueTo;
|
|
347
|
-
if (b == null || Number.isNaN(b)) return all;
|
|
348
|
-
const lo = Math.min(a, b), hi = Math.max(a, b);
|
|
349
|
-
return (v) => {
|
|
350
|
-
const x = n(v);
|
|
351
|
-
return x >= lo && x <= hi;
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return all;
|
|
356
|
-
}
|
|
357
|
-
case "date": {
|
|
358
|
-
if (model.op === "blank") return (v) => toDateKey(v) === null;
|
|
359
|
-
if (model.op === "notBlank") return (v) => toDateKey(v) !== null;
|
|
360
|
-
const a = model.value;
|
|
361
|
-
if (!a) return all;
|
|
362
|
-
const k = (v) => toDateKey(v);
|
|
363
|
-
switch (model.op) {
|
|
364
|
-
case "equals":
|
|
365
|
-
return (v) => k(v) === a;
|
|
366
|
-
case "before":
|
|
367
|
-
return (v) => {
|
|
368
|
-
const x = k(v);
|
|
369
|
-
return x !== null && x < a;
|
|
370
|
-
};
|
|
371
|
-
case "after":
|
|
372
|
-
return (v) => {
|
|
373
|
-
const x = k(v);
|
|
374
|
-
return x !== null && x > a;
|
|
375
|
-
};
|
|
376
|
-
case "between": {
|
|
377
|
-
const b = model.valueTo;
|
|
378
|
-
if (!b) return all;
|
|
379
|
-
const lo = a < b ? a : b, hi = a < b ? b : a;
|
|
380
|
-
return (v) => {
|
|
381
|
-
const x = k(v);
|
|
382
|
-
return x !== null && x >= lo && x <= hi;
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return all;
|
|
387
|
-
}
|
|
388
|
-
case "set": {
|
|
389
|
-
const allowed = new Set(model.values.map(setKey));
|
|
390
|
-
return (v) => allowed.has(setKey(v));
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return all;
|
|
394
|
-
}
|
|
395
|
-
function quickTokens(q) {
|
|
396
|
-
const t = q.trim().toLowerCase();
|
|
397
|
-
return t ? t.split(/\s+/) : [];
|
|
398
|
-
}
|
|
399
|
-
function matchesQuick(tokens, text2) {
|
|
400
|
-
return tokens.every((t) => text2.includes(t));
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// src/i18n/format.ts
|
|
404
|
-
var cache = /* @__PURE__ */ new Map();
|
|
405
|
-
function parseNumberPattern(p) {
|
|
406
|
-
if (p === "integer") return { maximumFractionDigits: 0 };
|
|
407
|
-
if (p === "percent") return { style: "percent" };
|
|
408
|
-
if (p.startsWith("currency:")) return { style: "currency", currency: p.slice(9) };
|
|
409
|
-
if (!/^[#0,]*(\.[#0]+)?%?$/.test(p) || !/[#0]/.test(p)) return null;
|
|
410
|
-
const pct = p.endsWith("%");
|
|
411
|
-
const body = pct ? p.slice(0, -1) : p;
|
|
412
|
-
const frac = body.includes(".") ? body.split(".")[1] : "";
|
|
413
|
-
return {
|
|
414
|
-
useGrouping: body.includes(","),
|
|
415
|
-
minimumFractionDigits: (frac.match(/0/g) ?? []).length,
|
|
416
|
-
maximumFractionDigits: frac.length,
|
|
417
|
-
...pct ? { style: "percent" } : {}
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
var DATE_PRESETS = {
|
|
421
|
-
date: { year: "numeric", month: "2-digit", day: "2-digit" },
|
|
422
|
-
datetime: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" },
|
|
423
|
-
time: { hour: "2-digit", minute: "2-digit" },
|
|
424
|
-
short: { dateStyle: "short" },
|
|
425
|
-
medium: { dateStyle: "medium" },
|
|
426
|
-
long: { dateStyle: "long" }
|
|
427
|
-
};
|
|
428
|
-
function toDate(v) {
|
|
429
|
-
if (isBlank(v)) return null;
|
|
430
|
-
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
|
|
431
|
-
if (typeof v === "string") {
|
|
432
|
-
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
|
|
433
|
-
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
434
|
-
}
|
|
435
|
-
if (typeof v !== "string" && typeof v !== "number") return null;
|
|
436
|
-
const d = new Date(v);
|
|
437
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
438
|
-
}
|
|
439
|
-
function create(type, format, locale) {
|
|
440
|
-
switch (type) {
|
|
441
|
-
case "number": {
|
|
442
|
-
let opts = {};
|
|
443
|
-
if (typeof format === "string") {
|
|
444
|
-
const parsed = parseNumberPattern(format);
|
|
445
|
-
if (!parsed) warn(`unknown number format "${format}"`);
|
|
446
|
-
opts = parsed ?? {};
|
|
447
|
-
} else if (format) opts = format;
|
|
448
|
-
const nf = new Intl.NumberFormat(locale, opts);
|
|
449
|
-
return (v) => {
|
|
450
|
-
if (isBlank(v)) return "";
|
|
451
|
-
const n = typeof v === "number" ? v : Number(v);
|
|
452
|
-
return Number.isNaN(n) ? String(v) : nf.format(n);
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
case "date": {
|
|
456
|
-
if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
|
|
457
|
-
const pad3 = (n) => String(n).padStart(2, "0");
|
|
458
|
-
const withTime = format === "datetime";
|
|
459
|
-
return (v) => {
|
|
460
|
-
const d = toDate(v);
|
|
461
|
-
if (!d) return isBlank(v) ? "" : String(v);
|
|
462
|
-
const day = `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}`;
|
|
463
|
-
return withTime ? `${day} ${pad3(d.getHours())}:${pad3(d.getMinutes())}` : day;
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
let opts = DATE_PRESETS.date;
|
|
467
|
-
if (typeof format === "string") {
|
|
468
|
-
if (!DATE_PRESETS[format]) warn(`unknown date format "${format}"`);
|
|
469
|
-
opts = DATE_PRESETS[format] ?? DATE_PRESETS.date;
|
|
470
|
-
} else if (format) opts = format;
|
|
471
|
-
const df = new Intl.DateTimeFormat(locale, opts);
|
|
472
|
-
return (v) => {
|
|
473
|
-
const d = toDate(v);
|
|
474
|
-
return d ? df.format(d) : isBlank(v) ? "" : String(v);
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
case "boolean":
|
|
478
|
-
return (v) => v === true ? "\u2713" : v === false ? "\u2717" : "";
|
|
479
|
-
default:
|
|
480
|
-
return (v) => isBlank(v) ? "" : v instanceof Date ? v.toISOString() : String(v);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
function getFormatter(def, locale) {
|
|
484
|
-
const f = def.format;
|
|
485
|
-
const key = `${locale}|${def.type ?? "text"}|${typeof f === "string" ? f : JSON.stringify(f ?? null)}`;
|
|
486
|
-
let fmt = cache.get(key);
|
|
487
|
-
if (!fmt) {
|
|
488
|
-
fmt = create(def.type, f, locale);
|
|
489
|
-
cache.set(key, fmt);
|
|
490
|
-
}
|
|
491
|
-
return fmt;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
327
|
// src/formula.ts
|
|
495
328
|
var FormulaError = class extends Error {
|
|
496
329
|
constructor(code) {
|
|
@@ -654,66 +487,509 @@ var Parser = class {
|
|
|
654
487
|
if (!this.take(")")) throw new FormulaError("#ERROR!");
|
|
655
488
|
return v;
|
|
656
489
|
}
|
|
657
|
-
const name = t.v;
|
|
658
|
-
if (this.peek("(")) {
|
|
659
|
-
this.i++;
|
|
660
|
-
const args = [];
|
|
661
|
-
if (!this.take(")")) {
|
|
662
|
-
do
|
|
663
|
-
args.push(this.comparison());
|
|
664
|
-
while (this.take(","));
|
|
665
|
-
if (!this.take(")")) throw new FormulaError("#ERROR!");
|
|
666
|
-
}
|
|
667
|
-
const upper = name.toUpperCase();
|
|
668
|
-
if (upper === "IF") {
|
|
669
|
-
if (args.length < 2) throw new FormulaError("#ERROR!");
|
|
670
|
-
const cond = typeof args[0] === "boolean" ? args[0] : num(args[0]) !== 0;
|
|
671
|
-
return cond ? args[1] : args.length > 2 ? args[2] : false;
|
|
490
|
+
const name = t.v;
|
|
491
|
+
if (this.peek("(")) {
|
|
492
|
+
this.i++;
|
|
493
|
+
const args = [];
|
|
494
|
+
if (!this.take(")")) {
|
|
495
|
+
do
|
|
496
|
+
args.push(this.comparison());
|
|
497
|
+
while (this.take(","));
|
|
498
|
+
if (!this.take(")")) throw new FormulaError("#ERROR!");
|
|
499
|
+
}
|
|
500
|
+
const upper = name.toUpperCase();
|
|
501
|
+
if (upper === "IF") {
|
|
502
|
+
if (args.length < 2) throw new FormulaError("#ERROR!");
|
|
503
|
+
const cond = typeof args[0] === "boolean" ? args[0] : num(args[0]) !== 0;
|
|
504
|
+
return cond ? args[1] : args.length > 2 ? args[2] : false;
|
|
505
|
+
}
|
|
506
|
+
const fn = Object.hasOwn(FUNCS, upper) ? FUNCS[upper] : void 0;
|
|
507
|
+
if (!fn) throw new FormulaError("#NAME?");
|
|
508
|
+
return fn(args);
|
|
509
|
+
}
|
|
510
|
+
if (/^(true|false)$/i.test(name)) return name.toLowerCase() === "true";
|
|
511
|
+
return this.resolve(name);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var MAX_DEPTH = 32;
|
|
515
|
+
function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0, memo = /* @__PURE__ */ new Map()) {
|
|
516
|
+
try {
|
|
517
|
+
const resolve = (name) => {
|
|
518
|
+
if (!row || !Object.hasOwn(row, name)) throw new FormulaError("#REF!");
|
|
519
|
+
const v2 = row[name];
|
|
520
|
+
if (typeof v2 === "string" && v2.startsWith("=")) {
|
|
521
|
+
if (seen2.has(name) || depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
|
|
522
|
+
let r;
|
|
523
|
+
if (memo.has(name)) r = memo.get(name);
|
|
524
|
+
else {
|
|
525
|
+
r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1, memo);
|
|
526
|
+
memo.set(name, r);
|
|
527
|
+
}
|
|
528
|
+
if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
|
|
529
|
+
return r;
|
|
530
|
+
}
|
|
531
|
+
return v2;
|
|
532
|
+
};
|
|
533
|
+
const v = new Parser(tokenize(formula.slice(1)), resolve).parse();
|
|
534
|
+
if (typeof v === "number" && !Number.isFinite(v)) return "#NUM!";
|
|
535
|
+
return v;
|
|
536
|
+
} catch (e) {
|
|
537
|
+
return e instanceof FormulaError ? e.code : "#ERROR!";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
|
|
541
|
+
|
|
542
|
+
// src/rows/values.ts
|
|
543
|
+
function getCellValue(def, row) {
|
|
544
|
+
if (def.valueGetter) {
|
|
545
|
+
try {
|
|
546
|
+
return def.valueGetter(row);
|
|
547
|
+
} catch {
|
|
548
|
+
warn(`valueGetter of column "${def.id ?? def.field}" threw; rendering an empty cell`);
|
|
549
|
+
return void 0;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (def.field == null) return void 0;
|
|
553
|
+
const raw = row[def.field];
|
|
554
|
+
return def.formulas && isFormula(raw) ? evaluateFormula(raw, row, /* @__PURE__ */ new Set([def.field])) : raw;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/interactions/filterRow.ts
|
|
558
|
+
var REGIONS2 = ["left", "center", "right"];
|
|
559
|
+
var NUM_OPS = [[">=", "gte"], ["<=", "lte"], ["<>", "ne"], ["!=", "ne"], [">", "gt"], ["<", "lt"], ["=", "eq"]];
|
|
560
|
+
var DATE_OPS = [[">", "after"], ["<", "before"], ["=", "equals"]];
|
|
561
|
+
var RANGE = /^(.+?)\s*(?:~|\.\.)\s*(.+)$/;
|
|
562
|
+
var toNumber = (s) => {
|
|
563
|
+
const n = Number(s.replace(/[,\s₩$€¥]/g, ""));
|
|
564
|
+
return s.trim() !== "" && Number.isFinite(n) ? n : null;
|
|
565
|
+
};
|
|
566
|
+
var toDateKey = (s) => {
|
|
567
|
+
const m = /^(\d{4})[-./]\s*(\d{1,2})[-./]\s*(\d{1,2})\.?$/.exec(s.trim());
|
|
568
|
+
if (!m) return null;
|
|
569
|
+
const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
|
|
570
|
+
if (mo < 1 || mo > 12 || d < 1 || d > new Date(Date.UTC(y, mo, 0)).getUTCDate()) return null;
|
|
571
|
+
return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
|
|
572
|
+
};
|
|
573
|
+
function parseFilterText(type, text2) {
|
|
574
|
+
const t = text2.trim();
|
|
575
|
+
if (!t) return null;
|
|
576
|
+
if (type === "text") return { type: "text", op: "contains", value: t };
|
|
577
|
+
const range = RANGE.exec(t);
|
|
578
|
+
if (type === "number") {
|
|
579
|
+
if (range) {
|
|
580
|
+
const a = toNumber(range[1]), b = toNumber(range[2]);
|
|
581
|
+
return a === null || b === null ? void 0 : { type: "number", op: "between", value: Math.min(a, b), valueTo: Math.max(a, b) };
|
|
582
|
+
}
|
|
583
|
+
const op2 = NUM_OPS.find(([p]) => t.startsWith(p));
|
|
584
|
+
const n = toNumber(op2 ? t.slice(op2[0].length) : t);
|
|
585
|
+
return n === null ? void 0 : { type: "number", op: op2 ? op2[1] : "eq", value: n };
|
|
586
|
+
}
|
|
587
|
+
if (range) {
|
|
588
|
+
const a = toDateKey(range[1]), b = toDateKey(range[2]);
|
|
589
|
+
return !a || !b ? void 0 : { type: "date", op: "between", value: a < b ? a : b, valueTo: a < b ? b : a };
|
|
590
|
+
}
|
|
591
|
+
const op = DATE_OPS.find(([p]) => t.startsWith(p));
|
|
592
|
+
const d = toDateKey(op ? t.slice(op[0].length) : t);
|
|
593
|
+
return d ? { type: "date", op: op ? op[1] : "equals", value: d } : void 0;
|
|
594
|
+
}
|
|
595
|
+
function filterText(model) {
|
|
596
|
+
if (!model) return "";
|
|
597
|
+
if (model.type === "text") return model.op === "contains" ? model.value ?? "" : "";
|
|
598
|
+
if (model.type === "number") {
|
|
599
|
+
if (model.op === "between") return `${model.value}~${model.valueTo}`;
|
|
600
|
+
const sym = NUM_OPS.find(([, op]) => op === model.op)?.[0];
|
|
601
|
+
return sym && model.value !== void 0 ? `${sym === "=" ? "" : sym}${model.value}` : "";
|
|
602
|
+
}
|
|
603
|
+
if (model.type === "date") {
|
|
604
|
+
if (model.op === "between") return `${model.value}~${model.valueTo}`;
|
|
605
|
+
const sym = DATE_OPS.find(([, op]) => op === model.op)?.[0];
|
|
606
|
+
return sym && model.value ? `${sym === "=" ? "" : sym}${model.value}` : "";
|
|
607
|
+
}
|
|
608
|
+
return "";
|
|
609
|
+
}
|
|
610
|
+
var FilterRow = class {
|
|
611
|
+
constructor(grid, panes) {
|
|
612
|
+
this.grid = grid;
|
|
613
|
+
this.layout = "";
|
|
614
|
+
this.timers = /* @__PURE__ */ new Map();
|
|
615
|
+
this.data = null;
|
|
616
|
+
this.synced = null;
|
|
617
|
+
this.rebuilt = false;
|
|
618
|
+
this.inners = {};
|
|
619
|
+
for (const r of REGIONS2) {
|
|
620
|
+
this.inners[r] = el("div", "fg-filter-row", { role: "row" });
|
|
621
|
+
panes[r].append(this.inners[r]);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
get height() {
|
|
625
|
+
return Math.round(this.grid.renderer.opts.rowHeight * 0.95);
|
|
626
|
+
}
|
|
627
|
+
render() {
|
|
628
|
+
const { columns, store } = this.grid;
|
|
629
|
+
const h = this.height;
|
|
630
|
+
const cols = columns.displayed();
|
|
631
|
+
const key = `${h}|${store.state.locale.code}|${cols.map((c) => `${c.id}:${c.left}:${c.width}:${c.region}`).join(",")}`;
|
|
632
|
+
if (key !== this.layout) {
|
|
633
|
+
this.layout = key;
|
|
634
|
+
for (const r of REGIONS2) {
|
|
635
|
+
const inner = this.inners[r];
|
|
636
|
+
inner.style.width = `${columns.regionWidth(r)}px`;
|
|
637
|
+
inner.style.height = `${h}px`;
|
|
638
|
+
inner.replaceChildren(...columns.region(r).map((c) => this.cell(c, h)));
|
|
639
|
+
}
|
|
640
|
+
this.data = store.state.data;
|
|
641
|
+
this.rebuilt = true;
|
|
642
|
+
} else if (store.state.data !== this.data) {
|
|
643
|
+
this.data = store.state.data;
|
|
644
|
+
this.refreshLists();
|
|
645
|
+
}
|
|
646
|
+
this.sync();
|
|
647
|
+
}
|
|
648
|
+
/** Show filters set elsewhere (menu, API) in the boxes that are not being typed in. */
|
|
649
|
+
sync() {
|
|
650
|
+
const filters = this.grid.store.state.filters;
|
|
651
|
+
if (filters === this.synced && !this.rebuilt) return;
|
|
652
|
+
this.synced = filters;
|
|
653
|
+
this.rebuilt = false;
|
|
654
|
+
for (const r of REGIONS2) {
|
|
655
|
+
for (const box of this.inners[r].querySelectorAll("[data-col]")) {
|
|
656
|
+
const colId = box.dataset.col;
|
|
657
|
+
const model = filters[colId];
|
|
658
|
+
box.closest(".fg-filter-cell").classList.toggle("fg-filter-active", !!model);
|
|
659
|
+
if (box === box.ownerDocument.activeElement || box.getRootNode().activeElement === box) continue;
|
|
660
|
+
if (box instanceof HTMLSelectElement) {
|
|
661
|
+
const v = model?.type === "set" && model.values.length === 1 ? JSON.stringify(model.values[0]) : "";
|
|
662
|
+
if ([...box.options].some((o) => o.value === v)) box.value = v;
|
|
663
|
+
} else box.value = filterText(model);
|
|
664
|
+
box.classList.remove("fg-filter-bad");
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** Refill the set lists from the current rows, keeping each list's choice. */
|
|
669
|
+
refreshLists() {
|
|
670
|
+
for (const r of REGIONS2) {
|
|
671
|
+
for (const select of this.inners[r].querySelectorAll("select[data-col]")) {
|
|
672
|
+
const def = this.grid.columns.getNode(select.dataset.col)?.def;
|
|
673
|
+
if (!def) continue;
|
|
674
|
+
const v = select.value;
|
|
675
|
+
this.fillOptions(select, def);
|
|
676
|
+
if (v && ![...select.options].some((o) => o.value === v)) {
|
|
677
|
+
const o = el("option", "", { value: v });
|
|
678
|
+
o.textContent = String(JSON.parse(v));
|
|
679
|
+
select.append(o);
|
|
680
|
+
}
|
|
681
|
+
select.value = v;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
fillOptions(select, def) {
|
|
686
|
+
const all2 = el("option", "", { value: "" });
|
|
687
|
+
all2.textContent = this.grid.store.state.locale.selectAll;
|
|
688
|
+
const opts = this.distinct(def).map((v) => {
|
|
689
|
+
const o = el("option", "", { value: JSON.stringify(v) });
|
|
690
|
+
o.textContent = v === true ? "\u2713" : v === false ? "\u2717" : String(v);
|
|
691
|
+
return o;
|
|
692
|
+
});
|
|
693
|
+
select.replaceChildren(all2, ...opts);
|
|
694
|
+
}
|
|
695
|
+
cell(c, h) {
|
|
696
|
+
const cell = el("div", "fg-filter-cell");
|
|
697
|
+
cell.style.left = `${c.left}px`;
|
|
698
|
+
cell.style.width = `${c.width}px`;
|
|
699
|
+
cell.style.height = `${h}px`;
|
|
700
|
+
const type = isSystemCol(c.id) ? null : filterTypeOf(c.def);
|
|
701
|
+
if (!type) return cell;
|
|
702
|
+
const loc = this.grid.store.state.locale;
|
|
703
|
+
const label = `${loc.filter}: ${c.def.header ?? c.def.field ?? c.id}`;
|
|
704
|
+
if (type === "set") {
|
|
705
|
+
const select = el("select", "fg-filter-input", { "aria-label": label });
|
|
706
|
+
select.dataset.col = c.id;
|
|
707
|
+
this.fillOptions(select, c.def);
|
|
708
|
+
select.addEventListener("change", () => {
|
|
709
|
+
this.grid.setFilter(c.id, select.value ? { type: "set", values: [JSON.parse(select.value)] } : null);
|
|
710
|
+
});
|
|
711
|
+
cell.append(select);
|
|
712
|
+
return cell;
|
|
713
|
+
}
|
|
714
|
+
const input = el("input", "fg-filter-input", { type: "text", "aria-label": label, spellcheck: "false", autocomplete: "off" });
|
|
715
|
+
input.dataset.col = c.id;
|
|
716
|
+
if (type === "number") input.placeholder = ">0, 10~20";
|
|
717
|
+
if (type === "date") input.placeholder = ">2026-01-01";
|
|
718
|
+
const apply = () => {
|
|
719
|
+
const model = parseFilterText(type, input.value);
|
|
720
|
+
input.classList.toggle("fg-filter-bad", model === void 0);
|
|
721
|
+
if (model === void 0) return;
|
|
722
|
+
this.grid.setFilter(c.id, model);
|
|
723
|
+
};
|
|
724
|
+
input.addEventListener("input", () => {
|
|
725
|
+
clearTimeout(this.timers.get(c.id));
|
|
726
|
+
this.timers.set(c.id, window.setTimeout(apply, 200));
|
|
727
|
+
});
|
|
728
|
+
input.addEventListener("keydown", (e) => {
|
|
729
|
+
e.stopPropagation();
|
|
730
|
+
if (e.key === "Enter") {
|
|
731
|
+
clearTimeout(this.timers.get(c.id));
|
|
732
|
+
apply();
|
|
733
|
+
} else if (e.key === "Escape") {
|
|
734
|
+
input.value = "";
|
|
735
|
+
clearTimeout(this.timers.get(c.id));
|
|
736
|
+
apply();
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
cell.append(input);
|
|
740
|
+
return cell;
|
|
741
|
+
}
|
|
742
|
+
/** Values for a set box: distinct, sorted, at most 200. */
|
|
743
|
+
distinct(def) {
|
|
744
|
+
const seen2 = /* @__PURE__ */ new Map();
|
|
745
|
+
for (const row of this.grid.store.state.data) {
|
|
746
|
+
const v = getCellValue(def, row);
|
|
747
|
+
if (v === null || v === void 0 || v === "") continue;
|
|
748
|
+
const k = JSON.stringify(v);
|
|
749
|
+
if (!seen2.has(k)) seen2.set(k, v);
|
|
750
|
+
if (seen2.size >= 200) break;
|
|
751
|
+
}
|
|
752
|
+
return [...seen2.values()].sort((a, b) => String(a).localeCompare(String(b)));
|
|
753
|
+
}
|
|
754
|
+
setTransform(x) {
|
|
755
|
+
this.inners.center.style.transform = `translateX(${-x}px)`;
|
|
756
|
+
}
|
|
757
|
+
destroy() {
|
|
758
|
+
for (const t of this.timers.values()) clearTimeout(t);
|
|
759
|
+
for (const r of REGIONS2) this.inners[r].remove();
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
// src/rows/filters.ts
|
|
764
|
+
function isBlank(v) {
|
|
765
|
+
return v == null || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
|
|
766
|
+
}
|
|
767
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
768
|
+
function toDateKey2(v) {
|
|
769
|
+
if (isBlank(v)) return null;
|
|
770
|
+
if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
|
|
771
|
+
const d = v instanceof Date ? v : new Date(v);
|
|
772
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
773
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
774
|
+
}
|
|
775
|
+
function setKey(v) {
|
|
776
|
+
if (isBlank(v)) return "";
|
|
777
|
+
if (v instanceof Date) return toDateKey2(v) ?? "";
|
|
778
|
+
return String(v);
|
|
779
|
+
}
|
|
780
|
+
var all = () => true;
|
|
781
|
+
function buildPredicate(model) {
|
|
782
|
+
switch (model.type) {
|
|
783
|
+
case "combined": {
|
|
784
|
+
const parts = (Array.isArray(model.conditions) ? model.conditions : []).map(buildPredicate);
|
|
785
|
+
if (!parts.length) return all;
|
|
786
|
+
return model.operator === "or" ? (v) => parts.some((p) => p(v)) : (v) => parts.every((p) => p(v));
|
|
787
|
+
}
|
|
788
|
+
case "text": {
|
|
789
|
+
const q = (model.value ?? "").toLowerCase();
|
|
790
|
+
const s = (v) => isBlank(v) ? "" : String(v).toLowerCase();
|
|
791
|
+
switch (model.op) {
|
|
792
|
+
case "contains":
|
|
793
|
+
return (v) => s(v).includes(q);
|
|
794
|
+
case "equals":
|
|
795
|
+
return (v) => s(v) === q;
|
|
796
|
+
case "notEquals":
|
|
797
|
+
return (v) => s(v) !== q;
|
|
798
|
+
case "startsWith":
|
|
799
|
+
return (v) => s(v).startsWith(q);
|
|
800
|
+
case "endsWith":
|
|
801
|
+
return (v) => s(v).endsWith(q);
|
|
802
|
+
case "blank":
|
|
803
|
+
return isBlank;
|
|
804
|
+
case "notBlank":
|
|
805
|
+
return (v) => !isBlank(v);
|
|
806
|
+
}
|
|
807
|
+
return all;
|
|
808
|
+
}
|
|
809
|
+
case "number": {
|
|
810
|
+
const n = (v) => isBlank(v) ? NaN : Number(v);
|
|
811
|
+
if (model.op === "blank") return (v) => Number.isNaN(n(v));
|
|
812
|
+
if (model.op === "notBlank") return (v) => !Number.isNaN(n(v));
|
|
813
|
+
const a = model.value;
|
|
814
|
+
if (a == null || Number.isNaN(a)) return all;
|
|
815
|
+
switch (model.op) {
|
|
816
|
+
case "eq":
|
|
817
|
+
return (v) => n(v) === a;
|
|
818
|
+
case "ne":
|
|
819
|
+
return (v) => n(v) !== a;
|
|
820
|
+
case "lt":
|
|
821
|
+
return (v) => n(v) < a;
|
|
822
|
+
case "lte":
|
|
823
|
+
return (v) => n(v) <= a;
|
|
824
|
+
case "gt":
|
|
825
|
+
return (v) => n(v) > a;
|
|
826
|
+
case "gte":
|
|
827
|
+
return (v) => n(v) >= a;
|
|
828
|
+
case "between": {
|
|
829
|
+
const b = model.valueTo;
|
|
830
|
+
if (b == null || Number.isNaN(b)) return all;
|
|
831
|
+
const lo = Math.min(a, b), hi = Math.max(a, b);
|
|
832
|
+
return (v) => {
|
|
833
|
+
const x = n(v);
|
|
834
|
+
return x >= lo && x <= hi;
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return all;
|
|
839
|
+
}
|
|
840
|
+
case "date": {
|
|
841
|
+
if (model.op === "blank") return (v) => toDateKey2(v) === null;
|
|
842
|
+
if (model.op === "notBlank") return (v) => toDateKey2(v) !== null;
|
|
843
|
+
const a = model.value;
|
|
844
|
+
if (!a) return all;
|
|
845
|
+
const k = (v) => toDateKey2(v);
|
|
846
|
+
switch (model.op) {
|
|
847
|
+
case "equals":
|
|
848
|
+
return (v) => k(v) === a;
|
|
849
|
+
case "before":
|
|
850
|
+
return (v) => {
|
|
851
|
+
const x = k(v);
|
|
852
|
+
return x !== null && x < a;
|
|
853
|
+
};
|
|
854
|
+
case "after":
|
|
855
|
+
return (v) => {
|
|
856
|
+
const x = k(v);
|
|
857
|
+
return x !== null && x > a;
|
|
858
|
+
};
|
|
859
|
+
case "between": {
|
|
860
|
+
const b = model.valueTo;
|
|
861
|
+
if (!b) return all;
|
|
862
|
+
const lo = a < b ? a : b, hi = a < b ? b : a;
|
|
863
|
+
return (v) => {
|
|
864
|
+
const x = k(v);
|
|
865
|
+
return x !== null && x >= lo && x <= hi;
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return all;
|
|
870
|
+
}
|
|
871
|
+
case "set": {
|
|
872
|
+
const allowed = new Set(model.values.map(setKey));
|
|
873
|
+
return (v) => allowed.has(setKey(v));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return all;
|
|
877
|
+
}
|
|
878
|
+
function quickTokens(q) {
|
|
879
|
+
const t = q.trim().toLowerCase();
|
|
880
|
+
return t ? t.split(/\s+/) : [];
|
|
881
|
+
}
|
|
882
|
+
function matchesQuick(tokens, text2) {
|
|
883
|
+
return tokens.every((t) => text2.includes(t));
|
|
884
|
+
}
|
|
885
|
+
function buildAdvanced(f, columnDef, valueOf) {
|
|
886
|
+
const parts = [];
|
|
887
|
+
for (const c of Array.isArray(f?.conditions) ? f.conditions : []) {
|
|
888
|
+
if (!c || typeof c !== "object") continue;
|
|
889
|
+
if ("conditions" in c) {
|
|
890
|
+
const sub = buildAdvanced(c, columnDef, valueOf);
|
|
891
|
+
if (sub) parts.push(sub);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const def = columnDef(c.colId);
|
|
895
|
+
if (!def || !c.model) continue;
|
|
896
|
+
const pred = buildPredicate(c.model);
|
|
897
|
+
parts.push((row) => pred(valueOf(def, row)));
|
|
898
|
+
}
|
|
899
|
+
if (!parts.length) return null;
|
|
900
|
+
return f.operator === "or" ? (row) => parts.some((p) => p(row)) : (row) => parts.every((p) => p(row));
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/i18n/format.ts
|
|
904
|
+
var cache = /* @__PURE__ */ new Map();
|
|
905
|
+
function parseNumberPattern(p) {
|
|
906
|
+
if (p === "integer") return { maximumFractionDigits: 0 };
|
|
907
|
+
if (p === "percent") return { style: "percent" };
|
|
908
|
+
if (p.startsWith("currency:")) return { style: "currency", currency: p.slice(9) };
|
|
909
|
+
if (!/^[#0,]*(\.[#0]+)?%?$/.test(p) || !/[#0]/.test(p)) return null;
|
|
910
|
+
const pct = p.endsWith("%");
|
|
911
|
+
const body = pct ? p.slice(0, -1) : p;
|
|
912
|
+
const frac = body.includes(".") ? body.split(".")[1] : "";
|
|
913
|
+
return {
|
|
914
|
+
useGrouping: body.includes(","),
|
|
915
|
+
minimumFractionDigits: (frac.match(/0/g) ?? []).length,
|
|
916
|
+
maximumFractionDigits: frac.length,
|
|
917
|
+
...pct ? { style: "percent" } : {}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
var DATE_PRESETS = {
|
|
921
|
+
date: { year: "numeric", month: "2-digit", day: "2-digit" },
|
|
922
|
+
datetime: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" },
|
|
923
|
+
time: { hour: "2-digit", minute: "2-digit" },
|
|
924
|
+
short: { dateStyle: "short" },
|
|
925
|
+
medium: { dateStyle: "medium" },
|
|
926
|
+
long: { dateStyle: "long" }
|
|
927
|
+
};
|
|
928
|
+
function toDate(v) {
|
|
929
|
+
if (isBlank(v)) return null;
|
|
930
|
+
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
|
|
931
|
+
if (typeof v === "string") {
|
|
932
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
|
|
933
|
+
if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
934
|
+
}
|
|
935
|
+
if (typeof v !== "string" && typeof v !== "number") return null;
|
|
936
|
+
const d = new Date(v);
|
|
937
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
938
|
+
}
|
|
939
|
+
function create(type, format, locale) {
|
|
940
|
+
switch (type) {
|
|
941
|
+
case "number": {
|
|
942
|
+
let opts = {};
|
|
943
|
+
if (typeof format === "string") {
|
|
944
|
+
const parsed = parseNumberPattern(format);
|
|
945
|
+
if (!parsed) warn(`unknown number format "${format}"`);
|
|
946
|
+
opts = parsed ?? {};
|
|
947
|
+
} else if (format) opts = format;
|
|
948
|
+
const nf = new Intl.NumberFormat(locale, opts);
|
|
949
|
+
return (v) => {
|
|
950
|
+
if (isBlank(v)) return "";
|
|
951
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
952
|
+
return Number.isNaN(n) ? String(v) : nf.format(n);
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
case "date": {
|
|
956
|
+
if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
|
|
957
|
+
const pad3 = (n) => String(n).padStart(2, "0");
|
|
958
|
+
const withTime = format === "datetime";
|
|
959
|
+
return (v) => {
|
|
960
|
+
const d = toDate(v);
|
|
961
|
+
if (!d) return isBlank(v) ? "" : String(v);
|
|
962
|
+
const day = `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}`;
|
|
963
|
+
return withTime ? `${day} ${pad3(d.getHours())}:${pad3(d.getMinutes())}` : day;
|
|
964
|
+
};
|
|
672
965
|
}
|
|
673
|
-
|
|
674
|
-
if (
|
|
675
|
-
|
|
966
|
+
let opts = DATE_PRESETS.date;
|
|
967
|
+
if (typeof format === "string") {
|
|
968
|
+
if (!DATE_PRESETS[format]) warn(`unknown date format "${format}"`);
|
|
969
|
+
opts = DATE_PRESETS[format] ?? DATE_PRESETS.date;
|
|
970
|
+
} else if (format) opts = format;
|
|
971
|
+
const df = new Intl.DateTimeFormat(locale, opts);
|
|
972
|
+
return (v) => {
|
|
973
|
+
const d = toDate(v);
|
|
974
|
+
return d ? df.format(d) : isBlank(v) ? "" : String(v);
|
|
975
|
+
};
|
|
676
976
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
var MAX_DEPTH = 32;
|
|
682
|
-
function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0) {
|
|
683
|
-
try {
|
|
684
|
-
const resolve = (name) => {
|
|
685
|
-
if (!row || !Object.hasOwn(row, name)) throw new FormulaError("#REF!");
|
|
686
|
-
const v2 = row[name];
|
|
687
|
-
if (typeof v2 === "string" && v2.startsWith("=")) {
|
|
688
|
-
if (seen2.has(name) || depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
|
|
689
|
-
const r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1);
|
|
690
|
-
if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
|
|
691
|
-
return r;
|
|
692
|
-
}
|
|
693
|
-
return v2;
|
|
694
|
-
};
|
|
695
|
-
const v = new Parser(tokenize(formula.slice(1)), resolve).parse();
|
|
696
|
-
if (typeof v === "number" && !Number.isFinite(v)) return "#NUM!";
|
|
697
|
-
return v;
|
|
698
|
-
} catch (e) {
|
|
699
|
-
return e instanceof FormulaError ? e.code : "#ERROR!";
|
|
977
|
+
case "boolean":
|
|
978
|
+
return (v) => v === true ? "\u2713" : v === false ? "\u2717" : "";
|
|
979
|
+
default:
|
|
980
|
+
return (v) => isBlank(v) ? "" : v instanceof Date ? v.toISOString() : String(v);
|
|
700
981
|
}
|
|
701
982
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
warn(`valueGetter of column "${def.id ?? def.field}" threw; rendering an empty cell`);
|
|
711
|
-
return void 0;
|
|
712
|
-
}
|
|
983
|
+
function getFormatter(def, locale) {
|
|
984
|
+
if (def.sparkline) return (v) => Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v);
|
|
985
|
+
const f = def.format;
|
|
986
|
+
const key = `${locale}|${def.type ?? "text"}|${typeof f === "string" ? f : JSON.stringify(f ?? null)}`;
|
|
987
|
+
let fmt = cache.get(key);
|
|
988
|
+
if (!fmt) {
|
|
989
|
+
fmt = create(def.type, f, locale);
|
|
990
|
+
cache.set(key, fmt);
|
|
713
991
|
}
|
|
714
|
-
|
|
715
|
-
const raw = row[def.field];
|
|
716
|
-
return def.formulas && isFormula(raw) ? evaluateFormula(raw, row, /* @__PURE__ */ new Set([def.field])) : raw;
|
|
992
|
+
return fmt;
|
|
717
993
|
}
|
|
718
994
|
|
|
719
995
|
// src/summary.ts
|
|
@@ -871,7 +1147,7 @@ function rowRangeByOffsets(offsets, scrollTop, height, count, overscan = 4) {
|
|
|
871
1147
|
}
|
|
872
1148
|
|
|
873
1149
|
// src/render/renderer.ts
|
|
874
|
-
var
|
|
1150
|
+
var REGIONS3 = ["left", "center", "right"];
|
|
875
1151
|
var ROW_OVERSCAN = 6;
|
|
876
1152
|
var MAX_COLSPAN_REACH = 8;
|
|
877
1153
|
var COL_OVERSCAN = 2;
|
|
@@ -888,6 +1164,8 @@ var Renderer = class {
|
|
|
888
1164
|
constructor(host, grid, opts) {
|
|
889
1165
|
this.grid = grid;
|
|
890
1166
|
this.opts = opts;
|
|
1167
|
+
/** Boxes under the header (filterRow option). */
|
|
1168
|
+
this.filterRow = null;
|
|
891
1169
|
this.activeCellId = "";
|
|
892
1170
|
this.idBase = `fg${Math.random().toString(36).slice(2, 8)}`;
|
|
893
1171
|
this.painted = { left: /* @__PURE__ */ new Map(), center: /* @__PURE__ */ new Map(), right: /* @__PURE__ */ new Map() };
|
|
@@ -935,6 +1213,8 @@ var Renderer = class {
|
|
|
935
1213
|
e.preventDefault();
|
|
936
1214
|
this.scroller.scrollTop += e.deltaY;
|
|
937
1215
|
};
|
|
1216
|
+
this.overlayKey = "";
|
|
1217
|
+
this.heightFn = null;
|
|
938
1218
|
this.onCardClick = (e) => {
|
|
939
1219
|
const target = e.target;
|
|
940
1220
|
const card = target.closest(".fg-card");
|
|
@@ -964,10 +1244,10 @@ var Renderer = class {
|
|
|
964
1244
|
});
|
|
965
1245
|
this.headerPanes = panes("fg-header-pane");
|
|
966
1246
|
this.headerInners = panes("fg-header-inner");
|
|
967
|
-
for (const r of
|
|
1247
|
+
for (const r of REGIONS3) this.headerInners[r].setAttribute("role", "row");
|
|
968
1248
|
this.bodyPanes = panes("fg-body-pane");
|
|
969
1249
|
this.canvases = panes("fg-canvas");
|
|
970
|
-
for (const r of
|
|
1250
|
+
for (const r of REGIONS3) {
|
|
971
1251
|
this.headerPanes[r].dataset.region = r;
|
|
972
1252
|
this.bodyPanes[r].dataset.region = r;
|
|
973
1253
|
this.headerPanes[r].append(this.headerInners[r]);
|
|
@@ -976,6 +1256,7 @@ var Renderer = class {
|
|
|
976
1256
|
this.bodyEl.append(this.bodyPanes[r]);
|
|
977
1257
|
}
|
|
978
1258
|
this.scroller = this.bodyPanes.center;
|
|
1259
|
+
if (grid.options.filterRow) this.filterRow = new FilterRow(grid, this.headerPanes);
|
|
979
1260
|
this.overlay = el("div", "fg-overlay", { "aria-live": "polite" });
|
|
980
1261
|
this.main = el("div", "fg-main");
|
|
981
1262
|
this.gridEl.append(this.headerEl, this.bodyEl);
|
|
@@ -1008,7 +1289,7 @@ var Renderer = class {
|
|
|
1008
1289
|
}
|
|
1009
1290
|
applyTheme() {
|
|
1010
1291
|
const rtl = this.grid.options.direction === "rtl";
|
|
1011
|
-
this.root.className = `fg-root fg-theme-${this.grid.store.state.theme}${rtl ? " fg-rtl" : ""}${this.cardMode ? " fg-card-mode" : ""}`;
|
|
1292
|
+
this.root.className = `fg-root fg-theme-${this.grid.store.state.theme}${rtl ? " fg-rtl" : ""}${this.cardMode ? " fg-card-mode" : ""}${this.grid.density !== "normal" ? ` fg-density-${this.grid.density}` : ""}`;
|
|
1012
1293
|
this.root.setAttribute("dir", rtl ? "rtl" : "ltr");
|
|
1013
1294
|
}
|
|
1014
1295
|
/** Right-to-left layout: the main area is mirrored, so screen deltas along x flip sign. */
|
|
@@ -1037,7 +1318,7 @@ var Renderer = class {
|
|
|
1037
1318
|
}
|
|
1038
1319
|
const count = rowModel.getRowCount();
|
|
1039
1320
|
const auto = !!this.grid.options.autoRowHeight && !rowModel.server;
|
|
1040
|
-
const lay = rowModel.layout(rh, this.detailHeight(), auto ? this.rowHeights : void 0, this.heightsVersion);
|
|
1321
|
+
const lay = rowModel.layout(rh, this.detailHeight(), auto ? this.rowHeights : void 0, this.heightsVersion, rowModel.server ? void 0 : this.heightOf);
|
|
1041
1322
|
this.offsets = lay.uniform ? null : lay.offsets;
|
|
1042
1323
|
this.firstDataId = columns.displayed().find((c) => !isSystemCol(c.id))?.id ?? "";
|
|
1043
1324
|
this.groupLabelId = columns.region("center").find((c) => !isSystemCol(c.id))?.id ?? this.firstDataId;
|
|
@@ -1046,8 +1327,7 @@ var Renderer = class {
|
|
|
1046
1327
|
this.renderStatusBar(rowModel.dataCount());
|
|
1047
1328
|
this.renderPager();
|
|
1048
1329
|
this.orderSections(null);
|
|
1049
|
-
this.
|
|
1050
|
-
this.overlay.textContent = count === 0 && rowModel.loading() ? store.state.locale.loading : store.state.locale.noRows;
|
|
1330
|
+
this.paintOverlay(count);
|
|
1051
1331
|
return;
|
|
1052
1332
|
}
|
|
1053
1333
|
const summary = this.grid.summary();
|
|
@@ -1057,9 +1337,9 @@ var Renderer = class {
|
|
|
1057
1337
|
this.topOffset = summary && !sumPinned && sumPos === "top" ? sumRows * rh : 0;
|
|
1058
1338
|
const bottomExtra = summary && !sumPinned && sumPos === "bottom" ? sumRows * rh : 0;
|
|
1059
1339
|
this.syncSummaryPane(summary && sumPinned ? sumPos : null, sumRows * rh);
|
|
1060
|
-
const headerHeight = columns.depth * hh;
|
|
1340
|
+
const headerHeight = columns.depth * hh + (this.filterRow?.height ?? 0);
|
|
1061
1341
|
this.headerEl.style.height = `${headerHeight}px`;
|
|
1062
|
-
for (const r of
|
|
1342
|
+
for (const r of REGIONS3) {
|
|
1063
1343
|
const w = columns.regionWidth(r);
|
|
1064
1344
|
if (r !== "center") {
|
|
1065
1345
|
this.headerPanes[r].style.width = this.bodyPanes[r].style.width = `${w}px`;
|
|
@@ -1068,17 +1348,22 @@ var Renderer = class {
|
|
|
1068
1348
|
this.canvases[r].style.width = `${w}px`;
|
|
1069
1349
|
this.canvases[r].style.height = `${this.rowsHeight(count) + this.topOffset + bottomExtra}px`;
|
|
1070
1350
|
}
|
|
1351
|
+
if (options.autoHeight && !this.useCards()) {
|
|
1352
|
+
this.root.classList.add("fg-auto-height");
|
|
1353
|
+
this.bodyEl.style.height = `${this.rowsHeight(count) + this.topOffset + bottomExtra + Math.max(0, this.scroller.offsetHeight - this.scroller.clientHeight)}px`;
|
|
1354
|
+
}
|
|
1071
1355
|
const hScrollbar = this.scroller.offsetHeight - this.scroller.clientHeight;
|
|
1072
1356
|
this.bodyPanes.left.style.marginBottom = this.bodyPanes.right.style.marginBottom = `${hScrollbar}px`;
|
|
1073
1357
|
const g = this.gridEl;
|
|
1074
1358
|
g.setAttribute("role", options.treeData || this.grid.getGroupBy().length ? "treegrid" : "grid");
|
|
1075
|
-
g.setAttribute("aria-rowcount", String(count + columns.depth));
|
|
1359
|
+
g.setAttribute("aria-rowcount", String(count + columns.depth + (this.filterRow ? 1 : 0)));
|
|
1076
1360
|
g.setAttribute("aria-colcount", String(columns.displayed().length));
|
|
1077
1361
|
if (options.rowSelection === "multiple") g.setAttribute("aria-multiselectable", "true");
|
|
1078
1362
|
if (this.headerDirty) {
|
|
1079
1363
|
this.headerDirty = false;
|
|
1080
1364
|
this.renderHeader();
|
|
1081
1365
|
}
|
|
1366
|
+
this.filterRow?.render();
|
|
1082
1367
|
this.renderRows(count);
|
|
1083
1368
|
this.updateActiveCell();
|
|
1084
1369
|
if (auto) this.measureRows();
|
|
@@ -1088,12 +1373,12 @@ var Renderer = class {
|
|
|
1088
1373
|
this.renderStatusBar(rowModel.dataCount());
|
|
1089
1374
|
this.renderPager();
|
|
1090
1375
|
this.orderSections(summary && sumPinned ? sumPos : null);
|
|
1091
|
-
this.
|
|
1092
|
-
this.overlay.textContent = count === 0 && rowModel.loading() ? store.state.locale.loading : store.state.locale.noRows;
|
|
1376
|
+
this.paintOverlay(count);
|
|
1093
1377
|
this.overlay.style.top = `${headerHeight}px`;
|
|
1094
1378
|
const { scrollTop, scrollLeft } = this.scroller;
|
|
1095
1379
|
this.bodyPanes.left.scrollTop = this.bodyPanes.right.scrollTop = scrollTop;
|
|
1096
1380
|
this.headerInners.center.style.transform = `translateX(${-scrollLeft}px)`;
|
|
1381
|
+
this.filterRow?.setTransform(scrollLeft);
|
|
1097
1382
|
if (this.summaryInners) this.summaryInners.center.style.transform = `translateX(${-scrollLeft}px)`;
|
|
1098
1383
|
for (const s of [this.pinnedTop, this.pinnedBottom]) if (s) s.inners.center.style.transform = `translateX(${-scrollLeft}px)`;
|
|
1099
1384
|
this.grid.fillHandle.update();
|
|
@@ -1120,6 +1405,37 @@ var Renderer = class {
|
|
|
1120
1405
|
ref = b;
|
|
1121
1406
|
}
|
|
1122
1407
|
}
|
|
1408
|
+
/** "No rows" / "Loading" over the body, or the noRowsOverlay / loadingOverlay given in the options. */
|
|
1409
|
+
paintOverlay(count) {
|
|
1410
|
+
const { grid } = this;
|
|
1411
|
+
const { store, rowModel, options } = grid;
|
|
1412
|
+
const kind = grid.forcedOverlay ?? (count > 0 ? null : rowModel.loading() ? "loading" : "noRows");
|
|
1413
|
+
this.overlay.hidden = !kind;
|
|
1414
|
+
if (!kind) {
|
|
1415
|
+
this.overlayKey = "";
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const filtered = !rowModel.server && store.state.data.length > 0;
|
|
1419
|
+
const custom = kind === "loading" ? options.loadingOverlay : options.noRowsOverlay;
|
|
1420
|
+
const key = `${kind}|${filtered}|${store.state.locale.code}`;
|
|
1421
|
+
if (key === this.overlayKey) return;
|
|
1422
|
+
this.overlayKey = key;
|
|
1423
|
+
this.overlay.classList.toggle("fg-overlay-custom", typeof custom === "function");
|
|
1424
|
+
if (custom === void 0 || custom === null) {
|
|
1425
|
+
this.overlay.textContent = kind === "loading" ? store.state.locale.loading : store.state.locale.noRows;
|
|
1426
|
+
} else if (typeof custom === "string") {
|
|
1427
|
+
this.overlay.textContent = custom;
|
|
1428
|
+
} else {
|
|
1429
|
+
try {
|
|
1430
|
+
const out = custom({ filtered });
|
|
1431
|
+
if (typeof out === "string") this.overlay.textContent = out;
|
|
1432
|
+
else this.overlay.replaceChildren(out);
|
|
1433
|
+
} catch {
|
|
1434
|
+
warn(`${kind === "loading" ? "loadingOverlay" : "noRowsOverlay"} threw`);
|
|
1435
|
+
this.overlay.textContent = kind === "loading" ? store.state.locale.loading : store.state.locale.noRows;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1123
1439
|
/** Point aria-activedescendant at the focused cell so screen readers announce it. */
|
|
1124
1440
|
updateActiveCell() {
|
|
1125
1441
|
const prev = this.activeCellId ? this.root.getRootNode().getElementById?.(this.activeCellId) ?? null : null;
|
|
@@ -1145,7 +1461,7 @@ var Renderer = class {
|
|
|
1145
1461
|
s.size(columns, rows.length * rh, this.scroller.offsetWidth - this.scroller.clientWidth);
|
|
1146
1462
|
const center = columns.region("center");
|
|
1147
1463
|
const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
|
|
1148
|
-
for (const r of
|
|
1464
|
+
for (const r of REGIONS3) {
|
|
1149
1465
|
const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
|
|
1150
1466
|
s.inners[r].replaceChildren(...rows.map((data, k) => {
|
|
1151
1467
|
const rowEl = el("div", "fg-row fg-row-pinned", { role: "row" });
|
|
@@ -1195,7 +1511,7 @@ var Renderer = class {
|
|
|
1195
1511
|
renderHeader() {
|
|
1196
1512
|
const { columns, store } = this.grid;
|
|
1197
1513
|
const { sort, filters, locale } = store.state;
|
|
1198
|
-
for (const r of
|
|
1514
|
+
for (const r of REGIONS3) {
|
|
1199
1515
|
const inner = this.headerInners[r];
|
|
1200
1516
|
inner.style.width = `${columns.regionWidth(r)}px`;
|
|
1201
1517
|
inner.style.height = `${columns.depth * this.opts.headerHeight}px`;
|
|
@@ -1211,6 +1527,10 @@ var Renderer = class {
|
|
|
1211
1527
|
cell.style.width = `${c.width}px`;
|
|
1212
1528
|
cell.style.height = `${c.rowSpan * hh}px`;
|
|
1213
1529
|
if (isSystemCol(c.id)) {
|
|
1530
|
+
if (c.id === ROWNUM_COL_ID) {
|
|
1531
|
+
cell.classList.add("fg-header-rownum");
|
|
1532
|
+
cell.textContent = c.label;
|
|
1533
|
+
}
|
|
1214
1534
|
if (c.id === SELECT_COL_ID) {
|
|
1215
1535
|
cell.classList.add("fg-header-select");
|
|
1216
1536
|
cell.append(this.selectAllBox(locale));
|
|
@@ -1230,6 +1550,23 @@ var Renderer = class {
|
|
|
1230
1550
|
label.textContent = c.label;
|
|
1231
1551
|
}
|
|
1232
1552
|
}
|
|
1553
|
+
let toggleEl = null;
|
|
1554
|
+
if (c.isGroup && this.grid.columns.isCollapsible(c.node)) {
|
|
1555
|
+
const open = c.node.open;
|
|
1556
|
+
const t = el("button", open ? "fg-colgroup-toggle fg-expanded" : "fg-colgroup-toggle", {
|
|
1557
|
+
type: "button",
|
|
1558
|
+
tabindex: "-1",
|
|
1559
|
+
"aria-expanded": String(open),
|
|
1560
|
+
"aria-label": open ? locale.collapseAll : locale.expandAll
|
|
1561
|
+
});
|
|
1562
|
+
t.textContent = open ? "\u2212" : "+";
|
|
1563
|
+
t.addEventListener("click", (e) => {
|
|
1564
|
+
e.stopPropagation();
|
|
1565
|
+
this.grid.setColumnGroupOpened(c.id, !open);
|
|
1566
|
+
});
|
|
1567
|
+
cell.classList.add("fg-header-collapsible");
|
|
1568
|
+
toggleEl = t;
|
|
1569
|
+
}
|
|
1233
1570
|
if (hdef.headerClass) {
|
|
1234
1571
|
try {
|
|
1235
1572
|
const extra = typeof hdef.headerClass === "function" ? hdef.headerClass(hdef) : hdef.headerClass;
|
|
@@ -1239,6 +1576,7 @@ var Renderer = class {
|
|
|
1239
1576
|
}
|
|
1240
1577
|
}
|
|
1241
1578
|
cell.append(label);
|
|
1579
|
+
if (toggleEl) cell.append(toggleEl);
|
|
1242
1580
|
if (c.isGroup || !c.column) return cell;
|
|
1243
1581
|
const def = c.node.def;
|
|
1244
1582
|
if (def.wrapHeaderText ?? this.grid.options.wrapHeaderText) cell.classList.add("fg-header-wrap");
|
|
@@ -1283,12 +1621,12 @@ var Renderer = class {
|
|
|
1283
1621
|
const allBounds = this.grid.allRangeBounds();
|
|
1284
1622
|
const spans = this.grid.mergeSpans();
|
|
1285
1623
|
const top = Math.max(0, this.scroller.scrollTop - this.topOffset);
|
|
1286
|
-
const range = this.offsets ? rowRangeByOffsets(this.offsets, top, this.scroller.clientHeight, count, ROW_OVERSCAN) : rowRange(top, this.scroller.clientHeight, this.opts.rowHeight, count, ROW_OVERSCAN);
|
|
1624
|
+
const range = this.grid.options.autoHeight && !rowModel.server ? { first: 0, last: count - 1 } : this.offsets ? rowRangeByOffsets(this.offsets, top, this.scroller.clientHeight, count, ROW_OVERSCAN) : rowRange(top, this.scroller.clientHeight, this.opts.rowHeight, count, ROW_OVERSCAN);
|
|
1287
1625
|
this.lastRange = range;
|
|
1288
1626
|
const center = columns.region("center");
|
|
1289
1627
|
const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
|
|
1290
1628
|
if (center.some((c) => c.def.colSpan)) cr.first = Math.max(0, cr.first - MAX_COLSPAN_REACH);
|
|
1291
|
-
for (const r of
|
|
1629
|
+
for (const r of REGIONS3) {
|
|
1292
1630
|
const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
|
|
1293
1631
|
const map = this.painted[r];
|
|
1294
1632
|
for (const [i, row] of map) {
|
|
@@ -1307,7 +1645,7 @@ var Renderer = class {
|
|
|
1307
1645
|
const inRange = hits.map((b) => `${b.c1}-${b.c2}`).join(",");
|
|
1308
1646
|
const merge2 = this.mergeState(spans, cols, i, range.first);
|
|
1309
1647
|
const pos = node.kind === "detail" ? `${this.scroller.scrollLeft}:${this.scroller.clientWidth}` : "";
|
|
1310
|
-
const key = `${node.id}|${this.version}|${colKey}|${isSel ? 1 : 0}|${focusCol}|${inRange}|${merge2}|${pos}`;
|
|
1648
|
+
const key = `${node.id}|${this.version}|${colKey}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
|
|
1311
1649
|
let row = map.get(i);
|
|
1312
1650
|
if (!row) {
|
|
1313
1651
|
const rowEl = this.pool[r].pop() ?? el("div", "fg-row", { role: "row" });
|
|
@@ -1342,7 +1680,7 @@ var Renderer = class {
|
|
|
1342
1680
|
}
|
|
1343
1681
|
paintRow(rowEl, node, i, cols, selected, focusCol, bounds, spans = /* @__PURE__ */ new Map(), first = 0) {
|
|
1344
1682
|
const rh = this.opts.rowHeight;
|
|
1345
|
-
rowEl.className = `fg-row${i % 2 ? " fg-row-odd" : ""}${selected ? " fg-row-selected" : ""}${this.grid.tracker.created.has(node.data) ? " fg-row-created" : ""}`;
|
|
1683
|
+
rowEl.className = `fg-row${i % 2 ? " fg-row-odd" : ""}${selected ? " fg-row-selected" : ""}${this.grid.tracker.created.has(node.data) ? " fg-row-created" : ""}${this.grid.editor.editingRow(node.id) ? " fg-row-editing" : ""}`;
|
|
1346
1684
|
this.placeRow(rowEl, i);
|
|
1347
1685
|
if (this.grid.options.rowSelection) rowEl.setAttribute("aria-selected", String(selected));
|
|
1348
1686
|
if (node.level !== void 0) rowEl.setAttribute("aria-level", String(node.level + 1));
|
|
@@ -1396,6 +1734,12 @@ var Renderer = class {
|
|
|
1396
1734
|
cell.dataset.col = col.id;
|
|
1397
1735
|
cell.setAttribute("aria-colindex", String(col.index + 1));
|
|
1398
1736
|
let cls = focused ? "fg-cell fg-cell-focused" : "fg-cell";
|
|
1737
|
+
if (col.id === ROWNUM_COL_ID) {
|
|
1738
|
+
cell.className = `${cls} fg-cell-rownum`;
|
|
1739
|
+
const n = i >= 0 && !node.kind ? this.grid.rowNumber(i) : null;
|
|
1740
|
+
cell.textContent = n === null ? "" : String(n);
|
|
1741
|
+
return;
|
|
1742
|
+
}
|
|
1399
1743
|
if (col.id === DRAG_COL_ID) {
|
|
1400
1744
|
cell.className = `${cls} fg-cell-drag`;
|
|
1401
1745
|
const handle = el("span", this.grid.canMoveRows() ? "fg-drag-handle" : "fg-drag-handle fg-drag-disabled", { "aria-hidden": "true" });
|
|
@@ -1431,7 +1775,15 @@ var Renderer = class {
|
|
|
1431
1775
|
if (def.wrapText) cls += " fg-cell-wrap";
|
|
1432
1776
|
if (typeof def.editable === "function" && i >= 0 && !cellEditable(def, node.data, i)) cls += " fg-cell-readonly";
|
|
1433
1777
|
if (i >= 0) cls += this.grid.finder.classFor(i, col.id);
|
|
1778
|
+
if (this.grid.isFlashing(node.id, col.id)) cls += " fg-cell-flash";
|
|
1434
1779
|
cell.className = cls;
|
|
1780
|
+
if (def.sparkline && !def.renderer) {
|
|
1781
|
+
const nums = Array.isArray(value) ? value.map(Number).filter(Number.isFinite) : [];
|
|
1782
|
+
if (nums.length) cell.replaceChildren(sparkline(nums, def.sparkline === true ? {} : def.sparkline, col.width, this.opts.rowHeight));
|
|
1783
|
+
else cell.replaceChildren();
|
|
1784
|
+
cell.setAttribute("aria-label", Array.isArray(value) ? value.join(", ") : "");
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1435
1787
|
if (!def.renderer) {
|
|
1436
1788
|
cell.textContent = formatted;
|
|
1437
1789
|
return;
|
|
@@ -1479,14 +1831,14 @@ var Renderer = class {
|
|
|
1479
1831
|
this.summaryEl = el("div", "fg-summary", { role: "rowgroup" });
|
|
1480
1832
|
this.summaryPanes = mk("fg-summary-pane");
|
|
1481
1833
|
this.summaryInners = mk("fg-summary-inner");
|
|
1482
|
-
for (const r of
|
|
1834
|
+
for (const r of REGIONS3) {
|
|
1483
1835
|
this.summaryPanes[r].append(this.summaryInners[r]);
|
|
1484
1836
|
this.summaryEl.append(this.summaryPanes[r]);
|
|
1485
1837
|
}
|
|
1486
1838
|
}
|
|
1487
1839
|
this.summaryEl.className = `fg-summary fg-summary-${position}`;
|
|
1488
1840
|
this.summaryEl.style.height = `${height}px`;
|
|
1489
|
-
for (const r of
|
|
1841
|
+
for (const r of REGIONS3) {
|
|
1490
1842
|
const w = this.grid.columns.regionWidth(r);
|
|
1491
1843
|
if (r !== "center") {
|
|
1492
1844
|
this.summaryPanes[r].style.width = `${w}px`;
|
|
@@ -1499,7 +1851,7 @@ var Renderer = class {
|
|
|
1499
1851
|
this.summaryPanes.center.style.marginRight = `${vScrollbar}px`;
|
|
1500
1852
|
}
|
|
1501
1853
|
renderSummary(count, defs, values, pinned, position) {
|
|
1502
|
-
for (const r of
|
|
1854
|
+
for (const r of REGIONS3) {
|
|
1503
1855
|
for (const old of this.canvasSummary[r]) old.remove();
|
|
1504
1856
|
this.canvasSummary[r] = [];
|
|
1505
1857
|
}
|
|
@@ -1509,7 +1861,7 @@ var Renderer = class {
|
|
|
1509
1861
|
const center = columns.region("center");
|
|
1510
1862
|
const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
|
|
1511
1863
|
const dataCols = columns.displayed().filter((c) => !isSystemCol(c.id));
|
|
1512
|
-
for (const r of
|
|
1864
|
+
for (const r of REGIONS3) {
|
|
1513
1865
|
const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
|
|
1514
1866
|
const rowsEls = defs.map((def, k) => {
|
|
1515
1867
|
const labelCol = def.label != null ? dataCols.find((c) => !(c.id in (def.columns ?? {})))?.id ?? null : null;
|
|
@@ -1557,8 +1909,21 @@ var Renderer = class {
|
|
|
1557
1909
|
cell.textContent = s.prefix && text2 ? `${s.prefix} ${text2}` : text2;
|
|
1558
1910
|
return cell;
|
|
1559
1911
|
}
|
|
1912
|
+
/** getRowHeight, guarded: a throwing function means the default height. */
|
|
1913
|
+
get heightOf() {
|
|
1914
|
+
const fn = this.grid.options.getRowHeight;
|
|
1915
|
+
if (!fn) return void 0;
|
|
1916
|
+
return this.heightFn ?? (this.heightFn = (row) => {
|
|
1917
|
+
try {
|
|
1918
|
+
return fn(row) ?? void 0;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return void 0;
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1560
1924
|
/** Forget measured row heights (data, widths or fonts changed); rows are measured again on screen. */
|
|
1561
1925
|
invalidateHeights() {
|
|
1926
|
+
if (this.heightOf) this.heightsVersion++;
|
|
1562
1927
|
if (!this.rowHeights.size) return;
|
|
1563
1928
|
this.rowHeights.clear();
|
|
1564
1929
|
this.heightsVersion++;
|
|
@@ -1567,7 +1932,7 @@ var Renderer = class {
|
|
|
1567
1932
|
measureRows() {
|
|
1568
1933
|
const base = this.opts.rowHeight;
|
|
1569
1934
|
const need = /* @__PURE__ */ new Map();
|
|
1570
|
-
for (const r of
|
|
1935
|
+
for (const r of REGIONS3) {
|
|
1571
1936
|
for (const [i, row] of this.painted[r]) {
|
|
1572
1937
|
const node = this.grid.rowModel.getRow(i);
|
|
1573
1938
|
if (!node || node.kind) continue;
|
|
@@ -1613,7 +1978,7 @@ var Renderer = class {
|
|
|
1613
1978
|
rowEl.style.transform = `translateY(${this.rowTop(i)}px)`;
|
|
1614
1979
|
rowEl.style.height = `${this.rowH(i)}px`;
|
|
1615
1980
|
rowEl.dataset.row = String(i);
|
|
1616
|
-
rowEl.setAttribute("aria-rowindex", String(i + 1 + this.grid.columns.depth));
|
|
1981
|
+
rowEl.setAttribute("aria-rowindex", String(i + 1 + this.grid.columns.depth + (this.filterRow ? 1 : 0)));
|
|
1617
1982
|
}
|
|
1618
1983
|
/** Tree indentation and chevron, detail toggle, group-leaf indentation in the first data column. */
|
|
1619
1984
|
decorate(cell, node) {
|
|
@@ -1673,6 +2038,19 @@ var Renderer = class {
|
|
|
1673
2038
|
cell.dataset.col = col.id;
|
|
1674
2039
|
cell.setAttribute("aria-colindex", String(col.index + 1));
|
|
1675
2040
|
const focused = focusCol === col.id ? " fg-cell-focused" : "";
|
|
2041
|
+
if (col.id === SELECT_COL_ID) {
|
|
2042
|
+
cell.className = `fg-cell fg-cell-select${focused}`;
|
|
2043
|
+
if (grid.options.rowSelection !== "multiple") {
|
|
2044
|
+
cell.replaceChildren();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const state = grid.groupSelection(node);
|
|
2048
|
+
const box = el("input", "fg-checkbox", { type: "checkbox", tabindex: "-1", "aria-label": loc.selectAllRows });
|
|
2049
|
+
box.checked = state === "all";
|
|
2050
|
+
box.indeterminate = state === "some";
|
|
2051
|
+
cell.replaceChildren(box);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
1676
2054
|
if (col.id === this.groupLabelId) {
|
|
1677
2055
|
cell.className = `fg-cell fg-group-cell${focused}`;
|
|
1678
2056
|
cell.style.setProperty("--fg-level", String(node.level ?? 0));
|
|
@@ -1687,7 +2065,8 @@ var Renderer = class {
|
|
|
1687
2065
|
const label = el("span", "fg-group-label");
|
|
1688
2066
|
const value2 = isBlank(g.value) ? loc.blanks : def2 ? getFormatter(def2, loc.code)(g.value) : String(g.value);
|
|
1689
2067
|
const nf = new Intl.NumberFormat(loc.code);
|
|
1690
|
-
|
|
2068
|
+
const count = g.count ?? (g.aggregates ? null : g.rows.length);
|
|
2069
|
+
label.textContent = `${def2?.header ?? def2?.field ?? g.colId}: ${value2}${count == null ? "" : ` (${nf.format(count)})`}`;
|
|
1691
2070
|
cell.replaceChildren(toggle, label);
|
|
1692
2071
|
return;
|
|
1693
2072
|
}
|
|
@@ -1936,6 +2315,7 @@ var Renderer = class {
|
|
|
1936
2315
|
return Math.max(1, Math.floor(this.scroller.clientHeight / this.opts.rowHeight));
|
|
1937
2316
|
}
|
|
1938
2317
|
destroy() {
|
|
2318
|
+
this.filterRow?.destroy();
|
|
1939
2319
|
if (this.frame) cancelAnimationFrame(this.frame);
|
|
1940
2320
|
this.scroller.removeEventListener("scroll", this.onScroll);
|
|
1941
2321
|
this.root.remove();
|
|
@@ -1951,14 +2331,14 @@ var Section = class {
|
|
|
1951
2331
|
this.el = el("div", cls, { role: "rowgroup" });
|
|
1952
2332
|
this.panes = mk("fg-section-pane");
|
|
1953
2333
|
this.inners = mk("fg-section-inner");
|
|
1954
|
-
for (const r of
|
|
2334
|
+
for (const r of REGIONS3) {
|
|
1955
2335
|
this.panes[r].append(this.inners[r]);
|
|
1956
2336
|
this.el.append(this.panes[r]);
|
|
1957
2337
|
}
|
|
1958
2338
|
}
|
|
1959
2339
|
size(columns, height, vScrollbar) {
|
|
1960
2340
|
this.el.style.height = `${height}px`;
|
|
1961
|
-
for (const r of
|
|
2341
|
+
for (const r of REGIONS3) {
|
|
1962
2342
|
const w = columns.regionWidth(r);
|
|
1963
2343
|
if (r !== "center") {
|
|
1964
2344
|
this.panes[r].style.width = `${w}px`;
|
|
@@ -1970,6 +2350,44 @@ var Section = class {
|
|
|
1970
2350
|
this.panes.center.style.marginRight = `${vScrollbar}px`;
|
|
1971
2351
|
}
|
|
1972
2352
|
};
|
|
2353
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
2354
|
+
function sparkline(nums, opt, width, rowHeight) {
|
|
2355
|
+
const w = Math.max(20, width - 16), h = Math.max(10, rowHeight - 14);
|
|
2356
|
+
const svgEl = document.createElementNS(SVG_NS, "svg");
|
|
2357
|
+
svgEl.setAttribute("class", "fg-sparkline");
|
|
2358
|
+
svgEl.setAttribute("width", String(w));
|
|
2359
|
+
svgEl.setAttribute("height", String(h));
|
|
2360
|
+
svgEl.setAttribute("viewBox", `0 0 ${w} ${h}`);
|
|
2361
|
+
svgEl.setAttribute("aria-hidden", "true");
|
|
2362
|
+
const color = opt.color ?? "var(--fg-accent)";
|
|
2363
|
+
const bars = opt.type === "bar";
|
|
2364
|
+
let min = bars ? Math.min(0, ...nums) : Math.min(...nums);
|
|
2365
|
+
let max = bars ? Math.max(0, ...nums) : Math.max(...nums);
|
|
2366
|
+
if (min === max) {
|
|
2367
|
+
min -= 1;
|
|
2368
|
+
max += 1;
|
|
2369
|
+
}
|
|
2370
|
+
const span = max - min;
|
|
2371
|
+
const y = (v) => h - 1 - (v - min) / span * (h - 2);
|
|
2372
|
+
const mk = (tag, attrs) => {
|
|
2373
|
+
const n = document.createElementNS(SVG_NS, tag);
|
|
2374
|
+
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, String(v));
|
|
2375
|
+
svgEl.append(n);
|
|
2376
|
+
return n;
|
|
2377
|
+
};
|
|
2378
|
+
if (opt.type === "bar") {
|
|
2379
|
+
const bw = w / nums.length;
|
|
2380
|
+
nums.forEach((v, i) => mk("rect", { x: i * bw + 1, y: Math.min(y(v), y(0)), width: Math.max(1, bw - 2), height: Math.max(1, Math.abs(y(v) - y(0))), fill: v < 0 ? "#ef4444" : color }));
|
|
2381
|
+
return svgEl;
|
|
2382
|
+
}
|
|
2383
|
+
const step = nums.length > 1 ? (w - 2) / (nums.length - 1) : 0;
|
|
2384
|
+
const pts = nums.map((v, i) => `${1 + i * step},${y(v)}`).join(" ");
|
|
2385
|
+
if (opt.type === "area") mk("polygon", { points: `1,${h} ${pts} ${1 + (nums.length - 1) * step},${h}`, fill: color, "fill-opacity": 0.2 });
|
|
2386
|
+
mk("polyline", { points: pts, fill: "none", stroke: color, "stroke-width": 1.5, "stroke-linejoin": "round" });
|
|
2387
|
+
const last = nums.length - 1;
|
|
2388
|
+
mk("circle", { cx: 1 + last * step, cy: y(nums[last]), r: 2, fill: color });
|
|
2389
|
+
return svgEl;
|
|
2390
|
+
}
|
|
1973
2391
|
|
|
1974
2392
|
// src/interactions/groupPanel.ts
|
|
1975
2393
|
var DRAG_THRESHOLD = 4;
|
|
@@ -2119,11 +2537,16 @@ var en = {
|
|
|
2119
2537
|
hideColumn: "Hide column",
|
|
2120
2538
|
close: "Close",
|
|
2121
2539
|
copy: "Copy",
|
|
2540
|
+
cut: "Cut",
|
|
2122
2541
|
copyWithHeaders: "Copy with headers",
|
|
2123
2542
|
paste: "Paste",
|
|
2124
2543
|
pasteHint: "Clipboard access was blocked. Press Ctrl+V to paste.",
|
|
2125
2544
|
required: "This field is required",
|
|
2126
2545
|
invalidValue: "Invalid value",
|
|
2546
|
+
and: "And",
|
|
2547
|
+
or: "Or",
|
|
2548
|
+
advancedFilter: "Advanced filter",
|
|
2549
|
+
addCondition: "Add condition",
|
|
2127
2550
|
duplicate: "This value is already used",
|
|
2128
2551
|
undo: "Undo",
|
|
2129
2552
|
redo: "Redo",
|
|
@@ -2171,6 +2594,9 @@ var en = {
|
|
|
2171
2594
|
chartLine: "Line chart",
|
|
2172
2595
|
chartArea: "Area chart",
|
|
2173
2596
|
chartPie: "Pie chart",
|
|
2597
|
+
chartDonut: "Donut chart",
|
|
2598
|
+
chartCombo: "Bar and line chart",
|
|
2599
|
+
chartScatter: "Scatter chart",
|
|
2174
2600
|
freeBadge: "Free version",
|
|
2175
2601
|
ops: {
|
|
2176
2602
|
contains: "Contains",
|
|
@@ -2202,7 +2628,11 @@ function matchCode(tag) {
|
|
|
2202
2628
|
const lower = tag.toLowerCase();
|
|
2203
2629
|
for (const k of registry.keys()) if (k.toLowerCase() === lower) return k;
|
|
2204
2630
|
const lang = lower.split("-")[0];
|
|
2205
|
-
if (lang === "zh"
|
|
2631
|
+
if (lang === "zh") {
|
|
2632
|
+
const traditional = /-(tw|hk|mo|hant)(-|$)/.test(lower);
|
|
2633
|
+
if (traditional && registry.has("zh-TW")) return "zh-TW";
|
|
2634
|
+
if (registry.has("zh-CN")) return "zh-CN";
|
|
2635
|
+
}
|
|
2206
2636
|
for (const k of registry.keys()) if (k.toLowerCase().split("-")[0] === lang) return k;
|
|
2207
2637
|
return void 0;
|
|
2208
2638
|
}
|
|
@@ -2344,6 +2774,9 @@ function attachBody(grid) {
|
|
|
2344
2774
|
const before = grid.store.state.focus;
|
|
2345
2775
|
const hit = renderer.cellAt(e.target);
|
|
2346
2776
|
if (hit && grid.rowModel.getRow(hit.rowIndex)?.kind) {
|
|
2777
|
+
if (hit.colId === SELECT_COL_ID && grid.rowModel.getRow(hit.rowIndex).kind === "group") {
|
|
2778
|
+
grid.toggleGroupSelection(grid.rowModel.getRow(hit.rowIndex));
|
|
2779
|
+
}
|
|
2347
2780
|
renderer.gridEl.focus({ preventScroll: true });
|
|
2348
2781
|
grid.setFocus(hit.rowIndex, hit.colId);
|
|
2349
2782
|
grid.setRange(null);
|
|
@@ -2352,6 +2785,14 @@ function attachBody(grid) {
|
|
|
2352
2785
|
const ev = toEvent(e);
|
|
2353
2786
|
if (!ev) return;
|
|
2354
2787
|
renderer.gridEl.focus({ preventScroll: true });
|
|
2788
|
+
if (ev.colId === ROWNUM_COL_ID) {
|
|
2789
|
+
const data = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
|
|
2790
|
+
if (data.length) {
|
|
2791
|
+
grid.setFocus(ev.rowIndex, data[0].id);
|
|
2792
|
+
grid.setRange({ start: { rowIndex: ev.rowIndex, colId: data[0].id }, end: { rowIndex: ev.rowIndex, colId: data[data.length - 1].id } });
|
|
2793
|
+
}
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2355
2796
|
const dragged = suppressClick;
|
|
2356
2797
|
suppressClick = false;
|
|
2357
2798
|
const prev = grid.store.state.focus;
|
|
@@ -2418,6 +2859,9 @@ function attachBody(grid) {
|
|
|
2418
2859
|
case "copy":
|
|
2419
2860
|
grid.copyToClipboard(false);
|
|
2420
2861
|
return true;
|
|
2862
|
+
case "cut":
|
|
2863
|
+
grid.cutToClipboard();
|
|
2864
|
+
return true;
|
|
2421
2865
|
case "selectAll":
|
|
2422
2866
|
grid.selectAllCells();
|
|
2423
2867
|
return true;
|
|
@@ -2653,6 +3097,14 @@ function attachBody(grid) {
|
|
|
2653
3097
|
e.clipboardData.setData("text/plain", text2);
|
|
2654
3098
|
e.preventDefault();
|
|
2655
3099
|
};
|
|
3100
|
+
const onCut = (e) => {
|
|
3101
|
+
if (fromField(e) || nested(e)) return;
|
|
3102
|
+
const text2 = grid.selectionTSV(false);
|
|
3103
|
+
if (!text2 || !e.clipboardData) return;
|
|
3104
|
+
e.clipboardData.setData("text/plain", text2);
|
|
3105
|
+
e.preventDefault();
|
|
3106
|
+
grid.clearCells(true);
|
|
3107
|
+
};
|
|
2656
3108
|
const onPaste = (e) => {
|
|
2657
3109
|
if (fromField(e) || nested(e)) return;
|
|
2658
3110
|
const text2 = e.clipboardData?.getData("text/plain");
|
|
@@ -2666,6 +3118,7 @@ function attachBody(grid) {
|
|
|
2666
3118
|
root.addEventListener("keydown", onKeyDown);
|
|
2667
3119
|
root.addEventListener("copy", onCopy);
|
|
2668
3120
|
root.addEventListener("paste", onPaste);
|
|
3121
|
+
root.addEventListener("cut", onCut);
|
|
2669
3122
|
return () => {
|
|
2670
3123
|
body.removeEventListener("click", onClick);
|
|
2671
3124
|
body.removeEventListener("dblclick", onDblClick);
|
|
@@ -2673,6 +3126,7 @@ function attachBody(grid) {
|
|
|
2673
3126
|
root.removeEventListener("keydown", onKeyDown);
|
|
2674
3127
|
root.removeEventListener("copy", onCopy);
|
|
2675
3128
|
root.removeEventListener("paste", onPaste);
|
|
3129
|
+
root.removeEventListener("cut", onCut);
|
|
2676
3130
|
};
|
|
2677
3131
|
}
|
|
2678
3132
|
|
|
@@ -3147,6 +3601,8 @@ var ContextMenu = class {
|
|
|
3147
3601
|
const items = [
|
|
3148
3602
|
{ label: loc.copy, shortcut: shortcut("C"), action: () => grid.copyToClipboard(false) },
|
|
3149
3603
|
{ label: loc.copyWithHeaders, action: () => grid.copyToClipboard(true) },
|
|
3604
|
+
// after the copy items, so defaultItems indexes from earlier versions still point at the same entries
|
|
3605
|
+
{ label: loc.cut, shortcut: shortcut("X"), action: () => grid.cutToClipboard() },
|
|
3150
3606
|
...grid.store.state.selected.size ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
|
|
3151
3607
|
{ label: loc.paste, shortcut: shortcut("V"), action: () => this.pasteFromClipboard() }
|
|
3152
3608
|
];
|
|
@@ -3186,6 +3642,7 @@ var ContextMenu = class {
|
|
|
3186
3642
|
{ label: loc.exportCsv, action: () => grid.api.exportCsv() },
|
|
3187
3643
|
{ label: loc.exportExcel, action: () => grid.api.exportXlsx() }
|
|
3188
3644
|
);
|
|
3645
|
+
if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
|
|
3189
3646
|
return items;
|
|
3190
3647
|
}
|
|
3191
3648
|
pasteFromClipboard() {
|
|
@@ -3532,7 +3989,7 @@ function fillSeries(source, count) {
|
|
|
3532
3989
|
const asDate = source[source.length - 1] instanceof Date;
|
|
3533
3990
|
return Array.from({ length: count }, (_, i) => {
|
|
3534
3991
|
const d = new Date(lastDate.getFullYear(), lastDate.getMonth(), lastDate.getDate() + step * (i + 1));
|
|
3535
|
-
return asDate ? d :
|
|
3992
|
+
return asDate ? d : toDateKey2(d);
|
|
3536
3993
|
});
|
|
3537
3994
|
}
|
|
3538
3995
|
return repeat();
|
|
@@ -3616,13 +4073,14 @@ function parseDelimited(text2, sep) {
|
|
|
3616
4073
|
}
|
|
3617
4074
|
function copyValue(def, value, locale) {
|
|
3618
4075
|
if (isBlank(value)) return "";
|
|
4076
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
3619
4077
|
switch (def.type) {
|
|
3620
4078
|
case "number": {
|
|
3621
4079
|
const n = typeof value === "number" ? value : Number(value);
|
|
3622
4080
|
return Number.isNaN(n) ? String(value) : String(n);
|
|
3623
4081
|
}
|
|
3624
4082
|
case "date":
|
|
3625
|
-
return
|
|
4083
|
+
return toDateKey2(value) ?? String(value);
|
|
3626
4084
|
case "boolean":
|
|
3627
4085
|
return value === true ? "TRUE" : value === false ? "FALSE" : String(value);
|
|
3628
4086
|
default:
|
|
@@ -3661,7 +4119,7 @@ function parseDate(text2, previous) {
|
|
|
3661
4119
|
else if (m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s)) date = validDate(+m[3], +m[1], +m[2]);
|
|
3662
4120
|
else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date = toDate(s);
|
|
3663
4121
|
if (!date) return { ok: false };
|
|
3664
|
-
return { ok: true, value: previous instanceof Date ? date :
|
|
4122
|
+
return { ok: true, value: previous instanceof Date ? date : toDateKey2(date) };
|
|
3665
4123
|
}
|
|
3666
4124
|
function parseValue(def, text2, previous) {
|
|
3667
4125
|
switch (def.type) {
|
|
@@ -3698,6 +4156,50 @@ var CellEditor = class {
|
|
|
3698
4156
|
constructor(grid) {
|
|
3699
4157
|
this.grid = grid;
|
|
3700
4158
|
this.cur = null;
|
|
4159
|
+
/** editType 'row': the row being edited and its values before the first change. */
|
|
4160
|
+
this.session = null;
|
|
4161
|
+
grid.events.on("focusChange", ({ rowIndex }) => {
|
|
4162
|
+
if (this.session && !this.cur && this.grid.rowModel.getRow(rowIndex)?.data !== this.session.row) this.endRow(true);
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
get rowMode() {
|
|
4166
|
+
return this.grid.options.editType === "row";
|
|
4167
|
+
}
|
|
4168
|
+
/** Is this row id being edited as a whole row? */
|
|
4169
|
+
editingRow(id) {
|
|
4170
|
+
return this.session?.id === id;
|
|
4171
|
+
}
|
|
4172
|
+
beginRow(row, rowIndex) {
|
|
4173
|
+
if (this.session?.row === row) return;
|
|
4174
|
+
if (this.session) this.endRow(true);
|
|
4175
|
+
this.session = { row, id: this.grid.rowModel.idOf(row), before: { ...row }, undoMark: this.grid.history.mark() };
|
|
4176
|
+
this.grid.renderer.invalidate({ rows: true });
|
|
4177
|
+
this.grid.events.emit("rowEditStart", { row, rowIndex });
|
|
4178
|
+
}
|
|
4179
|
+
/** Finish the row session: keep the values, or put the row back as it was. */
|
|
4180
|
+
endRow(commit) {
|
|
4181
|
+
const s = this.session;
|
|
4182
|
+
if (!s) return;
|
|
4183
|
+
if (this.cur) {
|
|
4184
|
+
if (commit && !this.commit(null)) return;
|
|
4185
|
+
if (!commit) this.close(false);
|
|
4186
|
+
}
|
|
4187
|
+
this.session = null;
|
|
4188
|
+
const { grid } = this;
|
|
4189
|
+
const rowIndex = grid.rowModel.indexOfId(s.id);
|
|
4190
|
+
const cols = grid.columns.displayed().filter((c) => c.def.field && !isSystemCol(c.id));
|
|
4191
|
+
if (!commit) {
|
|
4192
|
+
const changes2 = [];
|
|
4193
|
+
for (const c of cols) {
|
|
4194
|
+
const f = c.def.field;
|
|
4195
|
+
if (!Object.is(s.row[f], s.before[f])) grid.writeCell(changes2, s.row, f, c.id, rowIndex, s.before[f]);
|
|
4196
|
+
}
|
|
4197
|
+
grid.endCellTx(changes2);
|
|
4198
|
+
grid.history.rollback(s.undoMark);
|
|
4199
|
+
}
|
|
4200
|
+
const changes = cols.filter((c) => !Object.is(s.row[c.def.field], s.before[c.def.field])).map((c) => ({ colId: c.id, oldValue: s.before[c.def.field], newValue: s.row[c.def.field] }));
|
|
4201
|
+
grid.renderer.invalidate({ rows: true });
|
|
4202
|
+
grid.events.emit("rowEditStop", { row: s.row, rowIndex, committed: commit, changes });
|
|
3701
4203
|
}
|
|
3702
4204
|
get active() {
|
|
3703
4205
|
return this.cur && { rowIndex: this.cur.rowIndex, colId: this.cur.colId };
|
|
@@ -3710,7 +4212,7 @@ var CellEditor = class {
|
|
|
3710
4212
|
return !node.kind && cellEditable(col.def, node.data, rowIndex);
|
|
3711
4213
|
}
|
|
3712
4214
|
start(rowIndex, colId, initialText) {
|
|
3713
|
-
if (this.cur
|
|
4215
|
+
if (this.cur && !this.commit(null)) return false;
|
|
3714
4216
|
if (!this.canEdit(rowIndex, colId)) return false;
|
|
3715
4217
|
const { grid } = this;
|
|
3716
4218
|
if (grid.veto("beforeEdit", { row: grid.rowModel.getRow(rowIndex).data, rowIndex, colId })) return false;
|
|
@@ -3719,17 +4221,12 @@ var CellEditor = class {
|
|
|
3719
4221
|
const def = col.def;
|
|
3720
4222
|
const row = grid.rowModel.getRow(rowIndex).data;
|
|
3721
4223
|
const value = row[def.field];
|
|
4224
|
+
if (this.rowMode) this.beginRow(row, rowIndex);
|
|
3722
4225
|
grid.setFocus(rowIndex, colId);
|
|
3723
4226
|
renderer.ensureVisible(rowIndex, colId);
|
|
3724
4227
|
renderer.render();
|
|
3725
|
-
const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
|
|
3726
|
-
const rootBox = renderer.root.getBoundingClientRect();
|
|
3727
|
-
const box = cellEl?.getBoundingClientRect();
|
|
3728
4228
|
const wrap = el("div", "fg-editor");
|
|
3729
|
-
|
|
3730
|
-
wrap.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
|
|
3731
|
-
wrap.style.width = `${box?.width || col.width}px`;
|
|
3732
|
-
wrap.style.height = `${box?.height || renderer.opts.rowHeight}px`;
|
|
4229
|
+
this.place(wrap, rowIndex, colId);
|
|
3733
4230
|
let control;
|
|
3734
4231
|
let read;
|
|
3735
4232
|
let dispose = () => {
|
|
@@ -3774,7 +4271,7 @@ var CellEditor = class {
|
|
|
3774
4271
|
input.type = def.type === "date" && initialText === void 0 ? "date" : "text";
|
|
3775
4272
|
if (def.type === "number") input.inputMode = "decimal";
|
|
3776
4273
|
}
|
|
3777
|
-
const start = def.type === "date" ?
|
|
4274
|
+
const start = def.type === "date" ? toDateKey2(value) ?? "" : isBlank(value) ? "" : String(value);
|
|
3778
4275
|
input.value = initialText ?? start;
|
|
3779
4276
|
control = input;
|
|
3780
4277
|
read = () => ({ text: input.value });
|
|
@@ -3827,6 +4324,36 @@ var CellEditor = class {
|
|
|
3827
4324
|
grid.events.emit("editStart", { rowIndex, colId });
|
|
3828
4325
|
return true;
|
|
3829
4326
|
}
|
|
4327
|
+
/** Put the editor over its cell; false when the cell is not rendered. */
|
|
4328
|
+
place(wrap, rowIndex, colId) {
|
|
4329
|
+
const { renderer } = this.grid;
|
|
4330
|
+
const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
|
|
4331
|
+
const rootBox = renderer.root.getBoundingClientRect();
|
|
4332
|
+
const box = cellEl?.getBoundingClientRect();
|
|
4333
|
+
wrap.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
|
|
4334
|
+
wrap.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
|
|
4335
|
+
wrap.style.width = `${box?.width || this.grid.columns.getColumn(colId)?.width}px`;
|
|
4336
|
+
wrap.style.height = `${box?.height || renderer.opts.rowHeight}px`;
|
|
4337
|
+
return !!cellEl;
|
|
4338
|
+
}
|
|
4339
|
+
/** Display index of a row object, or -1 when it is no longer shown. */
|
|
4340
|
+
indexOf(row) {
|
|
4341
|
+
const m = this.grid.rowModel;
|
|
4342
|
+
const i = m.indexOfId(m.idOf(row));
|
|
4343
|
+
return i >= 0 && m.getRow(i)?.data === row ? i : -1;
|
|
4344
|
+
}
|
|
4345
|
+
/** The rows changed underneath (transactions): follow the edited row, or cancel when it is gone. */
|
|
4346
|
+
sync() {
|
|
4347
|
+
const cur = this.cur;
|
|
4348
|
+
if (!cur) return;
|
|
4349
|
+
const i = this.indexOf(cur.row);
|
|
4350
|
+
if (i < 0) return this.cancel();
|
|
4351
|
+
if (i === cur.rowIndex) return;
|
|
4352
|
+
cur.rowIndex = i;
|
|
4353
|
+
this.grid.setFocus(i, cur.colId);
|
|
4354
|
+
this.grid.renderer.render();
|
|
4355
|
+
if (!this.place(cur.wrap, i, cur.colId)) this.commit(null);
|
|
4356
|
+
}
|
|
3830
4357
|
/** Validate and write the value. Returns false (editor stays open) when invalid. */
|
|
3831
4358
|
commit(move) {
|
|
3832
4359
|
const cur = this.cur;
|
|
@@ -3846,13 +4373,29 @@ var CellEditor = class {
|
|
|
3846
4373
|
} else value = raw.value;
|
|
3847
4374
|
const error = grid.cellError(cur.def, cur.row, value);
|
|
3848
4375
|
if (error !== null) return this.fail(value, error);
|
|
4376
|
+
const rowIndex = this.indexOf(cur.row);
|
|
4377
|
+
if (rowIndex < 0) {
|
|
4378
|
+
this.cancel();
|
|
4379
|
+
return false;
|
|
4380
|
+
}
|
|
4381
|
+
cur.rowIndex = rowIndex;
|
|
3849
4382
|
this.close(true);
|
|
3850
|
-
grid.applyEdit(
|
|
3851
|
-
this.move
|
|
4383
|
+
grid.applyEdit(rowIndex, cur.colId, value);
|
|
4384
|
+
if (this.session && (move === "down" || move === "up")) {
|
|
4385
|
+
this.endRow(true);
|
|
4386
|
+
}
|
|
4387
|
+
this.move(rowIndex, cur.colId, move);
|
|
3852
4388
|
return true;
|
|
3853
4389
|
}
|
|
3854
4390
|
cancel() {
|
|
3855
4391
|
this.close(false);
|
|
4392
|
+
if (this.session) this.endRow(false);
|
|
4393
|
+
}
|
|
4394
|
+
/** Teardown: remove the editor without writing values or emitting events. */
|
|
4395
|
+
destroy() {
|
|
4396
|
+
this.cur?.dispose();
|
|
4397
|
+
this.cur = null;
|
|
4398
|
+
this.session = null;
|
|
3856
4399
|
}
|
|
3857
4400
|
fail(value, message) {
|
|
3858
4401
|
const cur = this.cur;
|
|
@@ -3886,11 +4429,11 @@ var CellEditor = class {
|
|
|
3886
4429
|
let i = cols.findIndex((c) => c.id === colId) + step;
|
|
3887
4430
|
if (i >= cols.length) {
|
|
3888
4431
|
i = 0;
|
|
3889
|
-
r++;
|
|
4432
|
+
if (!this.session) r++;
|
|
3890
4433
|
}
|
|
3891
4434
|
if (i < 0) {
|
|
3892
4435
|
i = cols.length - 1;
|
|
3893
|
-
r--;
|
|
4436
|
+
if (!this.session) r--;
|
|
3894
4437
|
}
|
|
3895
4438
|
if (r < 0 || r >= count) return;
|
|
3896
4439
|
this.start(r, cols[i].id);
|
|
@@ -3926,6 +4469,16 @@ var History = class {
|
|
|
3926
4469
|
this.entries[this.index++].redo();
|
|
3927
4470
|
return true;
|
|
3928
4471
|
}
|
|
4472
|
+
/** The current position, for rollback(). */
|
|
4473
|
+
mark() {
|
|
4474
|
+
return this.entries[this.index - 1] ?? null;
|
|
4475
|
+
}
|
|
4476
|
+
/** Forget every step pushed after `mark` (a cancelled edit session). */
|
|
4477
|
+
rollback(mark) {
|
|
4478
|
+
const i = mark ? this.entries.indexOf(mark) + 1 : 0;
|
|
4479
|
+
this.entries.splice(i);
|
|
4480
|
+
this.index = Math.min(this.index, i);
|
|
4481
|
+
}
|
|
3929
4482
|
clear() {
|
|
3930
4483
|
this.entries = [];
|
|
3931
4484
|
this.index = 0;
|
|
@@ -3954,6 +4507,14 @@ var ChangeTracker = class {
|
|
|
3954
4507
|
const o = this.originals.get(row);
|
|
3955
4508
|
return o ? fields.filter((f) => !same(o[f], row[f])) : [];
|
|
3956
4509
|
}
|
|
4510
|
+
/** Baseline `rows` (row → the values that were saved); `deleted` rows are no longer pending. */
|
|
4511
|
+
commit(rows, deleted) {
|
|
4512
|
+
for (const r of deleted) this.deleted.delete(r);
|
|
4513
|
+
for (const [row, values] of rows) {
|
|
4514
|
+
this.created.delete(row);
|
|
4515
|
+
this.originals.set(row, { ...values });
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
3957
4518
|
get size() {
|
|
3958
4519
|
return this.originals.size + this.created.size + this.deleted.size;
|
|
3959
4520
|
}
|
|
@@ -4214,12 +4775,34 @@ function zipStore(files) {
|
|
|
4214
4775
|
return out;
|
|
4215
4776
|
}
|
|
4216
4777
|
var isZip = (data) => data.length > 4 && data[0] === 80 && data[1] === 75 && data[2] === 3 && data[3] === 4;
|
|
4217
|
-
|
|
4778
|
+
var tooLarge = (max) => new Error(`Zip file is too large: it unpacks to more than ${max >= 1048576 ? `${Math.round(max / 1048576)} MB` : `${max} bytes`}`);
|
|
4779
|
+
async function inflateRaw(data, max) {
|
|
4218
4780
|
if (typeof DecompressionStream === "undefined") throw new Error("This browser cannot read compressed xlsx files (no DecompressionStream)");
|
|
4219
4781
|
const stream = new Response(data).body.pipeThrough(new DecompressionStream("deflate-raw"));
|
|
4220
|
-
|
|
4782
|
+
const reader = stream.getReader();
|
|
4783
|
+
const chunks = [];
|
|
4784
|
+
let size = 0;
|
|
4785
|
+
for (; ; ) {
|
|
4786
|
+
const { done, value } = await reader.read();
|
|
4787
|
+
if (done) break;
|
|
4788
|
+
size += value.length;
|
|
4789
|
+
if (size > max) {
|
|
4790
|
+
await reader.cancel().catch(() => void 0);
|
|
4791
|
+
throw tooLarge(max);
|
|
4792
|
+
}
|
|
4793
|
+
chunks.push(value);
|
|
4794
|
+
}
|
|
4795
|
+
const out = new Uint8Array(size);
|
|
4796
|
+
let at = 0;
|
|
4797
|
+
for (const c of chunks) {
|
|
4798
|
+
out.set(c, at);
|
|
4799
|
+
at += c.length;
|
|
4800
|
+
}
|
|
4801
|
+
return out;
|
|
4221
4802
|
}
|
|
4222
|
-
async function unzip(data) {
|
|
4803
|
+
async function unzip(data, limits = {}) {
|
|
4804
|
+
const maxBytes = limits.maxBytes ?? 200 * 1048576;
|
|
4805
|
+
const maxEntries = limits.maxEntries ?? 1e4;
|
|
4223
4806
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
4224
4807
|
let eocd = -1;
|
|
4225
4808
|
for (let i = data.length - 22; i >= Math.max(0, data.length - 65557); i--) {
|
|
@@ -4230,9 +4813,11 @@ async function unzip(data) {
|
|
|
4230
4813
|
}
|
|
4231
4814
|
if (eocd < 0) throw new Error("Not a zip file");
|
|
4232
4815
|
const count = view.getUint16(eocd + 10, true);
|
|
4816
|
+
if (count > maxEntries) throw new Error(`Zip file has too many entries (${count}, at most ${maxEntries})`);
|
|
4233
4817
|
let p = view.getUint32(eocd + 16, true);
|
|
4234
4818
|
const decoder = new TextDecoder();
|
|
4235
4819
|
const out = /* @__PURE__ */ new Map();
|
|
4820
|
+
let total = 0;
|
|
4236
4821
|
for (let i = 0; i < count; i++) {
|
|
4237
4822
|
if (view.getUint32(p, true) !== 33639248) throw new Error("Broken zip directory");
|
|
4238
4823
|
const method = view.getUint16(p + 10, true);
|
|
@@ -4243,9 +4828,13 @@ async function unzip(data) {
|
|
|
4243
4828
|
const lName = view.getUint16(offset + 26, true), lExtra = view.getUint16(offset + 28, true);
|
|
4244
4829
|
const start = offset + 30 + lName + lExtra;
|
|
4245
4830
|
const raw = data.subarray(start, start + size);
|
|
4246
|
-
|
|
4247
|
-
|
|
4831
|
+
let bytes;
|
|
4832
|
+
if (method === 0) bytes = raw;
|
|
4833
|
+
else if (method === 8) bytes = await inflateRaw(raw, maxBytes - total);
|
|
4248
4834
|
else throw new Error(`Unsupported zip compression ${method}`);
|
|
4835
|
+
total += bytes.length;
|
|
4836
|
+
if (total > maxBytes) throw tooLarge(maxBytes);
|
|
4837
|
+
out.set(name, bytes);
|
|
4249
4838
|
p += 46 + nameLen + extraLen + commentLen;
|
|
4250
4839
|
}
|
|
4251
4840
|
return out;
|
|
@@ -4366,7 +4955,8 @@ function writeXlsx(sheets) {
|
|
|
4366
4955
|
const views = freeze ? `<sheetViews><sheetView workbookViewId="0"><pane ySplit="${freeze}" topLeftCell="A${freeze + 1}" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>` : '<sheetViews><sheetView workbookViewId="0"/></sheetViews>';
|
|
4367
4956
|
const cols = sheet.widths?.length ? `<cols>${sheet.widths.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${Math.max(4, Math.min(120, w)).toFixed(1)}" customWidth="1"/>`).join("")}</cols>` : "";
|
|
4368
4957
|
const filter = sheet.autoFilter && width && freeze && sheet.rows.length >= freeze ? `<autoFilter ref="A${freeze}:${colName(width - 1)}${sheet.rows.length}"/>` : "";
|
|
4369
|
-
const
|
|
4958
|
+
const mergeRefs = (sheet.merges ?? []).filter((m) => typeof m === "string" && /^[A-Z]{1,3}\d{1,7}:[A-Z]{1,3}\d{1,7}$/.test(m));
|
|
4959
|
+
const merges = mergeRefs.length ? `<mergeCells count="${mergeRefs.length}">${mergeRefs.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
|
|
4370
4960
|
return `${XML}<worksheet xmlns="${NS}" xmlns:r="${REL_NS}">${views}<sheetFormatPr defaultRowHeight="15"/>${cols}<sheetData>${rowsXml}</sheetData>${filter}${merges}</worksheet>`;
|
|
4371
4961
|
});
|
|
4372
4962
|
const styles = `${XML}<styleSheet xmlns="${NS}">` + (customFmts.length ? `<numFmts count="${customFmts.length}">${customFmts.map((f, i) => `<numFmt numFmtId="${164 + i}" formatCode="${esc(f)}"/>`).join("")}</numFmts>` : "") + `<fonts count="${fonts.length}">${fonts.join("")}</fonts><fills count="${fills.length}">${fills.join("")}</fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${xfs.length}">${xfs.join("")}</cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
|
|
@@ -4414,6 +5004,8 @@ function colIndex(ref) {
|
|
|
4414
5004
|
}
|
|
4415
5005
|
return n - 1;
|
|
4416
5006
|
}
|
|
5007
|
+
var MAX_ROWS = 1048576;
|
|
5008
|
+
var MAX_COLS = 16384;
|
|
4417
5009
|
async function readXlsx(input) {
|
|
4418
5010
|
const data = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
4419
5011
|
if (!isZip(data)) throw new Error("Not an xlsx file");
|
|
@@ -4441,11 +5033,13 @@ async function readXlsx(input) {
|
|
|
4441
5033
|
if (doc) {
|
|
4442
5034
|
for (const row of tags(doc, "row")) {
|
|
4443
5035
|
const r = Number(row.getAttribute("r") ?? rows.length + 1) - 1;
|
|
5036
|
+
if (!Number.isInteger(r) || r < 0 || r >= MAX_ROWS) continue;
|
|
4444
5037
|
const out = [];
|
|
4445
5038
|
let next = 0;
|
|
4446
5039
|
for (const c of [...row.children].filter((x) => x.localName === "c")) {
|
|
4447
5040
|
const ref = c.getAttribute("r");
|
|
4448
5041
|
const ci = ref ? colIndex(ref) : next;
|
|
5042
|
+
if (ci < 0 || ci >= MAX_COLS) continue;
|
|
4449
5043
|
next = ci + 1;
|
|
4450
5044
|
const t = c.getAttribute("t");
|
|
4451
5045
|
const v = tags(c, "v")[0]?.textContent ?? null;
|
|
@@ -4490,6 +5084,81 @@ function exportRows(grid, mode) {
|
|
|
4490
5084
|
return grid.rowModel.dataRows().map((n) => n.data);
|
|
4491
5085
|
}
|
|
4492
5086
|
var header = (def, id) => def.header ?? def.field ?? id;
|
|
5087
|
+
var ALIGNS = ["left", "center", "right"];
|
|
5088
|
+
var PLAIN_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
5089
|
+
var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
5090
|
+
function buildPrintHtml(grid, opts = {}) {
|
|
5091
|
+
const cols = exportColumns(grid, opts.columns);
|
|
5092
|
+
const code = grid.store.state.locale.code;
|
|
5093
|
+
const fmts = cols.map((c) => getFormatter(c.def, code));
|
|
5094
|
+
const chains = cols.map((c) => {
|
|
5095
|
+
const chain = [];
|
|
5096
|
+
for (let n = c.node; n; n = n.parent) chain.unshift(n);
|
|
5097
|
+
return chain;
|
|
5098
|
+
});
|
|
5099
|
+
const depth = Math.max(1, ...chains.map((ch) => ch.length));
|
|
5100
|
+
const head = [];
|
|
5101
|
+
for (let level = 0; level < depth; level++) {
|
|
5102
|
+
let tr = "";
|
|
5103
|
+
for (let ci = 0; ci < cols.length; ci++) {
|
|
5104
|
+
const chain = chains[ci];
|
|
5105
|
+
const leaf = chain.length - 1;
|
|
5106
|
+
if (level < leaf) {
|
|
5107
|
+
const g = chain[level];
|
|
5108
|
+
if (ci > 0 && chains[ci - 1][level] === g) continue;
|
|
5109
|
+
let span = 1;
|
|
5110
|
+
while (ci + span < cols.length && chains[ci + span][level] === g) span++;
|
|
5111
|
+
tr += `<th colspan="${span}">${escHtml(header(g.def, g.id))}</th>`;
|
|
5112
|
+
} else if (level === leaf) {
|
|
5113
|
+
tr += `<th colspan="1" rowspan="${depth - level}">${escHtml(header(cols[ci].def, cols[ci].id))}</th>`;
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
head.push(`<tr>${tr}</tr>`);
|
|
5117
|
+
}
|
|
5118
|
+
const align = (def) => ALIGNS.includes(def.align) ? def.align : def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left";
|
|
5119
|
+
const body = exportRows(grid, opts.rows).map((r) => `<tr>${cols.map((c, i) => {
|
|
5120
|
+
const v = getCellValue(c.def, r);
|
|
5121
|
+
return `<td style="text-align:${align(c.def)}">${escHtml(isBlank(v) ? "" : fmts[i](v))}</td>`;
|
|
5122
|
+
}).join("")}</tr>`).join("");
|
|
5123
|
+
const title = escHtml(opts.title ?? document.title ?? "");
|
|
5124
|
+
return `<!doctype html><html lang="${escHtml(code)}"><head><meta charset="utf-8"><title>${title}</title><style>
|
|
5125
|
+
@page { size: auto; margin: 12mm; }
|
|
5126
|
+
body { font: 11px/1.4 system-ui, 'Pretendard', 'Malgun Gothic', sans-serif; color: #111; margin: 0; }
|
|
5127
|
+
h1 { font-size: 15px; margin: 0 0 8px; }
|
|
5128
|
+
table { border-collapse: collapse; width: 100%; }
|
|
5129
|
+
th, td { border: 1px solid #999; padding: 3px 6px; }
|
|
5130
|
+
th { background: #eee; font-weight: 600; }
|
|
5131
|
+
thead { display: table-header-group; }
|
|
5132
|
+
tr { page-break-inside: avoid; }
|
|
5133
|
+
</style></head><body>${opts.title ? `<h1>${title}</h1>` : ""}<table><thead>${opts.headers === false ? "" : head.join("")}</thead><tbody>${body}</tbody></table></body></html>`;
|
|
5134
|
+
}
|
|
5135
|
+
function printHtml(html) {
|
|
5136
|
+
if (typeof document === "undefined") return;
|
|
5137
|
+
const frame = document.createElement("iframe");
|
|
5138
|
+
frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0";
|
|
5139
|
+
document.body.append(frame);
|
|
5140
|
+
const doc = frame.contentDocument;
|
|
5141
|
+
if (!doc || !frame.contentWindow) {
|
|
5142
|
+
frame.remove();
|
|
5143
|
+
warn("printing is not supported in this environment");
|
|
5144
|
+
return;
|
|
5145
|
+
}
|
|
5146
|
+
doc.open();
|
|
5147
|
+
doc.write(html);
|
|
5148
|
+
doc.close();
|
|
5149
|
+
const win = frame.contentWindow;
|
|
5150
|
+
const done = () => setTimeout(() => frame.remove(), 1e3);
|
|
5151
|
+
win.addEventListener("afterprint", done);
|
|
5152
|
+
setTimeout(() => {
|
|
5153
|
+
try {
|
|
5154
|
+
win.focus();
|
|
5155
|
+
win.print();
|
|
5156
|
+
} catch {
|
|
5157
|
+
warn("printing is not supported in this environment");
|
|
5158
|
+
}
|
|
5159
|
+
done();
|
|
5160
|
+
}, 50);
|
|
5161
|
+
}
|
|
4493
5162
|
function buildCsv(grid, opts = {}) {
|
|
4494
5163
|
const cols = exportColumns(grid, opts.columns);
|
|
4495
5164
|
const code = grid.store.state.locale.code;
|
|
@@ -4497,7 +5166,8 @@ function buildCsv(grid, opts = {}) {
|
|
|
4497
5166
|
const matrix = exportRows(grid, opts.rows).map((r) => cols.map((c) => {
|
|
4498
5167
|
const v = getCellValue(c.def, r);
|
|
4499
5168
|
const t = c.def.type;
|
|
4500
|
-
|
|
5169
|
+
const value = copyValue(c.def, v, code);
|
|
5170
|
+
return { value, text: !(t === "number" || t === "date" || t === "boolean") || typeof v === "string" && !PLAIN_NUMBER.test(value) };
|
|
4501
5171
|
}));
|
|
4502
5172
|
if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
|
|
4503
5173
|
return toCsv(matrix, sep);
|
|
@@ -4569,6 +5239,9 @@ function classStyles(grid) {
|
|
|
4569
5239
|
};
|
|
4570
5240
|
}
|
|
4571
5241
|
function buildXlsx(grid, opts = {}) {
|
|
5242
|
+
return writeXlsx([buildXlsxSheet(grid, opts)]);
|
|
5243
|
+
}
|
|
5244
|
+
function buildXlsxSheet(grid, opts = {}) {
|
|
4572
5245
|
const cols = exportColumns(grid, opts.columns);
|
|
4573
5246
|
const code = grid.store.state.locale.code;
|
|
4574
5247
|
const rows = [];
|
|
@@ -4674,14 +5347,14 @@ function buildXlsx(grid, opts = {}) {
|
|
|
4674
5347
|
const s = computeSpans(data, c.def);
|
|
4675
5348
|
for (let i = 0; i < data.length; i++) if (s.start[i] === i && s.end[i] > i) mergeRange(i, ci, s.end[i], ci);
|
|
4676
5349
|
});
|
|
4677
|
-
return
|
|
5350
|
+
return {
|
|
4678
5351
|
name: opts.sheetName ?? "Sheet1",
|
|
4679
5352
|
rows,
|
|
4680
5353
|
widths: cols.map((c) => Math.round(c.width / 7)),
|
|
4681
5354
|
merges,
|
|
4682
5355
|
freezeRows: headRows,
|
|
4683
5356
|
autoFilter: headRows > 0
|
|
4684
|
-
}
|
|
5357
|
+
};
|
|
4685
5358
|
}
|
|
4686
5359
|
function download(filename, parts, type) {
|
|
4687
5360
|
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function" || typeof document === "undefined") {
|
|
@@ -4703,7 +5376,7 @@ function asText(v) {
|
|
|
4703
5376
|
if (v === null || v === void 0) return "";
|
|
4704
5377
|
if (v instanceof Date) {
|
|
4705
5378
|
const hasTime = v.getHours() || v.getMinutes() || v.getSeconds();
|
|
4706
|
-
return hasTime ? v.toISOString() :
|
|
5379
|
+
return hasTime ? v.toISOString() : toDateKey2(v) ?? "";
|
|
4707
5380
|
}
|
|
4708
5381
|
return String(v);
|
|
4709
5382
|
}
|
|
@@ -4811,7 +5484,8 @@ function renderChart(el2, spec) {
|
|
|
4811
5484
|
top = 36;
|
|
4812
5485
|
}
|
|
4813
5486
|
const legend = svg("g", {}, "fg-chart-legend");
|
|
4814
|
-
const
|
|
5487
|
+
const round2 = spec.type === "pie" || spec.type === "donut";
|
|
5488
|
+
const names = round2 ? cats : spec.type === "scatter" ? series.slice(1).map((s) => s.name) : series.map((s) => s.name);
|
|
4815
5489
|
let lx = 16;
|
|
4816
5490
|
names.forEach((name, i) => {
|
|
4817
5491
|
const item = svg("g", { transform: `translate(${lx},${H - 10})` }, "fg-chart-legend-item");
|
|
@@ -4823,7 +5497,13 @@ function renderChart(el2, spec) {
|
|
|
4823
5497
|
lx += 24 + Math.min(140, name.length * 7);
|
|
4824
5498
|
});
|
|
4825
5499
|
const bottom = H - 52;
|
|
4826
|
-
if (spec.type === "
|
|
5500
|
+
if (spec.type === "scatter") {
|
|
5501
|
+
drawScatter(root, spec, series, top, bottom, nf, short);
|
|
5502
|
+
root.append(legend);
|
|
5503
|
+
el2.replaceChildren(root);
|
|
5504
|
+
return;
|
|
5505
|
+
}
|
|
5506
|
+
if (round2) {
|
|
4827
5507
|
const values = (series[0]?.values ?? []).map((v) => v !== null && v > 0 ? v : 0);
|
|
4828
5508
|
const total = values.reduce((a, b) => a + b, 0);
|
|
4829
5509
|
const cx = W / 2, cy = (top + bottom) / 2, r = Math.max(10, (bottom - top) / 2 - 6);
|
|
@@ -4839,6 +5519,7 @@ function renderChart(el2, spec) {
|
|
|
4839
5519
|
root.append(slice);
|
|
4840
5520
|
angle = a2;
|
|
4841
5521
|
});
|
|
5522
|
+
if (spec.type === "donut") root.append(svg("circle", { cx, cy, r: r * 0.58, fill: "var(--fg-bg, #fff)" }, "fg-chart-donut-hole"));
|
|
4842
5523
|
root.append(legend);
|
|
4843
5524
|
el2.replaceChildren(root);
|
|
4844
5525
|
return;
|
|
@@ -4879,11 +5560,25 @@ function renderChart(el2, spec) {
|
|
|
4879
5560
|
cats.forEach((c, i) => {
|
|
4880
5561
|
if (i % every) return;
|
|
4881
5562
|
const label = svg("text", { x: left + band * (i + 0.5), y: bottom + 16, "text-anchor": "middle" });
|
|
4882
|
-
|
|
5563
|
+
const fit = Math.max(3, Math.min(12, Math.floor(band * every / 10)));
|
|
5564
|
+
label.textContent = c.length > fit ? `${c.slice(0, fit - 1)}\u2026` : c;
|
|
4883
5565
|
axis.append(label);
|
|
4884
5566
|
});
|
|
4885
5567
|
root.append(axis);
|
|
4886
5568
|
const plot = svg("g", {}, "fg-chart-plot");
|
|
5569
|
+
const combo = spec.type === "combo";
|
|
5570
|
+
if (combo) {
|
|
5571
|
+
const inner = band * 0.6;
|
|
5572
|
+
const s = series[0];
|
|
5573
|
+
cats.forEach((c, ci) => {
|
|
5574
|
+
const v = s?.values[ci];
|
|
5575
|
+
if (v === null || v === void 0 || !Number.isFinite(v)) return;
|
|
5576
|
+
const x = left + band * ci + (band - inner) / 2;
|
|
5577
|
+
const rect = svg("rect", { x, y: Math.min(y(0), y(v)), width: Math.max(1, inner - 2), height: Math.max(0.5, Math.abs(y(v) - y(0))), fill: chartColor(0), rx: 2 }, "fg-chart-bar");
|
|
5578
|
+
tip(rect, `${s.name} \xB7 ${c}: ${nf.format(v)}`);
|
|
5579
|
+
plot.append(rect);
|
|
5580
|
+
});
|
|
5581
|
+
}
|
|
4887
5582
|
if (spec.type === "bar" || stacked) {
|
|
4888
5583
|
const inner = band * 0.72;
|
|
4889
5584
|
const bw = stacked ? inner : inner / Math.max(1, series.length);
|
|
@@ -4907,6 +5602,7 @@ function renderChart(el2, spec) {
|
|
|
4907
5602
|
});
|
|
4908
5603
|
} else {
|
|
4909
5604
|
series.forEach((s, si) => {
|
|
5605
|
+
if (combo && si === 0) return;
|
|
4910
5606
|
const pts = s.values.map((v, ci) => v === null || v === void 0 || !Number.isFinite(v) ? null : [left + band * (ci + 0.5), y(v), v]);
|
|
4911
5607
|
let d = "";
|
|
4912
5608
|
let pen = false;
|
|
@@ -4937,11 +5633,53 @@ function renderChart(el2, spec) {
|
|
|
4937
5633
|
root.append(plot, legend);
|
|
4938
5634
|
el2.replaceChildren(root);
|
|
4939
5635
|
}
|
|
5636
|
+
function drawScatter(root, spec, series, top, bottom, nf, short) {
|
|
5637
|
+
const xs = series[0]?.values ?? [];
|
|
5638
|
+
const others = series.slice(1);
|
|
5639
|
+
const finite = (v) => v !== null && v !== void 0 && Number.isFinite(v);
|
|
5640
|
+
const allX = xs.filter(finite);
|
|
5641
|
+
const allY = others.flatMap((s) => s.values.filter(finite));
|
|
5642
|
+
if (!allX.length || !allY.length) return;
|
|
5643
|
+
const xt = niceTicks(Math.min(...allX), Math.max(...allX));
|
|
5644
|
+
const yt = niceTicks(Math.min(...allY), Math.max(...allY));
|
|
5645
|
+
const left = 56, right = W - 16;
|
|
5646
|
+
const x = (v) => left + (v - xt[0]) / (xt[xt.length - 1] - xt[0]) * (right - left);
|
|
5647
|
+
const y = (v) => bottom - (v - yt[0]) / (yt[yt.length - 1] - yt[0]) * (bottom - top);
|
|
5648
|
+
const axis = svg("g", {}, "fg-chart-axis");
|
|
5649
|
+
for (const t of yt) {
|
|
5650
|
+
axis.append(svg("line", { x1: left, x2: right, y1: y(t), y2: y(t) }, t === 0 ? "fg-chart-zero" : "fg-chart-grid"));
|
|
5651
|
+
const label = svg("text", { x: left - 6, y: y(t) + 4, "text-anchor": "end" });
|
|
5652
|
+
label.textContent = short.format(t);
|
|
5653
|
+
axis.append(label);
|
|
5654
|
+
}
|
|
5655
|
+
xt.forEach((t, i) => {
|
|
5656
|
+
const anchor = i === 0 ? "start" : i === xt.length - 1 ? "end" : "middle";
|
|
5657
|
+
const label = svg("text", { x: x(t), y: bottom + 16, "text-anchor": anchor });
|
|
5658
|
+
label.textContent = short.format(t);
|
|
5659
|
+
axis.append(label);
|
|
5660
|
+
});
|
|
5661
|
+
const xName = svg("text", { x: right, y: bottom + 32, "text-anchor": "end" }, "fg-chart-axis-name");
|
|
5662
|
+
xName.textContent = `${series[0]?.name ?? ""} \u2192`;
|
|
5663
|
+
axis.append(xName);
|
|
5664
|
+
root.append(axis);
|
|
5665
|
+
const plot = svg("g", {}, "fg-chart-plot");
|
|
5666
|
+
xs.forEach((xv, i) => {
|
|
5667
|
+
if (!finite(xv)) return;
|
|
5668
|
+
others.forEach((s, si) => {
|
|
5669
|
+
const yv = s.values[i];
|
|
5670
|
+
if (!finite(yv)) return;
|
|
5671
|
+
const dot = svg("circle", { cx: x(xv), cy: y(yv), r: 4, fill: chartColor(si), "fill-opacity": 0.8 }, "fg-chart-point");
|
|
5672
|
+
tip(dot, `${spec.categories[i] ?? ""}: ${series[0].name} ${nf.format(xv)}, ${s.name} ${nf.format(yv)}`);
|
|
5673
|
+
plot.append(dot);
|
|
5674
|
+
});
|
|
5675
|
+
});
|
|
5676
|
+
root.append(plot);
|
|
5677
|
+
}
|
|
4940
5678
|
|
|
4941
5679
|
// src/interactions/charts.ts
|
|
4942
5680
|
var MAX_SERIES = 12;
|
|
4943
|
-
var
|
|
4944
|
-
var TYPES = ["bar", "stackedBar", "line", "area", "pie"];
|
|
5681
|
+
var MAX_ROWS2 = 500;
|
|
5682
|
+
var TYPES = ["bar", "stackedBar", "line", "area", "combo", "pie", "donut", "scatter"];
|
|
4945
5683
|
var GridCharts = class {
|
|
4946
5684
|
constructor(grid) {
|
|
4947
5685
|
this.grid = grid;
|
|
@@ -4958,7 +5696,7 @@ var GridCharts = class {
|
|
|
4958
5696
|
if (a && b) {
|
|
4959
5697
|
const box = normalizeRange({ start: { rowIndex: r.start.rowIndex, colIndex: a.index }, end: { rowIndex: r.end.rowIndex, colIndex: b.index } });
|
|
4960
5698
|
const rows = [];
|
|
4961
|
-
for (let i = box.r1; i <= box.r2 && rows.length <
|
|
5699
|
+
for (let i = box.r1; i <= box.r2 && rows.length < MAX_ROWS2; i++) {
|
|
4962
5700
|
const n = grid.rowModel.getRow(i);
|
|
4963
5701
|
if (n && !n.kind) rows.push(n.data);
|
|
4964
5702
|
}
|
|
@@ -4966,7 +5704,7 @@ var GridCharts = class {
|
|
|
4966
5704
|
}
|
|
4967
5705
|
}
|
|
4968
5706
|
const data = () => {
|
|
4969
|
-
const rows = fixed ? fixed.rows : grid.rowModel.dataRows().slice(0,
|
|
5707
|
+
const rows = fixed ? fixed.rows : grid.rowModel.dataRows().slice(0, MAX_ROWS2).map((n) => n.data);
|
|
4970
5708
|
const cols = (fixed ? fixed.colIds.map((id) => grid.columns.getColumn(id)) : grid.columns.displayed().filter((c) => !isSystemCol(c.id))).filter((c) => !!c);
|
|
4971
5709
|
const cat = cols[0] && cols[0].def.type !== "number" ? cols[0] : fixed ? null : cols.find((c) => c.def.type !== "number") ?? null;
|
|
4972
5710
|
const isTotal = (c) => !!c.node.parent && c.node.parent.id === "pivotgroup|total";
|
|
@@ -4993,7 +5731,7 @@ var GridCharts = class {
|
|
|
4993
5731
|
const body = el("div", "fg-chart-body");
|
|
4994
5732
|
let panel = null;
|
|
4995
5733
|
const draw = () => renderChart(body, { type, title: opts.title, locale: loc().code, ...data() });
|
|
4996
|
-
const typeLabel = (t) => ({ bar: loc().chartBar, stackedBar: loc().chartStacked, line: loc().chartLine, area: loc().chartArea, pie: loc().chartPie })[t];
|
|
5734
|
+
const typeLabel = (t) => ({ bar: loc().chartBar, stackedBar: loc().chartStacked, line: loc().chartLine, area: loc().chartArea, pie: loc().chartPie, donut: loc().chartDonut, combo: loc().chartCombo, scatter: loc().chartScatter })[t];
|
|
4997
5735
|
const syncButtons = () => panel?.querySelectorAll(".fg-chart-type").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.type === type)));
|
|
4998
5736
|
if (opts.container) opts.container.append(body);
|
|
4999
5737
|
else {
|
|
@@ -5217,7 +5955,7 @@ function buildPivot(source, config, defOf, labels, locale) {
|
|
|
5217
5955
|
}
|
|
5218
5956
|
|
|
5219
5957
|
// src/version.ts
|
|
5220
|
-
var version = "0.
|
|
5958
|
+
var version = "1.0.1";
|
|
5221
5959
|
|
|
5222
5960
|
// src/license.ts
|
|
5223
5961
|
var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
|
|
@@ -5243,6 +5981,12 @@ function domainMatches(host, domains) {
|
|
|
5243
5981
|
return wildcard && h.endsWith(`.${dom}`);
|
|
5244
5982
|
});
|
|
5245
5983
|
}
|
|
5984
|
+
var isDay = (s) => {
|
|
5985
|
+
const m = typeof s === "string" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(s) : null;
|
|
5986
|
+
if (!m) return false;
|
|
5987
|
+
const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
|
|
5988
|
+
return d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
|
|
5989
|
+
};
|
|
5246
5990
|
var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
5247
5991
|
async function verifyLicense(key, host, opts = {}) {
|
|
5248
5992
|
const parts = typeof key === "string" ? key.trim().split(".") : [];
|
|
@@ -5250,7 +5994,7 @@ async function verifyLicense(key, host, opts = {}) {
|
|
|
5250
5994
|
let info;
|
|
5251
5995
|
try {
|
|
5252
5996
|
info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
|
|
5253
|
-
if (!Array.isArray(info.domains) ||
|
|
5997
|
+
if (!Array.isArray(info.domains) || !isDay(info.expires)) return { ok: false, reason: "format" };
|
|
5254
5998
|
} catch {
|
|
5255
5999
|
return { ok: false, reason: "format" };
|
|
5256
6000
|
}
|
|
@@ -5475,18 +6219,23 @@ var ColumnMenu = class {
|
|
|
5475
6219
|
t.textContent = loc.filter;
|
|
5476
6220
|
return t;
|
|
5477
6221
|
}
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
const
|
|
5481
|
-
const box = el("div", "fg-menu-section fg-filter");
|
|
6222
|
+
/** One condition: operator, value and (for between) a second value. */
|
|
6223
|
+
conditionRow(type, loc, cur, optional, apply) {
|
|
6224
|
+
const row = el("div", "fg-filter-cond");
|
|
5482
6225
|
const select = el("select", "fg-filter-op", { "aria-label": loc.filter });
|
|
6226
|
+
if (optional) {
|
|
6227
|
+
const none = el("option");
|
|
6228
|
+
none.value = "";
|
|
6229
|
+
none.textContent = "\u2014";
|
|
6230
|
+
select.append(none);
|
|
6231
|
+
}
|
|
5483
6232
|
for (const op of OPS[type]) {
|
|
5484
6233
|
const o = el("option");
|
|
5485
6234
|
o.value = op;
|
|
5486
6235
|
o.textContent = loc.ops[op];
|
|
5487
6236
|
select.append(o);
|
|
5488
6237
|
}
|
|
5489
|
-
select.value = cur?.op ?? OPS[type][0];
|
|
6238
|
+
select.value = cur?.op ?? (optional ? "" : OPS[type][0]);
|
|
5490
6239
|
const inputType = type === "text" ? "text" : type;
|
|
5491
6240
|
const v1 = el("input", "fg-input fg-filter-value", { type: inputType, placeholder: loc.value, "aria-label": loc.value });
|
|
5492
6241
|
const v2 = el("input", "fg-input fg-filter-value-to", { type: inputType, placeholder: loc.valueTo, "aria-label": loc.valueTo });
|
|
@@ -5495,27 +6244,55 @@ var ColumnMenu = class {
|
|
|
5495
6244
|
v2.value = "valueTo" in cur && cur.valueTo != null ? String(cur.valueTo) : "";
|
|
5496
6245
|
}
|
|
5497
6246
|
const sync = () => {
|
|
5498
|
-
v1.hidden = NO_VALUE.includes(select.value);
|
|
6247
|
+
v1.hidden = !select.value || NO_VALUE.includes(select.value);
|
|
5499
6248
|
v2.hidden = select.value !== "between";
|
|
5500
6249
|
};
|
|
5501
6250
|
select.addEventListener("change", sync);
|
|
5502
6251
|
sync();
|
|
6252
|
+
for (const input of [v1, v2]) input.addEventListener("keydown", (e) => e.key === "Enter" && apply());
|
|
6253
|
+
row.append(select, v1, v2);
|
|
6254
|
+
const read = () => {
|
|
6255
|
+
const op = select.value;
|
|
6256
|
+
if (!op) return null;
|
|
6257
|
+
const needsValue = !NO_VALUE.includes(op);
|
|
6258
|
+
if (type === "number") {
|
|
6259
|
+
const a = v1.value === "" ? void 0 : Number(v1.value);
|
|
6260
|
+
const b = v2.value === "" ? void 0 : Number(v2.value);
|
|
6261
|
+
return !needsValue || a !== void 0 ? { type, op, value: a, valueTo: b } : null;
|
|
6262
|
+
}
|
|
6263
|
+
if (type === "date") {
|
|
6264
|
+
return !needsValue || v1.value ? { type, op, value: v1.value || void 0, valueTo: v2.value || void 0 } : null;
|
|
6265
|
+
}
|
|
6266
|
+
return !needsValue || v1.value ? { type, op, value: v1.value } : null;
|
|
6267
|
+
};
|
|
6268
|
+
return { row, read };
|
|
6269
|
+
}
|
|
6270
|
+
conditionFilter(colId, type, loc, done) {
|
|
6271
|
+
const raw = this.grid.store.state.filters[colId];
|
|
6272
|
+
const parts = raw?.type === "combined" ? raw.conditions : raw ? [raw] : [];
|
|
6273
|
+
const first = parts[0]?.type === type ? parts[0] : void 0;
|
|
6274
|
+
const second = parts[1]?.type === type ? parts[1] : void 0;
|
|
6275
|
+
const box = el("div", "fg-menu-section fg-filter");
|
|
5503
6276
|
const apply = () => {
|
|
5504
|
-
const
|
|
5505
|
-
const
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
const a = v1.value === "" ? void 0 : Number(v1.value);
|
|
5509
|
-
const b = v2.value === "" ? void 0 : Number(v2.value);
|
|
5510
|
-
if (!needsValue || a !== void 0) model = { type, op, value: a, valueTo: b };
|
|
5511
|
-
} else if (type === "date") {
|
|
5512
|
-
if (!needsValue || v1.value) model = { type, op, value: v1.value || void 0, valueTo: v2.value || void 0 };
|
|
5513
|
-
} else if (!needsValue || v1.value) model = { type, op, value: v1.value };
|
|
6277
|
+
const a = c1.read();
|
|
6278
|
+
const b = c2.read();
|
|
6279
|
+
const operator = join.querySelector("input:checked")?.value === "or" ? "or" : "and";
|
|
6280
|
+
const model = a && b ? { type: "combined", operator, conditions: [a, b] } : a ?? b;
|
|
5514
6281
|
this.grid.setFilter(colId, model);
|
|
5515
6282
|
done();
|
|
5516
6283
|
};
|
|
5517
|
-
|
|
5518
|
-
|
|
6284
|
+
const c1 = this.conditionRow(type, loc, first, false, apply);
|
|
6285
|
+
const c2 = this.conditionRow(type, loc, second, true, apply);
|
|
6286
|
+
const join = el("div", "fg-filter-join", { role: "radiogroup" });
|
|
6287
|
+
const name = `fg-join-${colId}-${Math.random().toString(36).slice(2, 7)}`;
|
|
6288
|
+
for (const op of ["and", "or"]) {
|
|
6289
|
+
const label = el("label");
|
|
6290
|
+
const r = el("input", "", { type: "radio", name, value: op });
|
|
6291
|
+
r.checked = (raw?.type === "combined" ? raw.operator : "and") === op;
|
|
6292
|
+
label.append(r, ` ${loc[op]}`);
|
|
6293
|
+
join.append(label);
|
|
6294
|
+
}
|
|
6295
|
+
box.append(this.title(loc), c1.row, join, c2.row, this.actions(colId, loc, apply, done));
|
|
5519
6296
|
return box;
|
|
5520
6297
|
}
|
|
5521
6298
|
setFilter(colId, loc, done) {
|
|
@@ -5619,7 +6396,7 @@ var RowModel = class {
|
|
|
5619
6396
|
const d = input.display;
|
|
5620
6397
|
const treeChanged = !!d?.getParentId !== !!this.last.display?.getParentId;
|
|
5621
6398
|
const dataChanged = input.data !== this.last.data;
|
|
5622
|
-
const filterChanged = dataChanged || treeChanged || input.filters !== this.last.filters || input.quickFilter !== this.last.quickFilter;
|
|
6399
|
+
const filterChanged = dataChanged || treeChanged || input.filters !== this.last.filters || input.quickFilter !== this.last.quickFilter || input.advancedFilter !== this.last.advancedFilter;
|
|
5623
6400
|
const sortChanged = filterChanged || input.sort !== this.last.sort || collator !== this.last.collator;
|
|
5624
6401
|
const displayChanged = sortChanged || d?.version !== this.last.displayVersion || d?.groupBy !== this.last.display?.groupBy;
|
|
5625
6402
|
if (dataChanged) {
|
|
@@ -5685,7 +6462,7 @@ var RowModel = class {
|
|
|
5685
6462
|
* Prefix-sum offsets of the display rows: detail rows use `detailHeight`, rows with a
|
|
5686
6463
|
* measured height (auto row height) use it, everything else `rowHeight`.
|
|
5687
6464
|
*/
|
|
5688
|
-
layout(rowHeight, detailHeight, heights, heightsVersion = 0) {
|
|
6465
|
+
layout(rowHeight, detailHeight, heights, heightsVersion = 0, heightOf) {
|
|
5689
6466
|
const c = this.layoutCache;
|
|
5690
6467
|
if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight && c.hv === heightsVersion) return c;
|
|
5691
6468
|
const n = this.display.length;
|
|
@@ -5696,7 +6473,11 @@ var RowModel = class {
|
|
|
5696
6473
|
const node = this.display[i];
|
|
5697
6474
|
let h = rowHeight;
|
|
5698
6475
|
if (node.kind === "detail") h = detailHeight;
|
|
5699
|
-
else if (
|
|
6476
|
+
else if (!node.kind) {
|
|
6477
|
+
if (measured) h = measured.get(node.id) ?? rowHeight;
|
|
6478
|
+
const fixed = heightOf?.(node.data);
|
|
6479
|
+
if (typeof fixed === "number" && fixed > 0) h = measured ? Math.max(h, fixed) : fixed;
|
|
6480
|
+
}
|
|
5700
6481
|
if (h !== rowHeight) uniform = false;
|
|
5701
6482
|
offsets[i + 1] = offsets[i] + h;
|
|
5702
6483
|
}
|
|
@@ -5707,9 +6488,11 @@ var RowModel = class {
|
|
|
5707
6488
|
const preds = Object.entries(input.filters).map(([colId, model]) => ({ def: this.deps.columnDef(colId), pred: buildPredicate(model) })).filter((p) => !!p.def);
|
|
5708
6489
|
const tokens = quickTokens(input.quickFilter);
|
|
5709
6490
|
const quickCols = tokens.length ? this.deps.quickColumns() : [];
|
|
5710
|
-
|
|
6491
|
+
const advanced = input.advancedFilter ? buildAdvanced(input.advancedFilter, (id) => this.deps.columnDef(id), getCellValue) : null;
|
|
6492
|
+
if (!preds.length && !tokens.length && !advanced) return this.nodes;
|
|
5711
6493
|
const matches = this.nodes.filter((n) => {
|
|
5712
6494
|
for (const p of preds) if (!p.pred(getCellValue(p.def, n.data))) return false;
|
|
6495
|
+
if (advanced && !advanced(n.data)) return false;
|
|
5713
6496
|
if (tokens.length) {
|
|
5714
6497
|
const text2 = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
|
|
5715
6498
|
if (!matchesQuick(tokens, text2)) return false;
|
|
@@ -5819,8 +6602,200 @@ var RowModel = class {
|
|
|
5819
6602
|
}
|
|
5820
6603
|
};
|
|
5821
6604
|
|
|
5822
|
-
// src/rows/
|
|
6605
|
+
// src/rows/serverGroups.ts
|
|
6606
|
+
var LEVEL_ROWS = 1e4;
|
|
6607
|
+
var MAX_LEVEL_ROWS = 1e5;
|
|
6608
|
+
var MAX_IN_FLIGHT = 4;
|
|
5823
6609
|
var RETRY_MS = 3e3;
|
|
6610
|
+
var ServerGroups = class {
|
|
6611
|
+
constructor(datasource, getRowId, groupBy, query, onChange, onFailed) {
|
|
6612
|
+
this.datasource = datasource;
|
|
6613
|
+
this.getRowId = getRowId;
|
|
6614
|
+
this.groupBy = groupBy;
|
|
6615
|
+
this.query = query;
|
|
6616
|
+
this.onChange = onChange;
|
|
6617
|
+
this.onFailed = onFailed;
|
|
6618
|
+
this.levels = /* @__PURE__ */ new Map();
|
|
6619
|
+
this.gen = 0;
|
|
6620
|
+
this.display = null;
|
|
6621
|
+
this.ids = null;
|
|
6622
|
+
this.queue = [];
|
|
6623
|
+
this.inFlight = 0;
|
|
6624
|
+
/** Earliest time a failed level in the display may be asked again (0: none). */
|
|
6625
|
+
this.retryAt = 0;
|
|
6626
|
+
this.version = 0;
|
|
6627
|
+
this.builtFor = -1;
|
|
6628
|
+
}
|
|
6629
|
+
matches(groupBy, q) {
|
|
6630
|
+
return groupBy.join("") === this.groupBy.join("") && q.sort === this.query.sort && q.filters === this.query.filters && q.quickFilter === this.query.quickFilter && q.advancedFilter === this.query.advancedFilter;
|
|
6631
|
+
}
|
|
6632
|
+
purge() {
|
|
6633
|
+
this.gen++;
|
|
6634
|
+
this.queue = [];
|
|
6635
|
+
this.levels.clear();
|
|
6636
|
+
this.touch();
|
|
6637
|
+
}
|
|
6638
|
+
/** Replaced by another grouping: answers still on their way are dropped, no more change calls. */
|
|
6639
|
+
dispose() {
|
|
6640
|
+
this.gen++;
|
|
6641
|
+
this.queue = [];
|
|
6642
|
+
}
|
|
6643
|
+
touch() {
|
|
6644
|
+
this.version++;
|
|
6645
|
+
this.display = null;
|
|
6646
|
+
this.ids = null;
|
|
6647
|
+
}
|
|
6648
|
+
load(path, keys, level) {
|
|
6649
|
+
if (this.levels.has(path)) return;
|
|
6650
|
+
this.levels.set(path, { state: "loading", nodes: [] });
|
|
6651
|
+
this.fetch({ path, keys, level, start: 0, gen: this.gen });
|
|
6652
|
+
}
|
|
6653
|
+
fetch(job) {
|
|
6654
|
+
this.queue.push(job);
|
|
6655
|
+
this.pump();
|
|
6656
|
+
}
|
|
6657
|
+
pump() {
|
|
6658
|
+
while (this.inFlight < MAX_IN_FLIGHT && this.queue.length) {
|
|
6659
|
+
const job = this.queue.shift();
|
|
6660
|
+
if (job.gen !== this.gen) continue;
|
|
6661
|
+
this.inFlight++;
|
|
6662
|
+
void this.request(job).finally(() => {
|
|
6663
|
+
this.inFlight--;
|
|
6664
|
+
this.pump();
|
|
6665
|
+
});
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
request({ path, keys, level, start, gen }) {
|
|
6669
|
+
const last = level >= this.groupBy.length;
|
|
6670
|
+
const q = this.query;
|
|
6671
|
+
const end = start + LEVEL_ROWS;
|
|
6672
|
+
let result;
|
|
6673
|
+
try {
|
|
6674
|
+
result = Promise.resolve(this.datasource.getRows({
|
|
6675
|
+
start,
|
|
6676
|
+
end,
|
|
6677
|
+
sort: [...q.sort],
|
|
6678
|
+
filters: { ...q.filters },
|
|
6679
|
+
quickFilter: q.quickFilter,
|
|
6680
|
+
advancedFilter: q.advancedFilter,
|
|
6681
|
+
groupBy: [...this.groupBy],
|
|
6682
|
+
groupKeys: [...keys]
|
|
6683
|
+
}));
|
|
6684
|
+
} catch (e) {
|
|
6685
|
+
result = Promise.reject(e);
|
|
6686
|
+
}
|
|
6687
|
+
return result.then((res) => {
|
|
6688
|
+
if (gen !== this.gen) return;
|
|
6689
|
+
const rows = Array.isArray(res?.rows) ? res.rows.filter((r) => r !== null && typeof r === "object") : [];
|
|
6690
|
+
if (!Array.isArray(res?.rows)) warn("datasource.getRows must resolve to { rows: Row[] }");
|
|
6691
|
+
const added = last ? rows.map((data, i) => ({ id: this.getRowId ? String(this.getRowId(data)) : autoId(data), data, sourceIndex: start + i, level })) : rows.map((data) => {
|
|
6692
|
+
const colId = this.groupBy[level];
|
|
6693
|
+
const value = data[colId];
|
|
6694
|
+
const key = `${path ? `${path}|` : ""}${colId}:${setKey(value)}`;
|
|
6695
|
+
const count = typeof data.count === "number" ? data.count : typeof data.childCount === "number" ? data.childCount : void 0;
|
|
6696
|
+
return {
|
|
6697
|
+
id: `group:${key}`,
|
|
6698
|
+
data: {},
|
|
6699
|
+
sourceIndex: -1,
|
|
6700
|
+
kind: "group",
|
|
6701
|
+
level,
|
|
6702
|
+
group: { key, colId, value, rows: [], expanded: false, count, aggregates: data, keys: [...keys, value] }
|
|
6703
|
+
};
|
|
6704
|
+
});
|
|
6705
|
+
const nodes = start ? [...this.levels.get(path)?.nodes ?? [], ...added] : added;
|
|
6706
|
+
const total = typeof res?.total === "number" && res.total >= 0 ? res.total : null;
|
|
6707
|
+
const more = rows.length > 0 && (total !== null ? nodes.length < total : rows.length >= end - start);
|
|
6708
|
+
if (more && nodes.length < MAX_LEVEL_ROWS) {
|
|
6709
|
+
this.levels.set(path, { state: "loading", nodes });
|
|
6710
|
+
this.fetch({ path, keys, level, start: nodes.length, gen });
|
|
6711
|
+
} else {
|
|
6712
|
+
if (more) warn(`a server group level has more than ${MAX_LEVEL_ROWS} rows; only the first ${MAX_LEVEL_ROWS} are shown`);
|
|
6713
|
+
this.levels.set(path, { state: "loaded", nodes });
|
|
6714
|
+
}
|
|
6715
|
+
this.touch();
|
|
6716
|
+
this.onChange();
|
|
6717
|
+
}, (error) => {
|
|
6718
|
+
if (gen !== this.gen) return;
|
|
6719
|
+
this.levels.set(path, { state: "failed", nodes: [], retryAt: Date.now() + RETRY_MS });
|
|
6720
|
+
this.touch();
|
|
6721
|
+
this.onFailed(start, end, error);
|
|
6722
|
+
});
|
|
6723
|
+
}
|
|
6724
|
+
isOpen(d, key, level) {
|
|
6725
|
+
const explicit = d.expanded.get(key);
|
|
6726
|
+
if (explicit !== void 0) return explicit;
|
|
6727
|
+
return d.defaultExpanded === -1 || level < d.defaultExpanded;
|
|
6728
|
+
}
|
|
6729
|
+
/** Display rows: open groups show their loaded children, or a loading row while they arrive. */
|
|
6730
|
+
rows(d, displayVersion) {
|
|
6731
|
+
const now = Date.now();
|
|
6732
|
+
if (this.display && this.builtFor === displayVersion && !(this.retryAt && now >= this.retryAt)) return this.display;
|
|
6733
|
+
const out = [];
|
|
6734
|
+
let retryAt = 0;
|
|
6735
|
+
const walk = (path, keys, level) => {
|
|
6736
|
+
let lv = this.levels.get(path);
|
|
6737
|
+
if (lv?.state === "failed" && now >= (lv.retryAt ?? 0)) {
|
|
6738
|
+
this.levels.delete(path);
|
|
6739
|
+
lv = void 0;
|
|
6740
|
+
}
|
|
6741
|
+
if (!lv) {
|
|
6742
|
+
this.load(path, keys, level);
|
|
6743
|
+
out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
|
|
6744
|
+
return;
|
|
6745
|
+
}
|
|
6746
|
+
if (lv.state === "failed") {
|
|
6747
|
+
const at = lv.retryAt ?? 0;
|
|
6748
|
+
retryAt = retryAt ? Math.min(retryAt, at) : at;
|
|
6749
|
+
return;
|
|
6750
|
+
}
|
|
6751
|
+
for (const n of lv.nodes) {
|
|
6752
|
+
if (n.kind !== "group") {
|
|
6753
|
+
out.push(n);
|
|
6754
|
+
continue;
|
|
6755
|
+
}
|
|
6756
|
+
const g = n.group;
|
|
6757
|
+
const open = this.isOpen(d, g.key, level);
|
|
6758
|
+
const node = { ...n, group: { ...g, expanded: open } };
|
|
6759
|
+
out.push(node);
|
|
6760
|
+
if (open) walk(g.key, g.keys ?? [], level + 1);
|
|
6761
|
+
}
|
|
6762
|
+
if (lv.state === "loading") out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
|
|
6763
|
+
};
|
|
6764
|
+
walk("", [], 0);
|
|
6765
|
+
this.retryAt = retryAt;
|
|
6766
|
+
this.display = out;
|
|
6767
|
+
this.builtFor = displayVersion;
|
|
6768
|
+
this.ids = null;
|
|
6769
|
+
return out;
|
|
6770
|
+
}
|
|
6771
|
+
dataRows() {
|
|
6772
|
+
const out = [];
|
|
6773
|
+
for (const lv of this.levels.values()) for (const n of lv.nodes) if (!n.kind) out.push(n);
|
|
6774
|
+
return out;
|
|
6775
|
+
}
|
|
6776
|
+
/** Rows matching the filters, from the counts of the top level when the server sent them. */
|
|
6777
|
+
dataCount() {
|
|
6778
|
+
const top = this.levels.get("");
|
|
6779
|
+
if (top?.state === "loaded" && top.nodes.every((n) => typeof n.group?.count === "number")) {
|
|
6780
|
+
return top.nodes.reduce((s, n) => s + (n.group?.count ?? 0), 0);
|
|
6781
|
+
}
|
|
6782
|
+
return this.dataRows().length;
|
|
6783
|
+
}
|
|
6784
|
+
loading() {
|
|
6785
|
+
for (const lv of this.levels.values()) if (lv.state === "loading") return true;
|
|
6786
|
+
return false;
|
|
6787
|
+
}
|
|
6788
|
+
indexOf(id) {
|
|
6789
|
+
if (!this.display) return -1;
|
|
6790
|
+
if (!this.ids) {
|
|
6791
|
+
this.ids = /* @__PURE__ */ new Map();
|
|
6792
|
+
this.display.forEach((n, i) => this.ids.set(n.id, i));
|
|
6793
|
+
}
|
|
6794
|
+
return this.ids.get(id) ?? -1;
|
|
6795
|
+
}
|
|
6796
|
+
};
|
|
6797
|
+
|
|
6798
|
+
// src/rows/serverRowModel.ts
|
|
5824
6799
|
var NO_OFFSETS = new Float64Array(0);
|
|
5825
6800
|
var ServerRowModel = class {
|
|
5826
6801
|
constructor(deps) {
|
|
@@ -5839,6 +6814,9 @@ var ServerRowModel = class {
|
|
|
5839
6814
|
this.lastBlock = 0;
|
|
5840
6815
|
this.version = 0;
|
|
5841
6816
|
this.cache = null;
|
|
6817
|
+
/** Server-side grouping, while groupBy has columns. */
|
|
6818
|
+
this.groups = null;
|
|
6819
|
+
this.displayIn = null;
|
|
5842
6820
|
}
|
|
5843
6821
|
get bs() {
|
|
5844
6822
|
return this.page ? this.page.size : this.deps.blockSize;
|
|
@@ -5847,11 +6825,36 @@ var ServerRowModel = class {
|
|
|
5847
6825
|
return this.page ? this.page.index * this.page.size : 0;
|
|
5848
6826
|
}
|
|
5849
6827
|
update(input) {
|
|
6828
|
+
const groupBy = input.display?.groupBy ?? [];
|
|
6829
|
+
if (groupBy.length && !input.display?.getParentId) {
|
|
6830
|
+
const q2 = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter, advancedFilter: input.advancedFilter ?? null };
|
|
6831
|
+
if (!this.groups || !this.groups.matches(groupBy, q2)) {
|
|
6832
|
+
this.groups?.dispose();
|
|
6833
|
+
this.groups = new ServerGroups(
|
|
6834
|
+
this.deps.datasource,
|
|
6835
|
+
this.deps.getRowId,
|
|
6836
|
+
[...groupBy],
|
|
6837
|
+
q2,
|
|
6838
|
+
() => this.deps.onLoaded(0, 0, null),
|
|
6839
|
+
(start, end, error) => this.deps.onFailed(start, end, error)
|
|
6840
|
+
);
|
|
6841
|
+
this.touch();
|
|
6842
|
+
}
|
|
6843
|
+
this.displayIn = input.display ?? null;
|
|
6844
|
+
this.query = null;
|
|
6845
|
+
return;
|
|
6846
|
+
}
|
|
6847
|
+
if (this.groups) {
|
|
6848
|
+
this.groups.dispose();
|
|
6849
|
+
this.groups = null;
|
|
6850
|
+
this.query = null;
|
|
6851
|
+
this.touch();
|
|
6852
|
+
}
|
|
5850
6853
|
const q = this.query;
|
|
5851
|
-
const queryChanged = !q || q.sort !== input.sort || q.filters !== input.filters || q.quickFilter !== input.quickFilter;
|
|
6854
|
+
const queryChanged = !q || q.sort !== input.sort || q.filters !== input.filters || q.quickFilter !== input.quickFilter || q.advancedFilter !== (input.advancedFilter ?? null);
|
|
5852
6855
|
const p = input.page ?? null;
|
|
5853
6856
|
const sizeChanged = (p?.size ?? 0) !== (this.page?.size ?? 0);
|
|
5854
|
-
this.query = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter };
|
|
6857
|
+
this.query = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter, advancedFilter: input.advancedFilter ?? null };
|
|
5855
6858
|
this.page = p ? { ...p } : null;
|
|
5856
6859
|
if (queryChanged || sizeChanged) this.purge();
|
|
5857
6860
|
this.ensure(Math.floor(this.base / this.bs));
|
|
@@ -5860,6 +6863,10 @@ var ServerRowModel = class {
|
|
|
5860
6863
|
}
|
|
5861
6864
|
/** Forget every row and start over (sort/filter change or refreshRows). */
|
|
5862
6865
|
purge() {
|
|
6866
|
+
if (this.groups) {
|
|
6867
|
+
this.groups.purge();
|
|
6868
|
+
return;
|
|
6869
|
+
}
|
|
5863
6870
|
this.gen++;
|
|
5864
6871
|
this.blocks.clear();
|
|
5865
6872
|
this.total = null;
|
|
@@ -5879,13 +6886,16 @@ var ServerRowModel = class {
|
|
|
5879
6886
|
return this.loadedEnd + (last && last.nodes.length < this.bs ? 0 : this.bs);
|
|
5880
6887
|
}
|
|
5881
6888
|
getRowCount() {
|
|
6889
|
+
if (this.groups) return this.groupRows().length;
|
|
5882
6890
|
const all2 = this.fullCount();
|
|
5883
6891
|
return this.page ? Math.max(0, Math.min(this.page.size, all2 - this.base)) : all2;
|
|
5884
6892
|
}
|
|
5885
6893
|
dataCount() {
|
|
6894
|
+
if (this.groups) return this.groups.dataCount();
|
|
5886
6895
|
return this.fullCount();
|
|
5887
6896
|
}
|
|
5888
6897
|
loading() {
|
|
6898
|
+
if (this.groups) return this.groups.loading();
|
|
5889
6899
|
for (const b of this.blocks.values()) if (b.state === "loading") return true;
|
|
5890
6900
|
return false;
|
|
5891
6901
|
}
|
|
@@ -5897,7 +6907,11 @@ var ServerRowModel = class {
|
|
|
5897
6907
|
const b = this.blocks.get(k);
|
|
5898
6908
|
return b?.state === "loaded" ? b.nodes[abs - k * this.bs] ?? null : null;
|
|
5899
6909
|
}
|
|
6910
|
+
groupRows() {
|
|
6911
|
+
return this.groups.rows(this.displayIn, this.displayIn.version * 1e6 + this.groups.version);
|
|
6912
|
+
}
|
|
5900
6913
|
getRow(index) {
|
|
6914
|
+
if (this.groups) return this.groupRows()[index];
|
|
5901
6915
|
const abs = this.base + index;
|
|
5902
6916
|
const hit = this.peek(abs);
|
|
5903
6917
|
if (hit) return hit;
|
|
@@ -5920,7 +6934,7 @@ var ServerRowModel = class {
|
|
|
5920
6934
|
};
|
|
5921
6935
|
let result;
|
|
5922
6936
|
try {
|
|
5923
|
-
result = Promise.resolve(this.deps.datasource.getRows({ start, end, sort: [...q.sort], filters: { ...q.filters }, quickFilter: q.quickFilter }));
|
|
6937
|
+
result = Promise.resolve(this.deps.datasource.getRows({ start, end, sort: [...q.sort], filters: { ...q.filters }, quickFilter: q.quickFilter, advancedFilter: q.advancedFilter }));
|
|
5924
6938
|
} catch (e) {
|
|
5925
6939
|
result = Promise.reject(e);
|
|
5926
6940
|
}
|
|
@@ -5967,6 +6981,7 @@ var ServerRowModel = class {
|
|
|
5967
6981
|
}
|
|
5968
6982
|
/** Every row of the window, with placeholders for those not loaded (does not request anything). */
|
|
5969
6983
|
rows() {
|
|
6984
|
+
if (this.groups) return this.groupRows();
|
|
5970
6985
|
const c = this.built();
|
|
5971
6986
|
if (!c.rows) {
|
|
5972
6987
|
const n = this.getRowCount();
|
|
@@ -5976,15 +6991,21 @@ var ServerRowModel = class {
|
|
|
5976
6991
|
return c.rows;
|
|
5977
6992
|
}
|
|
5978
6993
|
dataRows() {
|
|
6994
|
+
if (this.groups) return this.groups.dataRows();
|
|
5979
6995
|
return this.built().data;
|
|
5980
6996
|
}
|
|
5981
6997
|
allNodes() {
|
|
6998
|
+
if (this.groups) return this.groups.dataRows();
|
|
5982
6999
|
return this.built().data;
|
|
5983
7000
|
}
|
|
5984
7001
|
idOf(row) {
|
|
5985
7002
|
return this.deps.getRowId ? String(this.deps.getRowId(row)) : autoId(row);
|
|
5986
7003
|
}
|
|
5987
7004
|
indexOfId(id) {
|
|
7005
|
+
if (this.groups) {
|
|
7006
|
+
this.groupRows();
|
|
7007
|
+
return this.groups.indexOf(id);
|
|
7008
|
+
}
|
|
5988
7009
|
const abs = this.built().ids.get(id);
|
|
5989
7010
|
if (abs === void 0) return -1;
|
|
5990
7011
|
const i = abs - this.base;
|
|
@@ -6035,10 +7056,280 @@ var Store = class {
|
|
|
6035
7056
|
}
|
|
6036
7057
|
};
|
|
6037
7058
|
|
|
7059
|
+
// src/interactions/tooltip.ts
|
|
7060
|
+
var DELAY = 500;
|
|
7061
|
+
function attachTooltips(grid) {
|
|
7062
|
+
const root = grid.renderer.root;
|
|
7063
|
+
let timer = 0;
|
|
7064
|
+
let tipEl = null;
|
|
7065
|
+
let target = null;
|
|
7066
|
+
const hide = () => {
|
|
7067
|
+
clearTimeout(timer);
|
|
7068
|
+
tipEl?.remove();
|
|
7069
|
+
tipEl = null;
|
|
7070
|
+
target = null;
|
|
7071
|
+
};
|
|
7072
|
+
const textFor = (cell) => {
|
|
7073
|
+
const colId = cell.dataset.col;
|
|
7074
|
+
if (!colId || isSystemCol(colId)) return null;
|
|
7075
|
+
const node = grid.columns.getNode(colId);
|
|
7076
|
+
if (!node) return null;
|
|
7077
|
+
if (cell.classList.contains("fg-header-cell")) return node.def.headerTooltip ?? null;
|
|
7078
|
+
const rowEl = cell.closest(".fg-row[data-row]");
|
|
7079
|
+
if (!rowEl || !cell.closest(".fg-body")) return null;
|
|
7080
|
+
const rowIndex = Number(rowEl.dataset.row);
|
|
7081
|
+
const row = grid.rowModel.getRow(rowIndex);
|
|
7082
|
+
if (!row || row.kind) return null;
|
|
7083
|
+
const def = node.def;
|
|
7084
|
+
const value = getCellValue(def, row.data);
|
|
7085
|
+
const formatted = getFormatter(def, grid.store.state.locale.code)(value);
|
|
7086
|
+
if (typeof def.tooltip === "function") {
|
|
7087
|
+
try {
|
|
7088
|
+
const t = def.tooltip({ value, formatted, row: row.data, rowIndex, column: def });
|
|
7089
|
+
return t ? String(t) : null;
|
|
7090
|
+
} catch {
|
|
7091
|
+
warn(`tooltip of column "${colId}" threw`);
|
|
7092
|
+
return null;
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
if (def.tooltip === true) return formatted || null;
|
|
7096
|
+
if (grid.options.overflowTooltip !== false && def.tooltip !== false && cell.scrollWidth > cell.clientWidth + 1) return cell.textContent || null;
|
|
7097
|
+
return null;
|
|
7098
|
+
};
|
|
7099
|
+
const show = (cell, text2) => {
|
|
7100
|
+
tipEl?.remove();
|
|
7101
|
+
tipEl = el("div", `fg-tooltip fg-theme-${grid.store.state.theme}`, { role: "tooltip" });
|
|
7102
|
+
tipEl.textContent = text2;
|
|
7103
|
+
grid.applyFontTo(tipEl);
|
|
7104
|
+
popupParent(root).append(tipEl);
|
|
7105
|
+
const box = cell.getBoundingClientRect();
|
|
7106
|
+
const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
7107
|
+
const vw = window.innerWidth || 1024, vh = window.innerHeight || 768;
|
|
7108
|
+
const top = box.bottom + 6 + h > vh ? box.top - h - 6 : box.bottom + 6;
|
|
7109
|
+
tipEl.style.left = `${Math.max(4, Math.min(vw - w - 4, box.left))}px`;
|
|
7110
|
+
tipEl.style.top = `${Math.max(4, top)}px`;
|
|
7111
|
+
};
|
|
7112
|
+
const onOver = (e) => {
|
|
7113
|
+
const cell = e.target.closest?.(".fg-cell, .fg-header-cell");
|
|
7114
|
+
if (!cell || cell === target || !root.contains(cell)) return;
|
|
7115
|
+
hide();
|
|
7116
|
+
target = cell;
|
|
7117
|
+
timer = window.setTimeout(() => {
|
|
7118
|
+
if (target !== cell || !cell.isConnected) return;
|
|
7119
|
+
const text2 = textFor(cell);
|
|
7120
|
+
if (text2) show(cell, text2);
|
|
7121
|
+
}, DELAY);
|
|
7122
|
+
};
|
|
7123
|
+
const onOut = (e) => {
|
|
7124
|
+
const to = e.relatedTarget;
|
|
7125
|
+
if (target && to && target.contains(to)) return;
|
|
7126
|
+
hide();
|
|
7127
|
+
};
|
|
7128
|
+
root.addEventListener("pointerover", onOver);
|
|
7129
|
+
root.addEventListener("pointerout", onOut);
|
|
7130
|
+
root.addEventListener("pointerdown", hide);
|
|
7131
|
+
root.addEventListener("keydown", hide);
|
|
7132
|
+
root.addEventListener("wheel", hide, { passive: true });
|
|
7133
|
+
return () => {
|
|
7134
|
+
hide();
|
|
7135
|
+
root.removeEventListener("pointerover", onOver);
|
|
7136
|
+
root.removeEventListener("pointerout", onOut);
|
|
7137
|
+
root.removeEventListener("pointerdown", hide);
|
|
7138
|
+
root.removeEventListener("keydown", hide);
|
|
7139
|
+
root.removeEventListener("wheel", hide);
|
|
7140
|
+
};
|
|
7141
|
+
}
|
|
7142
|
+
|
|
7143
|
+
// src/interactions/filterBuilder.ts
|
|
7144
|
+
var NO_VALUE2 = ["blank", "notBlank"];
|
|
7145
|
+
var FilterBuilder = class {
|
|
7146
|
+
constructor(grid) {
|
|
7147
|
+
this.grid = grid;
|
|
7148
|
+
this.popup = null;
|
|
7149
|
+
this.off = [];
|
|
7150
|
+
}
|
|
7151
|
+
get isOpen() {
|
|
7152
|
+
return !!this.popup;
|
|
7153
|
+
}
|
|
7154
|
+
/** Columns that can be filtered, with the condition type the builder uses for them. */
|
|
7155
|
+
columns() {
|
|
7156
|
+
return this.grid.columns.getState().map((s) => this.grid.columns.getNode(s.colId)).filter((n) => n && !n.children && !isSystemCol(n.id) && filterTypeOf(n.def)).map((n) => {
|
|
7157
|
+
const ft = filterTypeOf(n.def);
|
|
7158
|
+
const kind = ft === "number" ? "number" : ft === "date" ? "date" : "text";
|
|
7159
|
+
return { id: n.id, label: n.def.header ?? n.def.field ?? n.id, kind };
|
|
7160
|
+
});
|
|
7161
|
+
}
|
|
7162
|
+
open() {
|
|
7163
|
+
this.close();
|
|
7164
|
+
const { grid } = this;
|
|
7165
|
+
const loc = grid.store.state.locale;
|
|
7166
|
+
const cols = this.columns();
|
|
7167
|
+
const popup = el("div", `fg-menu fg-filter-builder fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": loc.advancedFilter });
|
|
7168
|
+
grid.applyFontTo(popup);
|
|
7169
|
+
const head = el("div", "fg-chooser-head");
|
|
7170
|
+
const title = el("span", "fg-menu-title");
|
|
7171
|
+
title.textContent = loc.advancedFilter;
|
|
7172
|
+
const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
|
|
7173
|
+
x.textContent = "\xD7";
|
|
7174
|
+
x.addEventListener("click", () => this.close());
|
|
7175
|
+
head.append(title, x);
|
|
7176
|
+
const cur = grid.store.state.advancedFilter;
|
|
7177
|
+
const join = el("div", "fg-filter-join", { role: "radiogroup" });
|
|
7178
|
+
const name = `fg-fb-join-${Math.random().toString(36).slice(2, 7)}`;
|
|
7179
|
+
for (const op of ["and", "or"]) {
|
|
7180
|
+
const label = el("label");
|
|
7181
|
+
const r2 = el("input", "", { type: "radio", name, value: op });
|
|
7182
|
+
r2.checked = (cur?.operator ?? "and") === op;
|
|
7183
|
+
label.append(r2, ` ${loc[op]}`);
|
|
7184
|
+
join.append(label);
|
|
7185
|
+
}
|
|
7186
|
+
const list = el("div", "fg-fb-list");
|
|
7187
|
+
const rows = [];
|
|
7188
|
+
const remove = (row) => {
|
|
7189
|
+
rows.splice(rows.indexOf(row), 1);
|
|
7190
|
+
row.el.remove();
|
|
7191
|
+
};
|
|
7192
|
+
const conditionRow = (start) => {
|
|
7193
|
+
const rowEl = el("div", "fg-fb-row");
|
|
7194
|
+
const colSel = el("select", "fg-filter-op fg-fb-col", { "aria-label": loc.columns });
|
|
7195
|
+
for (const c of cols) {
|
|
7196
|
+
const o = el("option");
|
|
7197
|
+
o.value = c.id;
|
|
7198
|
+
o.textContent = c.label;
|
|
7199
|
+
colSel.append(o);
|
|
7200
|
+
}
|
|
7201
|
+
const opSel = el("select", "fg-filter-op fg-fb-op", { "aria-label": loc.filter });
|
|
7202
|
+
const value = el("input", "fg-input fg-fb-value", { type: "text", placeholder: loc.value, "aria-label": loc.value });
|
|
7203
|
+
const valueTo = el("input", "fg-input fg-fb-value-to", { type: "text", placeholder: loc.valueTo, "aria-label": loc.valueTo });
|
|
7204
|
+
const del = el("button", "fg-chooser-close fg-fb-remove", { type: "button", "aria-label": loc.close });
|
|
7205
|
+
del.textContent = "\xD7";
|
|
7206
|
+
const kindOf = () => cols.find((c) => c.id === colSel.value)?.kind ?? "text";
|
|
7207
|
+
const fillOps = (keep) => {
|
|
7208
|
+
const kind = kindOf();
|
|
7209
|
+
opSel.replaceChildren(...OPS[kind].map((op) => {
|
|
7210
|
+
const o = el("option");
|
|
7211
|
+
o.value = op;
|
|
7212
|
+
o.textContent = loc.ops[op];
|
|
7213
|
+
return o;
|
|
7214
|
+
}));
|
|
7215
|
+
if (keep && OPS[kind].includes(keep)) opSel.value = keep;
|
|
7216
|
+
const type = kind === "text" ? "text" : kind;
|
|
7217
|
+
value.type = valueTo.type = type;
|
|
7218
|
+
sync();
|
|
7219
|
+
};
|
|
7220
|
+
const sync = () => {
|
|
7221
|
+
value.hidden = NO_VALUE2.includes(opSel.value);
|
|
7222
|
+
valueTo.hidden = opSel.value !== "between";
|
|
7223
|
+
};
|
|
7224
|
+
colSel.addEventListener("change", () => fillOps(opSel.value));
|
|
7225
|
+
opSel.addEventListener("change", sync);
|
|
7226
|
+
if (start && cols.some((c) => c.id === start.colId)) colSel.value = start.colId;
|
|
7227
|
+
const m = start?.model;
|
|
7228
|
+
fillOps(m?.op);
|
|
7229
|
+
if (m?.value != null) value.value = String(m.value);
|
|
7230
|
+
if (m?.valueTo != null) valueTo.value = String(m.valueTo);
|
|
7231
|
+
rowEl.append(colSel, opSel, value, valueTo, del);
|
|
7232
|
+
const state = {
|
|
7233
|
+
el: rowEl,
|
|
7234
|
+
read: () => {
|
|
7235
|
+
const kind = kindOf();
|
|
7236
|
+
const op = opSel.value;
|
|
7237
|
+
const needs = !NO_VALUE2.includes(op);
|
|
7238
|
+
let model = null;
|
|
7239
|
+
if (kind === "number") {
|
|
7240
|
+
const a = value.value === "" ? void 0 : Number(value.value);
|
|
7241
|
+
const b = valueTo.value === "" ? void 0 : Number(valueTo.value);
|
|
7242
|
+
if (!needs || a !== void 0 && !Number.isNaN(a)) model = op === "between" ? { type: "number", op, value: a, valueTo: b } : { type: "number", op, value: a };
|
|
7243
|
+
} else if (kind === "date") {
|
|
7244
|
+
if (!needs || value.value) model = op === "between" ? { type: "date", op, value: value.value, valueTo: valueTo.value } : { type: "date", op, value: value.value || void 0 };
|
|
7245
|
+
} else if (!needs || value.value) model = { type: "text", op, value: value.value };
|
|
7246
|
+
return model ? { colId: colSel.value, model } : null;
|
|
7247
|
+
}
|
|
7248
|
+
};
|
|
7249
|
+
del.addEventListener("click", () => remove(state));
|
|
7250
|
+
return state;
|
|
7251
|
+
};
|
|
7252
|
+
const keptRow = (kept, label) => {
|
|
7253
|
+
const rowEl = el("div", "fg-fb-row fg-fb-group");
|
|
7254
|
+
const text2 = el("span", "fg-fb-group-label");
|
|
7255
|
+
text2.textContent = label;
|
|
7256
|
+
const del = el("button", "fg-chooser-close fg-fb-remove", { type: "button", "aria-label": loc.close });
|
|
7257
|
+
del.textContent = "\xD7";
|
|
7258
|
+
rowEl.append(text2, del);
|
|
7259
|
+
const state = { el: rowEl, read: () => kept };
|
|
7260
|
+
del.addEventListener("click", () => remove(state));
|
|
7261
|
+
return state;
|
|
7262
|
+
};
|
|
7263
|
+
const rowFor = (c) => {
|
|
7264
|
+
if ("conditions" in c) return keptRow(c, `( ${c.conditions.length} \xD7 ${loc[c.operator]} )`);
|
|
7265
|
+
const col = cols.find((x2) => x2.id === c.colId);
|
|
7266
|
+
const m = c.model;
|
|
7267
|
+
if (col && m && (m.type === "text" || m.type === "number" || m.type === "date")) return conditionRow(c);
|
|
7268
|
+
const what = m?.type === "set" ? m.values.map(String).join(", ") : m?.type === "combined" ? `( ${m.conditions.length} \xD7 ${loc[m.operator]} )` : "";
|
|
7269
|
+
return keptRow(c, `${col?.label ?? c.colId}: ${what}`);
|
|
7270
|
+
};
|
|
7271
|
+
const add = (row) => {
|
|
7272
|
+
rows.push(row);
|
|
7273
|
+
list.append(row.el);
|
|
7274
|
+
};
|
|
7275
|
+
for (const c of cur?.conditions ?? []) add(rowFor(c));
|
|
7276
|
+
const addBtn = el("button", "fg-btn fg-fb-add", { type: "button" });
|
|
7277
|
+
addBtn.textContent = `+ ${loc.addCondition}`;
|
|
7278
|
+
addBtn.addEventListener("click", () => {
|
|
7279
|
+
const row = conditionRow();
|
|
7280
|
+
add(row);
|
|
7281
|
+
row.el.querySelector(".fg-fb-value").focus();
|
|
7282
|
+
});
|
|
7283
|
+
const actions = el("div", "fg-menu-actions");
|
|
7284
|
+
const reset = el("button", "fg-btn", { type: "button" });
|
|
7285
|
+
reset.textContent = loc.reset;
|
|
7286
|
+
reset.addEventListener("click", () => {
|
|
7287
|
+
grid.setAdvancedFilter(null);
|
|
7288
|
+
this.close();
|
|
7289
|
+
});
|
|
7290
|
+
const ok = el("button", "fg-btn fg-btn-primary", { type: "button" });
|
|
7291
|
+
ok.textContent = loc.apply;
|
|
7292
|
+
ok.addEventListener("click", () => {
|
|
7293
|
+
const conditions = rows.map((r2) => r2.read()).filter((c) => !!c);
|
|
7294
|
+
const operator = join.querySelector("input:checked")?.value === "or" ? "or" : "and";
|
|
7295
|
+
grid.setAdvancedFilter(conditions.length ? { operator, conditions } : null);
|
|
7296
|
+
this.close();
|
|
7297
|
+
});
|
|
7298
|
+
actions.append(reset, ok);
|
|
7299
|
+
popup.append(head, join, list, addBtn, actions);
|
|
7300
|
+
popupParent(grid.renderer.root).append(popup);
|
|
7301
|
+
const r = grid.renderer.root.getBoundingClientRect();
|
|
7302
|
+
const w = popup.offsetWidth || 460;
|
|
7303
|
+
popup.style.left = `${Math.max(4, Math.min(r.left + 24, (window.innerWidth || 1024) - w - 4))}px`;
|
|
7304
|
+
popup.style.top = `${Math.max(4, r.top + 24)}px`;
|
|
7305
|
+
this.popup = popup;
|
|
7306
|
+
if (!rows.length) addBtn.click();
|
|
7307
|
+
const onDown = (e) => {
|
|
7308
|
+
if (!eventInside(e, popup)) this.close();
|
|
7309
|
+
};
|
|
7310
|
+
const onKey = (e) => {
|
|
7311
|
+
if (e.key === "Escape") this.close();
|
|
7312
|
+
};
|
|
7313
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
7314
|
+
document.addEventListener("keydown", onKey, true);
|
|
7315
|
+
this.off.push(
|
|
7316
|
+
() => document.removeEventListener("pointerdown", onDown, true),
|
|
7317
|
+
() => document.removeEventListener("keydown", onKey, true)
|
|
7318
|
+
);
|
|
7319
|
+
}
|
|
7320
|
+
close() {
|
|
7321
|
+
for (const f of this.off) f();
|
|
7322
|
+
this.off = [];
|
|
7323
|
+
this.popup?.remove();
|
|
7324
|
+
this.popup = null;
|
|
7325
|
+
}
|
|
7326
|
+
};
|
|
7327
|
+
|
|
6038
7328
|
// src/grid.ts
|
|
6039
7329
|
var autoRowHeight = (size) => Math.round(size * 2.75);
|
|
6040
7330
|
var autoHeaderHeight = (size) => Math.round(size * 3.1);
|
|
6041
7331
|
var DEFAULT_FONT_SIZE = 13;
|
|
7332
|
+
var DENSITY = { compact: [0.8, 0.85], normal: [1, 1], comfortable: [1.25, 1.15] };
|
|
6042
7333
|
var NO_GROUPS = [];
|
|
6043
7334
|
var DEFAULT_PAGE_SIZE = 100;
|
|
6044
7335
|
var NO_SPANS = /* @__PURE__ */ new Map();
|
|
@@ -6078,6 +7369,18 @@ var Grid = class {
|
|
|
6078
7369
|
/** 0-based page and rows per page (pagination only). */
|
|
6079
7370
|
this.page = 0;
|
|
6080
7371
|
this.pageSize = DEFAULT_PAGE_SIZE;
|
|
7372
|
+
// ── pagination and server rows ──────────────────────────────
|
|
7373
|
+
/** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
|
|
7374
|
+
/** showOverlay: an overlay the app asked for, shown whatever the row count. */
|
|
7375
|
+
this.forcedOverlay = null;
|
|
7376
|
+
this.filterBuilder = new FilterBuilder(this);
|
|
7377
|
+
this.flashing = /* @__PURE__ */ new Map();
|
|
7378
|
+
this.flashTimer = 0;
|
|
7379
|
+
this.txQueue = [];
|
|
7380
|
+
this.txTimer = 0;
|
|
7381
|
+
/** A transaction is replacing the data: keep the selected range. */
|
|
7382
|
+
this.keepRange = false;
|
|
7383
|
+
this.rowNumberCache = null;
|
|
6081
7384
|
/** Tree rows whose children were loaded (or are loading) through treeData.loadChildren. */
|
|
6082
7385
|
this.treeLoaded = /* @__PURE__ */ new Set();
|
|
6083
7386
|
this.treeLoading = /* @__PURE__ */ new Set();
|
|
@@ -6089,6 +7392,9 @@ var Grid = class {
|
|
|
6089
7392
|
return false;
|
|
6090
7393
|
}
|
|
6091
7394
|
};
|
|
7395
|
+
this.density = "normal";
|
|
7396
|
+
/** Bumped on every selection change (group row checkboxes depend on their rows). */
|
|
7397
|
+
this.selectionVersion = 0;
|
|
6092
7398
|
if (!Array.isArray(options.columns)) warn("options.columns must be an array");
|
|
6093
7399
|
if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
|
|
6094
7400
|
this.store = new Store({
|
|
@@ -6101,6 +7407,7 @@ var Grid = class {
|
|
|
6101
7407
|
focus: null,
|
|
6102
7408
|
range: null,
|
|
6103
7409
|
extraRanges: [],
|
|
7410
|
+
advancedFilter: null,
|
|
6104
7411
|
locale: resolveLocale(options.locale),
|
|
6105
7412
|
theme: options.theme ?? "light"
|
|
6106
7413
|
});
|
|
@@ -6137,9 +7444,11 @@ var Grid = class {
|
|
|
6137
7444
|
this.font = { size: explicit, family: options.fontFamily ?? null };
|
|
6138
7445
|
const size = explicit ?? DEFAULT_FONT_SIZE;
|
|
6139
7446
|
this.renderer = new Renderer(host, this, {
|
|
6140
|
-
rowHeight: options.rowHeight ?? autoRowHeight(size),
|
|
6141
|
-
headerHeight: options.headerHeight ?? autoHeaderHeight(size)
|
|
7447
|
+
rowHeight: options.rowHeight ?? Math.round(autoRowHeight(size) * DENSITY[this.densityOf(options.density)][0]),
|
|
7448
|
+
headerHeight: options.headerHeight ?? Math.round(autoHeaderHeight(size) * DENSITY[this.densityOf(options.density)][1])
|
|
6142
7449
|
});
|
|
7450
|
+
this.density = this.densityOf(options.density);
|
|
7451
|
+
this.renderer.applyTheme();
|
|
6143
7452
|
this.renderer.applyFont(this.font.size, this.font.family);
|
|
6144
7453
|
this.menu = new ColumnMenu(this);
|
|
6145
7454
|
this.chooser = new ColumnChooserDialog(this);
|
|
@@ -6161,10 +7470,12 @@ var Grid = class {
|
|
|
6161
7470
|
() => this.menu.close(),
|
|
6162
7471
|
() => this.chooser.close(),
|
|
6163
7472
|
() => this.contextMenu.destroy(),
|
|
6164
|
-
() => this.editor.
|
|
7473
|
+
() => this.editor.destroy(),
|
|
6165
7474
|
() => this.fillHandle.destroy(),
|
|
6166
7475
|
() => this.groupPanel?.destroy(),
|
|
6167
|
-
() => this.charts.destroy()
|
|
7476
|
+
() => this.charts.destroy(),
|
|
7477
|
+
() => this.filterBuilder.close(),
|
|
7478
|
+
attachTooltips(this)
|
|
6168
7479
|
);
|
|
6169
7480
|
if (typeof ResizeObserver !== "undefined") {
|
|
6170
7481
|
const ro = new ResizeObserver(() => this.renderer.schedule());
|
|
@@ -6313,7 +7624,208 @@ var Grid = class {
|
|
|
6313
7624
|
this.renderer.invalidate({ header: true, rows: true });
|
|
6314
7625
|
this.refreshChoosers();
|
|
6315
7626
|
}
|
|
6316
|
-
|
|
7627
|
+
isFlashing(rowId, colId) {
|
|
7628
|
+
return this.flashing.size > 0 && this.flashing.has(`${rowId}\0${colId}`);
|
|
7629
|
+
}
|
|
7630
|
+
flash(keys) {
|
|
7631
|
+
const until = Date.now() + 1e3;
|
|
7632
|
+
for (const k of keys) this.flashing.set(k, until);
|
|
7633
|
+
if (this.flashTimer) return;
|
|
7634
|
+
const tick = () => {
|
|
7635
|
+
const now = Date.now();
|
|
7636
|
+
for (const [k, t] of this.flashing) if (t <= now) this.flashing.delete(k);
|
|
7637
|
+
this.renderer.invalidate({ rows: true });
|
|
7638
|
+
this.flashTimer = this.flashing.size && !this.destroyed ? window.setTimeout(tick, 250) : 0;
|
|
7639
|
+
};
|
|
7640
|
+
this.flashTimer = window.setTimeout(tick, 1e3);
|
|
7641
|
+
}
|
|
7642
|
+
applyTransaction(tx) {
|
|
7643
|
+
if (!tx || typeof tx !== "object") return { added: [], updated: [], removed: [], notFound: [] };
|
|
7644
|
+
return this.applyTransactions([tx]);
|
|
7645
|
+
}
|
|
7646
|
+
/** Apply transactions one after another, then render and emit once. */
|
|
7647
|
+
applyTransactions(list) {
|
|
7648
|
+
const result = { added: [], updated: [], removed: [], notFound: [] };
|
|
7649
|
+
if (this.clientOnly("applyTransaction")) return result;
|
|
7650
|
+
const colsByField = /* @__PURE__ */ new Map();
|
|
7651
|
+
for (const st of this.columns.getState()) {
|
|
7652
|
+
const f = this.columns.getNode(st.colId)?.def.field;
|
|
7653
|
+
if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
|
|
7654
|
+
}
|
|
7655
|
+
const flashKeys = [];
|
|
7656
|
+
let next = this.store.state.data;
|
|
7657
|
+
const present = new Set(next);
|
|
7658
|
+
const byId = new Map(next.map((r) => [this.rowModel.idOf(r), r]));
|
|
7659
|
+
for (const tx of list) {
|
|
7660
|
+
if (tx && typeof tx === "object") next = this.txStep(tx, next, { present, byId }, result, flashKeys, colsByField);
|
|
7661
|
+
}
|
|
7662
|
+
if (!result.added.length && !result.updated.length && !result.removed.length) {
|
|
7663
|
+
if (result.notFound.length) this.events.emit("transactionApplied", result);
|
|
7664
|
+
return result;
|
|
7665
|
+
}
|
|
7666
|
+
this.editVersion++;
|
|
7667
|
+
this.renderer.invalidateHeights();
|
|
7668
|
+
this.keepRange = true;
|
|
7669
|
+
try {
|
|
7670
|
+
this.replaceData(next === this.store.state.data ? [...next] : next);
|
|
7671
|
+
} finally {
|
|
7672
|
+
this.keepRange = false;
|
|
7673
|
+
}
|
|
7674
|
+
this.editor.sync();
|
|
7675
|
+
if (flashKeys.length && this.options.flashCells !== false) this.flash(flashKeys);
|
|
7676
|
+
this.events.emit("transactionApplied", result);
|
|
7677
|
+
return result;
|
|
7678
|
+
}
|
|
7679
|
+
/** One transaction against `data` (remove → update → add); returns the new array. */
|
|
7680
|
+
txStep(tx, data, index, result, flashKeys, colsByField) {
|
|
7681
|
+
const getId = this.options.getRowId;
|
|
7682
|
+
const { present, byId } = index;
|
|
7683
|
+
const keyOf = (r) => {
|
|
7684
|
+
if (!r || typeof r !== "object") return null;
|
|
7685
|
+
if (present.has(r)) return this.rowModel.idOf(r);
|
|
7686
|
+
try {
|
|
7687
|
+
return getId ? String(getId(r)) : null;
|
|
7688
|
+
} catch {
|
|
7689
|
+
return null;
|
|
7690
|
+
}
|
|
7691
|
+
};
|
|
7692
|
+
const gone = /* @__PURE__ */ new Set();
|
|
7693
|
+
for (const r of tx.remove ?? []) {
|
|
7694
|
+
const k = keyOf(r);
|
|
7695
|
+
const row = k === null ? void 0 : byId.get(k);
|
|
7696
|
+
if (row && !gone.has(row)) {
|
|
7697
|
+
gone.add(row);
|
|
7698
|
+
present.delete(row);
|
|
7699
|
+
byId.delete(k);
|
|
7700
|
+
result.removed.push(row);
|
|
7701
|
+
this.tracker.created.delete(row);
|
|
7702
|
+
this.tracker.originals.delete(row);
|
|
7703
|
+
} else if (!row) result.notFound.push(r);
|
|
7704
|
+
}
|
|
7705
|
+
for (const r of tx.update ?? []) {
|
|
7706
|
+
const k = keyOf(r);
|
|
7707
|
+
const row = k === null ? void 0 : byId.get(k);
|
|
7708
|
+
if (!row || gone.has(row)) {
|
|
7709
|
+
result.notFound.push(r);
|
|
7710
|
+
continue;
|
|
7711
|
+
}
|
|
7712
|
+
const orig = this.tracker.originals.get(row);
|
|
7713
|
+
for (const [field, value] of Object.entries(r)) {
|
|
7714
|
+
if (orig) orig[field] = value;
|
|
7715
|
+
if (Object.is(row[field], value)) continue;
|
|
7716
|
+
row[field] = value;
|
|
7717
|
+
for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
|
|
7718
|
+
}
|
|
7719
|
+
result.updated.push(row);
|
|
7720
|
+
}
|
|
7721
|
+
const added = cleanRows(Array.isArray(tx.add) ? tx.add : []);
|
|
7722
|
+
result.added.push(...added);
|
|
7723
|
+
for (const r of added) {
|
|
7724
|
+
present.add(r);
|
|
7725
|
+
byId.set(this.rowModel.idOf(r), r);
|
|
7726
|
+
}
|
|
7727
|
+
if (!gone.size && !added.length) return data;
|
|
7728
|
+
const next = gone.size ? data.filter((r) => !gone.has(r)) : [...data];
|
|
7729
|
+
const at = typeof tx.addIndex === "number" ? Math.max(0, Math.min(next.length, Math.floor(tx.addIndex))) : next.length;
|
|
7730
|
+
next.splice(at, 0, ...added);
|
|
7731
|
+
return next;
|
|
7732
|
+
}
|
|
7733
|
+
applyTransactionAsync(tx) {
|
|
7734
|
+
if (!tx || typeof tx !== "object") return;
|
|
7735
|
+
this.txQueue.push(tx);
|
|
7736
|
+
if (this.txTimer) return;
|
|
7737
|
+
this.txTimer = window.setTimeout(() => {
|
|
7738
|
+
this.txTimer = 0;
|
|
7739
|
+
const queue = this.txQueue;
|
|
7740
|
+
this.txQueue = [];
|
|
7741
|
+
if (this.destroyed || !queue.length) return;
|
|
7742
|
+
this.applyTransactions(queue);
|
|
7743
|
+
}, 50);
|
|
7744
|
+
}
|
|
7745
|
+
setAdvancedFilter(filter) {
|
|
7746
|
+
const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
|
|
7747
|
+
if (next === this.store.state.advancedFilter) return;
|
|
7748
|
+
this.store.set({ advancedFilter: next });
|
|
7749
|
+
this.events.emit("advancedFilterChange", { advancedFilter: next });
|
|
7750
|
+
}
|
|
7751
|
+
setColumnGroupOpened(groupId, open) {
|
|
7752
|
+
if (!this.columns.setGroupOpen(groupId, !!open)) return;
|
|
7753
|
+
this.layoutChanged();
|
|
7754
|
+
this.events.emit("columnGroupOpened", { groupId, open: !!open });
|
|
7755
|
+
}
|
|
7756
|
+
getState() {
|
|
7757
|
+
const s = this.store.state;
|
|
7758
|
+
const groupBy = this.savedGroup?.groupBy ?? this.groupBy;
|
|
7759
|
+
const expanded = this.savedGroup?.expanded ?? this.expanded;
|
|
7760
|
+
const state = {
|
|
7761
|
+
columns: this.columns.getState(),
|
|
7762
|
+
columnGroups: this.columns.groupOpenState(),
|
|
7763
|
+
advancedFilter: s.advancedFilter ? JSON.parse(JSON.stringify(s.advancedFilter)) : null,
|
|
7764
|
+
sort: s.sort.map((x) => ({ ...x })),
|
|
7765
|
+
filters: JSON.parse(JSON.stringify(s.filters)),
|
|
7766
|
+
quickFilter: s.quickFilter,
|
|
7767
|
+
groupBy: [...groupBy],
|
|
7768
|
+
pivot: this.pivotCfg ? JSON.parse(JSON.stringify(this.pivotCfg)) : null,
|
|
7769
|
+
expanded: Object.fromEntries(expanded),
|
|
7770
|
+
selected: [...s.selected]
|
|
7771
|
+
};
|
|
7772
|
+
if (this.options.pagination) {
|
|
7773
|
+
state.page = this.page;
|
|
7774
|
+
state.pageSize = this.pageSize;
|
|
7775
|
+
}
|
|
7776
|
+
return state;
|
|
7777
|
+
}
|
|
7778
|
+
setState(state, options = {}) {
|
|
7779
|
+
if (!state || typeof state !== "object") return;
|
|
7780
|
+
const want = (k) => state[k] !== void 0 && (!options.parts || options.parts.includes(k));
|
|
7781
|
+
const known = (id) => !!this.columns.getNode(id);
|
|
7782
|
+
if (want("columns") && Array.isArray(state.columns)) {
|
|
7783
|
+
this.columns.applyState(state.columns.filter((c) => c && known(c.colId)));
|
|
7784
|
+
this.columnsChanged();
|
|
7785
|
+
}
|
|
7786
|
+
if (want("columnGroups") && state.columnGroups && typeof state.columnGroups === "object") {
|
|
7787
|
+
for (const [id, open] of Object.entries(state.columnGroups)) if (typeof open === "boolean") this.setColumnGroupOpened(id, open);
|
|
7788
|
+
}
|
|
7789
|
+
if (want("groupBy") && Array.isArray(state.groupBy)) this.setGroupBy(state.groupBy.filter(known));
|
|
7790
|
+
if (want("pivot")) this.setPivot(state.pivot ?? null);
|
|
7791
|
+
if (want("sort") && Array.isArray(state.sort)) this.setSort(state.sort.filter((x) => x && known(x.colId)));
|
|
7792
|
+
if (want("filters") && state.filters && typeof state.filters === "object") {
|
|
7793
|
+
const filters = Object.fromEntries(Object.entries(state.filters).filter(([id]) => known(id)));
|
|
7794
|
+
this.store.set({ filters });
|
|
7795
|
+
this.events.emit("filterChange", { filters: { ...filters } });
|
|
7796
|
+
}
|
|
7797
|
+
if (want("advancedFilter")) this.setAdvancedFilter(state.advancedFilter ?? null);
|
|
7798
|
+
if (want("quickFilter") && typeof state.quickFilter === "string") this.store.set({ quickFilter: state.quickFilter });
|
|
7799
|
+
if (want("expanded") && state.expanded && typeof state.expanded === "object") {
|
|
7800
|
+
const expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
|
|
7801
|
+
if (this.savedGroup) this.savedGroup.expanded = expanded;
|
|
7802
|
+
else {
|
|
7803
|
+
this.expanded = expanded;
|
|
7804
|
+
this.displayChanged();
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
|
|
7808
|
+
if (want("page") && typeof state.page === "number") this.setPage(state.page);
|
|
7809
|
+
if (want("selected") && Array.isArray(state.selected)) {
|
|
7810
|
+
const ids = new Set(this.rowModel.allNodes().map((n) => n.id));
|
|
7811
|
+
this.setSelected(new Set(state.selected.map(String).filter((id) => ids.has(id))));
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
/** Number shown in the row-number column for display row `i` (data rows only; continues across pages). */
|
|
7815
|
+
rowNumber(i) {
|
|
7816
|
+
const rows = this.rowModel.rows();
|
|
7817
|
+
if (this.rowNumberCache?.rows !== rows) {
|
|
7818
|
+
const nums = new Int32Array(rows.length);
|
|
7819
|
+
let n = 0;
|
|
7820
|
+
rows.forEach((r, k) => {
|
|
7821
|
+
nums[k] = r.kind ? 0 : ++n;
|
|
7822
|
+
});
|
|
7823
|
+
this.rowNumberCache = { rows, nums };
|
|
7824
|
+
}
|
|
7825
|
+
const v = this.rowNumberCache.nums[i];
|
|
7826
|
+
if (!v) return null;
|
|
7827
|
+
return v + (this.options.pagination && !this.rowModel.server ? this.page * this.pageSize : 0);
|
|
7828
|
+
}
|
|
6317
7829
|
getPage() {
|
|
6318
7830
|
return this.options.pagination ? this.page : 0;
|
|
6319
7831
|
}
|
|
@@ -6428,7 +7940,6 @@ var Grid = class {
|
|
|
6428
7940
|
}
|
|
6429
7941
|
validGroupBy(ids) {
|
|
6430
7942
|
if (!Array.isArray(ids)) return [];
|
|
6431
|
-
if (ids.length && this.clientOnly("grouping")) return [];
|
|
6432
7943
|
return [...new Set(ids)].filter((id) => {
|
|
6433
7944
|
const node = this.columns.getNode(id);
|
|
6434
7945
|
if (!node || node.children || isSystemCol(id)) {
|
|
@@ -6515,6 +8026,7 @@ var Grid = class {
|
|
|
6515
8026
|
groupAggregate(node, colId) {
|
|
6516
8027
|
const def = this.columns.getNode(colId)?.def;
|
|
6517
8028
|
if (!node.group || !def?.aggFunc) return void 0;
|
|
8029
|
+
if (node.group.aggregates) return node.group.aggregates[colId] ?? (def.field ? node.group.aggregates[def.field] : void 0);
|
|
6518
8030
|
if (this.aggCache.ver !== this.editVersion) this.aggCache = { ver: this.editVersion, map: /* @__PURE__ */ new WeakMap() };
|
|
6519
8031
|
let m = this.aggCache.map.get(node.group);
|
|
6520
8032
|
if (!m) this.aggCache.map.set(node.group, m = /* @__PURE__ */ new Map());
|
|
@@ -6688,13 +8200,30 @@ var Grid = class {
|
|
|
6688
8200
|
const family = font.family === void 0 ? this.font.family : font.family || null;
|
|
6689
8201
|
this.font = { size, family };
|
|
6690
8202
|
const effective = size ?? DEFAULT_FONT_SIZE;
|
|
6691
|
-
|
|
6692
|
-
if (this.options.headerHeight == null) this.renderer.opts.headerHeight = autoHeaderHeight(effective);
|
|
8203
|
+
this.applyAutoHeights(effective);
|
|
6693
8204
|
this.renderer.applyFont(size, family);
|
|
6694
8205
|
this.renderer.invalidateHeights();
|
|
6695
8206
|
this.menu.close();
|
|
6696
8207
|
this.layoutChanged();
|
|
6697
8208
|
}
|
|
8209
|
+
densityOf(d) {
|
|
8210
|
+
return d === "compact" || d === "comfortable" ? d : "normal";
|
|
8211
|
+
}
|
|
8212
|
+
/** Row and header heights from the font size and density, unless the options fix them. */
|
|
8213
|
+
applyAutoHeights(fontSize) {
|
|
8214
|
+
const [r, h] = DENSITY[this.density];
|
|
8215
|
+
if (this.options.rowHeight == null) this.renderer.opts.rowHeight = Math.round(autoRowHeight(fontSize) * r);
|
|
8216
|
+
if (this.options.headerHeight == null) this.renderer.opts.headerHeight = Math.round(autoHeaderHeight(fontSize) * h);
|
|
8217
|
+
}
|
|
8218
|
+
setDensity(d) {
|
|
8219
|
+
const next = this.densityOf(d);
|
|
8220
|
+
if (next === this.density) return;
|
|
8221
|
+
this.density = next;
|
|
8222
|
+
this.renderer.applyTheme();
|
|
8223
|
+
this.applyAutoHeights(this.font.size ?? DEFAULT_FONT_SIZE);
|
|
8224
|
+
this.renderer.invalidateHeights();
|
|
8225
|
+
this.layoutChanged();
|
|
8226
|
+
}
|
|
6698
8227
|
/** Copy the grid font variables onto an element rendered outside the root (popups, drag ghost). */
|
|
6699
8228
|
applyFontTo(target) {
|
|
6700
8229
|
if (this.font.size) target.style.setProperty("--fg-font-size", `${this.font.size}px`);
|
|
@@ -6797,7 +8326,7 @@ var Grid = class {
|
|
|
6797
8326
|
if (all2.length > 1) {
|
|
6798
8327
|
const sameCols = all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2);
|
|
6799
8328
|
const sameRows = all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2);
|
|
6800
|
-
if (
|
|
8329
|
+
if (this.rangesStack(all2)) {
|
|
6801
8330
|
const parts = [...all2].sort((x, y) => sameCols ? x.r1 - y.r1 : x.c1 - y.c1);
|
|
6802
8331
|
const blocks = parts.map((p) => this.rangeMatrix(p));
|
|
6803
8332
|
const matrix2 = sameCols ? blocks.flat() : blocks[0].map((_, ri) => blocks.flatMap((bl) => bl[ri] ?? []));
|
|
@@ -6828,6 +8357,10 @@ var Grid = class {
|
|
|
6828
8357
|
if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
|
|
6829
8358
|
return toTSV(matrix);
|
|
6830
8359
|
}
|
|
8360
|
+
/** Like Excel: ranges stack when they share columns or rows; otherwise only the active one is copied. */
|
|
8361
|
+
rangesStack(all2) {
|
|
8362
|
+
return all2.length > 1 && (all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2) || all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2));
|
|
8363
|
+
}
|
|
6831
8364
|
rangeHeaders(b) {
|
|
6832
8365
|
return this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id)).map((c) => c.def.header ?? c.def.field ?? c.id);
|
|
6833
8366
|
}
|
|
@@ -6843,19 +8376,22 @@ var Grid = class {
|
|
|
6843
8376
|
return out;
|
|
6844
8377
|
}
|
|
6845
8378
|
copyCell(def, row, code) {
|
|
6846
|
-
|
|
8379
|
+
return this.copyAllowed(def, row) ? copyValue(def, getCellValue(def, row), code) : "";
|
|
8380
|
+
}
|
|
8381
|
+
copyAllowed(def, row) {
|
|
6847
8382
|
const rule = def.copyable;
|
|
6848
|
-
if (rule
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
} catch {
|
|
6854
|
-
ok = false;
|
|
6855
|
-
}
|
|
6856
|
-
if (!ok) return "";
|
|
8383
|
+
if (typeof rule !== "function") return rule !== false;
|
|
8384
|
+
try {
|
|
8385
|
+
return rule({ value: getCellValue(def, row), row, column: def }) !== false;
|
|
8386
|
+
} catch {
|
|
8387
|
+
return false;
|
|
6857
8388
|
}
|
|
6858
|
-
|
|
8389
|
+
}
|
|
8390
|
+
/** Copy, then clear the editable cells of the selection. */
|
|
8391
|
+
cutToClipboard() {
|
|
8392
|
+
const text2 = this.copyToClipboard(false);
|
|
8393
|
+
if (text2) this.clearCells(true);
|
|
8394
|
+
return text2;
|
|
6859
8395
|
}
|
|
6860
8396
|
copyToClipboard(headers, rows = false) {
|
|
6861
8397
|
const text2 = this.selectionTSV(headers, rows);
|
|
@@ -7113,10 +8649,14 @@ var Grid = class {
|
|
|
7113
8649
|
this.writeCell(changes, node.data, def.field, colId, rowIndex, value);
|
|
7114
8650
|
this.endCellTx(changes);
|
|
7115
8651
|
}
|
|
7116
|
-
/** Delete/Backspace: clear editable cells in the range, or the focused cell. */
|
|
7117
|
-
clearCells() {
|
|
8652
|
+
/** Delete/Backspace: clear editable cells in the range, or the focused cell. `cut` clears only what a copy takes. */
|
|
8653
|
+
clearCells(cut = false) {
|
|
7118
8654
|
const displayed = this.columns.displayed();
|
|
7119
|
-
|
|
8655
|
+
let all2 = this.allRangeBounds();
|
|
8656
|
+
if (cut && !this.rangesStack(all2)) {
|
|
8657
|
+
const active = this.rangeBounds();
|
|
8658
|
+
all2 = active ? [active] : [];
|
|
8659
|
+
}
|
|
7120
8660
|
const f = this.store.state.focus;
|
|
7121
8661
|
const fi = f ? this.columns.getColumn(f.colId)?.index : void 0;
|
|
7122
8662
|
const boxes = all2.length ? all2 : f && fi !== void 0 ? [{ r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi }] : [];
|
|
@@ -7126,7 +8666,7 @@ var Grid = class {
|
|
|
7126
8666
|
const node = this.rowModel.getRow(r);
|
|
7127
8667
|
for (let c = box.c1; c <= box.c2; c++) {
|
|
7128
8668
|
const def = displayed[c]?.def;
|
|
7129
|
-
if (node && !node.kind && def?.field && cellEditable(def, node.data, r)) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
|
|
8669
|
+
if (node && !node.kind && def?.field && cellEditable(def, node.data, r) && (!cut || this.copyAllowed(def, node.data))) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
|
|
7130
8670
|
}
|
|
7131
8671
|
}
|
|
7132
8672
|
this.endCellTx(changes);
|
|
@@ -7228,8 +8768,9 @@ var Grid = class {
|
|
|
7228
8768
|
const m = this.getModifiedRows();
|
|
7229
8769
|
return m.created.length + m.updated.length + m.deleted.length > 0;
|
|
7230
8770
|
}
|
|
7231
|
-
commitChanges() {
|
|
7232
|
-
this.tracker.
|
|
8771
|
+
commitChanges(saved) {
|
|
8772
|
+
if (saved && saved.rows instanceof Map) this.tracker.commit(saved.rows, Array.isArray(saved.deleted) ? saved.deleted : []);
|
|
8773
|
+
else this.tracker.reset();
|
|
7233
8774
|
this.renderer.invalidate({ rows: true });
|
|
7234
8775
|
}
|
|
7235
8776
|
revertChanges() {
|
|
@@ -7346,6 +8887,7 @@ var Grid = class {
|
|
|
7346
8887
|
this.events.emit("focusChange", { rowIndex, colId });
|
|
7347
8888
|
}
|
|
7348
8889
|
setSelected(selected, anchorId = this.store.state.anchorId) {
|
|
8890
|
+
this.selectionVersion++;
|
|
7349
8891
|
this.store.set({ selected, anchorId });
|
|
7350
8892
|
this.events.emit("selectionChange", { selected: this.selectedRows() });
|
|
7351
8893
|
}
|
|
@@ -7359,6 +8901,55 @@ var Grid = class {
|
|
|
7359
8901
|
return false;
|
|
7360
8902
|
}
|
|
7361
8903
|
}
|
|
8904
|
+
/** Selectable data row ids under a group row, or the descendants of a tree row. */
|
|
8905
|
+
groupMemberIds(node) {
|
|
8906
|
+
if (node.group) return node.group.rows.filter((r) => this.rowSelectable(r)).map((r) => this.rowModel.idOf(r));
|
|
8907
|
+
const getParent = this.options.treeData?.getParentId;
|
|
8908
|
+
if (!getParent) return [];
|
|
8909
|
+
const kids = /* @__PURE__ */ new Map();
|
|
8910
|
+
for (const r of this.store.state.data) {
|
|
8911
|
+
let p = null;
|
|
8912
|
+
try {
|
|
8913
|
+
p = getParent(r);
|
|
8914
|
+
} catch {
|
|
8915
|
+
p = null;
|
|
8916
|
+
}
|
|
8917
|
+
if (p == null) continue;
|
|
8918
|
+
const k = String(p);
|
|
8919
|
+
const list = kids.get(k);
|
|
8920
|
+
if (list) list.push(r);
|
|
8921
|
+
else kids.set(k, [r]);
|
|
8922
|
+
}
|
|
8923
|
+
const out = [];
|
|
8924
|
+
const seen2 = /* @__PURE__ */ new Set([node.id]);
|
|
8925
|
+
const queue = [node.id];
|
|
8926
|
+
while (queue.length) {
|
|
8927
|
+
for (const r of kids.get(queue.shift()) ?? []) {
|
|
8928
|
+
const id = this.rowModel.idOf(r);
|
|
8929
|
+
if (seen2.has(id)) continue;
|
|
8930
|
+
seen2.add(id);
|
|
8931
|
+
queue.push(id);
|
|
8932
|
+
if (this.rowSelectable(r)) out.push(id);
|
|
8933
|
+
}
|
|
8934
|
+
}
|
|
8935
|
+
return out;
|
|
8936
|
+
}
|
|
8937
|
+
/** 'all', 'some' or 'none' of a group row's rows are selected. */
|
|
8938
|
+
groupSelection(node) {
|
|
8939
|
+
const ids = this.groupMemberIds(node);
|
|
8940
|
+
const sel = this.store.state.selected;
|
|
8941
|
+
const n = ids.filter((id) => sel.has(id)).length;
|
|
8942
|
+
return n === 0 ? "none" : n === ids.length ? "all" : "some";
|
|
8943
|
+
}
|
|
8944
|
+
/** Group row checkbox: select every row of the group, or clear them when all were selected. */
|
|
8945
|
+
toggleGroupSelection(node) {
|
|
8946
|
+
if (this.options.rowSelection !== "multiple") return;
|
|
8947
|
+
const ids = this.groupMemberIds(node);
|
|
8948
|
+
const next = new Set(this.store.state.selected);
|
|
8949
|
+
const all2 = ids.length > 0 && ids.every((id) => next.has(id));
|
|
8950
|
+
for (const id of ids) all2 ? next.delete(id) : next.add(id);
|
|
8951
|
+
this.setSelected(next);
|
|
8952
|
+
}
|
|
7362
8953
|
selectFromClick(index, toggle, range) {
|
|
7363
8954
|
const mode = this.options.rowSelection;
|
|
7364
8955
|
if (!mode || index < 0 || index >= this.rowModel.getRowCount()) return;
|
|
@@ -7381,6 +8972,10 @@ var Grid = class {
|
|
|
7381
8972
|
if (n && (n.kind || !this.rowSelectable(n.data))) r.selected.delete(id);
|
|
7382
8973
|
}
|
|
7383
8974
|
}
|
|
8975
|
+
if (mode === "multiple" && this.options.groupSelectsChildren && this.options.treeData && target.hasChildren) {
|
|
8976
|
+
const on = r.selected.has(target.id);
|
|
8977
|
+
for (const id of this.groupMemberIds(target)) on ? r.selected.add(id) : r.selected.delete(id);
|
|
8978
|
+
}
|
|
7384
8979
|
this.setSelected(r.selected, this.rowModel.getRow(r.anchorIndex)?.id ?? null);
|
|
7385
8980
|
}
|
|
7386
8981
|
/** Displayed index of the selection anchor, or null when it is not displayed (filtered out or removed). */
|
|
@@ -7412,6 +9007,11 @@ var Grid = class {
|
|
|
7412
9007
|
}
|
|
7413
9008
|
withSelect(defs) {
|
|
7414
9009
|
const sys = [];
|
|
9010
|
+
const rn = this.options.rowNumbers;
|
|
9011
|
+
if (rn) {
|
|
9012
|
+
const o = typeof rn === "object" ? rn : {};
|
|
9013
|
+
sys.push(rowNumberColumn(typeof o.width === "number" && o.width > 0 ? o.width : void 0, typeof o.header === "string" ? o.header : void 0));
|
|
9014
|
+
}
|
|
7415
9015
|
if (this.options.rowDrag) sys.push(DRAG_COLUMN);
|
|
7416
9016
|
if (this.options.rowSelection) sys.push(SELECT_COLUMN);
|
|
7417
9017
|
return [...sys, ...defs];
|
|
@@ -7478,21 +9078,21 @@ var Grid = class {
|
|
|
7478
9078
|
this.sideBar?.refresh();
|
|
7479
9079
|
this.rowModel.invalidate();
|
|
7480
9080
|
}
|
|
7481
|
-
const resetPage = has("sort", "filters", "quickFilter") && this.page !== 0;
|
|
9081
|
+
const resetPage = has("sort", "filters", "quickFilter", "advancedFilter") && this.page !== 0;
|
|
7482
9082
|
if (resetPage) this.page = 0;
|
|
7483
|
-
if (has("data", "sort", "filters", "quickFilter", "locale")) this.updateRows();
|
|
9083
|
+
if (has("data", "sort", "filters", "quickFilter", "advancedFilter", "locale")) this.updateRows();
|
|
7484
9084
|
if (resetPage) this.emitPage();
|
|
7485
|
-
if (has("data", "sort", "filters", "quickFilter") && this.store.state.range) this.setRange(null);
|
|
7486
|
-
if (has("data", "sort", "filters", "quickFilter", "locale")) this.finder.refresh();
|
|
7487
|
-
if (has("data", "filters", "quickFilter", "locale") || has("selected") && this.summaryOpts?.scope === "selected") {
|
|
9085
|
+
if (has("data", "sort", "filters", "quickFilter", "advancedFilter") && this.store.state.range && !this.keepRange) this.setRange(null);
|
|
9086
|
+
if (has("data", "sort", "filters", "quickFilter", "advancedFilter", "locale")) this.finder.refresh();
|
|
9087
|
+
if (has("data", "filters", "quickFilter", "advancedFilter", "locale") || has("selected") && this.summaryOpts?.scope === "selected") {
|
|
7488
9088
|
this.refreshSummary();
|
|
7489
9089
|
}
|
|
7490
9090
|
if (has("locale")) this.groupPanel?.refresh();
|
|
7491
9091
|
if (has("filters")) this.sideBar?.refresh();
|
|
7492
9092
|
if (has("theme")) this.renderer.applyTheme();
|
|
7493
9093
|
this.renderer.invalidate({
|
|
7494
|
-
header: has("sort", "filters", "quickFilter", "locale", "selected", "data"),
|
|
7495
|
-
rows: has("data", "sort", "filters", "quickFilter", "locale")
|
|
9094
|
+
header: has("sort", "filters", "quickFilter", "advancedFilter", "locale", "selected", "data"),
|
|
9095
|
+
rows: has("data", "sort", "filters", "quickFilter", "advancedFilter", "locale")
|
|
7496
9096
|
});
|
|
7497
9097
|
}
|
|
7498
9098
|
createApi() {
|
|
@@ -7523,6 +9123,24 @@ var Grid = class {
|
|
|
7523
9123
|
getDisplayedRowCount: guard(() => this.rowModel.getRowCount()),
|
|
7524
9124
|
scrollToRow: guard((i) => this.renderer.scrollToRow(i)),
|
|
7525
9125
|
validate: guard(() => this.validateAll()),
|
|
9126
|
+
setDensity: guard((d) => this.setDensity(d)),
|
|
9127
|
+
getDensity: guard(() => this.density),
|
|
9128
|
+
showOverlay: guard((kind) => {
|
|
9129
|
+
this.forcedOverlay = kind === "loading" || kind === "noRows" ? kind : null;
|
|
9130
|
+
this.renderer.invalidate();
|
|
9131
|
+
}),
|
|
9132
|
+
getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
|
|
9133
|
+
print: guard((o) => printHtml(buildPrintHtml(this, o))),
|
|
9134
|
+
getPrintHtml: guard((o) => buildPrintHtml(this, o)),
|
|
9135
|
+
setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
|
|
9136
|
+
setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
|
|
9137
|
+
getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
|
|
9138
|
+
openFilterBuilder: guard(() => this.filterBuilder.open()),
|
|
9139
|
+
isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
|
|
9140
|
+
applyTransaction: guard((tx) => this.applyTransaction(tx)),
|
|
9141
|
+
applyTransactionAsync: guard((tx) => this.applyTransactionAsync(tx)),
|
|
9142
|
+
getState: guard(() => this.getState()),
|
|
9143
|
+
setState: guard((s, o) => this.setState(s, o)),
|
|
7526
9144
|
getScrollPosition: guard(() => this.renderer.getScroll()),
|
|
7527
9145
|
setScrollPosition: guard((p) => this.renderer.setScroll(p ?? {})),
|
|
7528
9146
|
syncScroll: guard((other, axis = "both") => this.syncScroll(other, axis)),
|
|
@@ -7546,6 +9164,8 @@ var Grid = class {
|
|
|
7546
9164
|
setRanges: guard((list) => this.setRanges(list)),
|
|
7547
9165
|
copySelection: guard((o) => this.copyToClipboard(!!o?.headers, !!o?.rows)),
|
|
7548
9166
|
pasteText: guard((text2) => this.pasteText(text2)),
|
|
9167
|
+
cutSelection: guard(() => this.cutToClipboard()),
|
|
9168
|
+
stopRowEdit: guard((cancel) => this.editor.endRow(!cancel)),
|
|
7549
9169
|
startEdit: guard((rowIndex, colId) => this.editor.start(rowIndex, colId)),
|
|
7550
9170
|
stopEdit: guard((cancel) => cancel ? this.editor.cancel() : void this.editor.commit(null)),
|
|
7551
9171
|
addRows: guard((rows, index) => this.addRows(rows, index)),
|
|
@@ -7554,7 +9174,7 @@ var Grid = class {
|
|
|
7554
9174
|
getModifiedRows: guard(() => this.getModifiedRows()),
|
|
7555
9175
|
getChanges: guard(() => this.getChanges()),
|
|
7556
9176
|
hasChanges: guard(() => this.hasChanges()),
|
|
7557
|
-
commitChanges: guard(() => this.commitChanges()),
|
|
9177
|
+
commitChanges: guard((saved) => this.commitChanges(saved)),
|
|
7558
9178
|
revertChanges: guard(() => this.revertChanges()),
|
|
7559
9179
|
undo: guard(() => this.undo()),
|
|
7560
9180
|
redo: guard(() => this.redo()),
|
|
@@ -7597,6 +9217,8 @@ var Grid = class {
|
|
|
7597
9217
|
if (this.destroyed) return;
|
|
7598
9218
|
this.destroyed = true;
|
|
7599
9219
|
liveGrids.delete(this.renderer.root);
|
|
9220
|
+
clearTimeout(this.flashTimer);
|
|
9221
|
+
clearTimeout(this.txTimer);
|
|
7600
9222
|
for (const d of this.disposers) d();
|
|
7601
9223
|
this.renderer.destroy();
|
|
7602
9224
|
this.events.clear();
|
|
@@ -7605,14 +9227,113 @@ var Grid = class {
|
|
|
7605
9227
|
function createGrid(host, options) {
|
|
7606
9228
|
return new Grid(host, options).api;
|
|
7607
9229
|
}
|
|
9230
|
+
|
|
9231
|
+
// src/workbook.ts
|
|
9232
|
+
function buildWorkbook(sheets) {
|
|
9233
|
+
const list = (Array.isArray(sheets) ? sheets : []).filter((s) => s && s.grid && typeof s.grid.getXlsxSheet === "function");
|
|
9234
|
+
if (!list.length) {
|
|
9235
|
+
warn("buildWorkbook: no grids given; the file has one empty sheet");
|
|
9236
|
+
return writeXlsx([{ name: "Sheet1", rows: [] }]);
|
|
9237
|
+
}
|
|
9238
|
+
return writeXlsx(list.map((s, i) => s.grid.getXlsxSheet({ ...s.options, sheetName: s.sheetName ?? s.options?.sheetName ?? `Sheet${i + 1}` })));
|
|
9239
|
+
}
|
|
9240
|
+
function exportWorkbook(filename, sheets) {
|
|
9241
|
+
download(filename || "export.xlsx", [buildWorkbook(sheets)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
9242
|
+
}
|
|
9243
|
+
|
|
9244
|
+
// src/rest.ts
|
|
9245
|
+
var headersOf = (h) => typeof h === "function" ? h() : h ?? {};
|
|
9246
|
+
async function send(url, method, body, opts) {
|
|
9247
|
+
let target = url;
|
|
9248
|
+
const where = url.split(/[?#]/)[0];
|
|
9249
|
+
const init = { method, credentials: opts.credentials ?? "same-origin", headers: { Accept: "application/json", ...headersOf(opts.headers) } };
|
|
9250
|
+
if (method === "GET") {
|
|
9251
|
+
const q = new URLSearchParams();
|
|
9252
|
+
for (const [k, v] of Object.entries(body)) if (v !== void 0) q.set(k, typeof v === "string" ? v : JSON.stringify(v));
|
|
9253
|
+
target += (url.includes("?") ? "&" : "?") + q.toString();
|
|
9254
|
+
} else {
|
|
9255
|
+
init.headers["Content-Type"] = "application/json";
|
|
9256
|
+
init.body = JSON.stringify(body);
|
|
9257
|
+
}
|
|
9258
|
+
const res = await fetch(target, init);
|
|
9259
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from ${where}`);
|
|
9260
|
+
const text2 = await res.text();
|
|
9261
|
+
if (!text2) return null;
|
|
9262
|
+
try {
|
|
9263
|
+
return JSON.parse(text2);
|
|
9264
|
+
} catch {
|
|
9265
|
+
throw new Error(`HTTP ${res.status} from ${where}: the response is not JSON`);
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
function readRows(json) {
|
|
9269
|
+
if (Array.isArray(json)) return { rows: json };
|
|
9270
|
+
const o = json ?? {};
|
|
9271
|
+
const rows = [o.rows, o.list, o.content, o.data, o.items, o.resultList].find(Array.isArray);
|
|
9272
|
+
const total = [o.total, o.totalCount, o.totalElements, o.totCnt, o.count].find((v) => typeof v === "number");
|
|
9273
|
+
return { rows: (rows ?? []).filter((r) => r !== null && typeof r === "object"), total };
|
|
9274
|
+
}
|
|
9275
|
+
function restDatasource(url, options = {}) {
|
|
9276
|
+
return {
|
|
9277
|
+
async getRows(p) {
|
|
9278
|
+
const size = Math.max(1, p.end - p.start);
|
|
9279
|
+
const body = {
|
|
9280
|
+
start: p.start,
|
|
9281
|
+
end: p.end,
|
|
9282
|
+
page: Math.floor(p.start / size),
|
|
9283
|
+
size,
|
|
9284
|
+
sort: p.sort,
|
|
9285
|
+
filters: p.filters,
|
|
9286
|
+
quickFilter: p.quickFilter,
|
|
9287
|
+
// only when set (undefined is left out of the request)
|
|
9288
|
+
advancedFilter: p.advancedFilter ?? void 0,
|
|
9289
|
+
groupBy: p.groupBy,
|
|
9290
|
+
groupKeys: p.groupKeys,
|
|
9291
|
+
...options.params?.() ?? {}
|
|
9292
|
+
};
|
|
9293
|
+
const json = await send(url, options.method ?? "POST", body, options);
|
|
9294
|
+
return (options.transform ?? readRows)(json);
|
|
9295
|
+
}
|
|
9296
|
+
};
|
|
9297
|
+
}
|
|
9298
|
+
var saving = /* @__PURE__ */ new WeakMap();
|
|
9299
|
+
function saveChanges(grid, url, options = {}) {
|
|
9300
|
+
const pending = saving.get(grid);
|
|
9301
|
+
if (pending) return pending;
|
|
9302
|
+
if (!grid.hasChanges()) return Promise.resolve(null);
|
|
9303
|
+
const { created, updated, deleted } = grid.getModifiedRows();
|
|
9304
|
+
const rows = new Map([...created, ...updated].map((r) => [r, { ...r }]));
|
|
9305
|
+
const run = (async () => {
|
|
9306
|
+
const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
|
|
9307
|
+
grid.commitChanges({ rows, deleted });
|
|
9308
|
+
return json;
|
|
9309
|
+
})().finally(() => saving.delete(grid));
|
|
9310
|
+
saving.set(grid, run);
|
|
9311
|
+
return run;
|
|
9312
|
+
}
|
|
9313
|
+
async function loadData(grid, url, options = {}) {
|
|
9314
|
+
const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });
|
|
9315
|
+
grid.setData(rows);
|
|
9316
|
+
return rows;
|
|
9317
|
+
}
|
|
9318
|
+
function csrfHeaders(doc = document) {
|
|
9319
|
+
const token = doc.querySelector('meta[name="_csrf"]')?.content;
|
|
9320
|
+
const header2 = doc.querySelector('meta[name="_csrf_header"]')?.content || "X-CSRF-TOKEN";
|
|
9321
|
+
return token ? { [header2]: token } : {};
|
|
9322
|
+
}
|
|
7608
9323
|
export {
|
|
9324
|
+
buildWorkbook,
|
|
7609
9325
|
createGrid,
|
|
9326
|
+
csrfHeaders,
|
|
7610
9327
|
en_default as en,
|
|
9328
|
+
exportWorkbook,
|
|
9329
|
+
loadData,
|
|
7611
9330
|
parseCsv,
|
|
7612
9331
|
readXlsx,
|
|
7613
9332
|
registerLocale,
|
|
7614
9333
|
renderChart,
|
|
7615
9334
|
resolveLocale,
|
|
9335
|
+
restDatasource,
|
|
9336
|
+
saveChanges,
|
|
7616
9337
|
setLicenseKey,
|
|
7617
9338
|
verifyLicense,
|
|
7618
9339
|
version,
|