@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/dist/index.js CHANGED
@@ -32,7 +32,20 @@ var DRAG_COLUMN = {
32
32
  filter: false,
33
33
  chooser: false
34
34
  };
35
- var isSystemCol = (id) => id === SELECT_COL_ID || id === DRAG_COL_ID;
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
- node.flex = null;
240
- node.width = clampWidth(node.def, s.width);
241
- node.hide = s.hide;
242
- node.pinned = s.pinned;
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
- const fn = Object.hasOwn(FUNCS, upper) ? FUNCS[upper] : void 0;
674
- if (!fn) throw new FormulaError("#NAME?");
675
- return fn(args);
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
- if (/^(true|false)$/i.test(name)) return name.toLowerCase() === "true";
678
- return this.resolve(name);
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
- var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
703
-
704
- // src/rows/values.ts
705
- function getCellValue(def, row) {
706
- if (def.valueGetter) {
707
- try {
708
- return def.valueGetter(row);
709
- } catch {
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
- if (def.field == null) return void 0;
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 REGIONS2 = ["left", "center", "right"];
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 REGIONS2) this.headerInners[r].setAttribute("role", "row");
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 REGIONS2) {
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.overlay.hidden = count > 0;
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 REGIONS2) {
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.overlay.hidden = count > 0;
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 REGIONS2) {
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 REGIONS2) {
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 REGIONS2) {
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 REGIONS2) {
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 REGIONS2) {
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 REGIONS2) {
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 REGIONS2) {
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 REGIONS2) {
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
- label.textContent = `${def2?.header ?? def2?.field ?? g.colId}: ${value2} (${nf.format(g.rows.length)})`;
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 REGIONS2) {
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 REGIONS2) {
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" && registry.has("zh-CN")) return "zh-CN";
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 : toDateKey(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 toDateKey(value) ?? String(value);
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 : toDateKey(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) this.commit(null);
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
- wrap.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
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" ? toDateKey(value) ?? "" : isBlank(value) ? "" : String(value);
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(cur.rowIndex, cur.colId, value);
3851
- this.move(cur.rowIndex, cur.colId, 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
- async function inflateRaw(data) {
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
- return new Uint8Array(await new Response(stream).arrayBuffer());
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
- if (method === 0) out.set(name, raw);
4247
- else if (method === 8) out.set(name, await inflateRaw(raw));
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 merges = sheet.merges?.length ? `<mergeCells count="${sheet.merges.length}">${sheet.merges.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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
- return { value: copyValue(c.def, v, code), text: !(t === "number" || t === "date" || t === "boolean") || typeof v === "string" };
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 writeXlsx([{
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() : toDateKey(v) ?? "";
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 names = spec.type === "pie" ? cats : series.map((s) => s.name);
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 === "pie") {
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
- label.textContent = c.length > 12 ? `${c.slice(0, 11)}\u2026` : c;
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 MAX_ROWS = 500;
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 < MAX_ROWS; i++) {
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, MAX_ROWS).map((n) => n.data);
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.8.1";
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) || typeof info.expires !== "string") return { ok: false, reason: "format" };
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
- conditionFilter(colId, type, loc, done) {
5479
- const raw = this.grid.store.state.filters[colId];
5480
- const cur = raw && raw.type === type ? raw : void 0;
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 op = select.value;
5505
- const needsValue = !NO_VALUE.includes(op);
5506
- let model = null;
5507
- if (type === "number") {
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
- for (const input of [v1, v2]) input.addEventListener("keydown", (e) => e.key === "Enter" && apply());
5518
- box.append(this.title(loc), select, v1, v2, this.actions(colId, loc, apply, done));
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 (measured && !node.kind) h = measured.get(node.id) ?? rowHeight;
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
- if (!preds.length && !tokens.length) return this.nodes;
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/serverRowModel.ts
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.cancel(),
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
- // ── pagination and server rows ──────────────────────────────
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
- if (this.options.rowHeight == null) this.renderer.opts.rowHeight = autoRowHeight(effective);
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 (sameCols || sameRows) {
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
- const value = getCellValue(def, row);
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 === false) return "";
6849
- if (typeof rule === "function") {
6850
- let ok = false;
6851
- try {
6852
- ok = rule({ value, row, column: def }) !== false;
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
- return copyValue(def, value, code);
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
- const all2 = this.allRangeBounds();
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.reset();
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,