@freegridjs/core 0.8.1 → 1.0.0

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) {
@@ -670,50 +503,465 @@ var Parser = class {
670
503
  const cond = typeof args[0] === "boolean" ? args[0] : num(args[0]) !== 0;
671
504
  return cond ? args[1] : args.length > 2 ? args[2] : false;
672
505
  }
673
- const fn = Object.hasOwn(FUNCS, upper) ? FUNCS[upper] : void 0;
674
- if (!fn) throw new FormulaError("#NAME?");
675
- return fn(args);
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) {
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
+ const r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1);
523
+ if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
524
+ return r;
525
+ }
526
+ return v2;
527
+ };
528
+ const v = new Parser(tokenize(formula.slice(1)), resolve).parse();
529
+ if (typeof v === "number" && !Number.isFinite(v)) return "#NUM!";
530
+ return v;
531
+ } catch (e) {
532
+ return e instanceof FormulaError ? e.code : "#ERROR!";
533
+ }
534
+ }
535
+ var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
536
+
537
+ // src/rows/values.ts
538
+ function getCellValue(def, row) {
539
+ if (def.valueGetter) {
540
+ try {
541
+ return def.valueGetter(row);
542
+ } catch {
543
+ warn(`valueGetter of column "${def.id ?? def.field}" threw; rendering an empty cell`);
544
+ return void 0;
545
+ }
546
+ }
547
+ if (def.field == null) return void 0;
548
+ const raw = row[def.field];
549
+ return def.formulas && isFormula(raw) ? evaluateFormula(raw, row, /* @__PURE__ */ new Set([def.field])) : raw;
550
+ }
551
+
552
+ // src/interactions/filterRow.ts
553
+ var REGIONS2 = ["left", "center", "right"];
554
+ var NUM_OPS = [[">=", "gte"], ["<=", "lte"], ["<>", "ne"], ["!=", "ne"], [">", "gt"], ["<", "lt"], ["=", "eq"]];
555
+ var DATE_OPS = [[">", "after"], ["<", "before"], ["=", "equals"]];
556
+ var RANGE = /^(.+?)\s*(?:~|\.\.)\s*(.+)$/;
557
+ var toNumber = (s) => {
558
+ const n = Number(s.replace(/[,\s₩$€¥]/g, ""));
559
+ return s.trim() !== "" && Number.isFinite(n) ? n : null;
560
+ };
561
+ var toDateKey = (s) => {
562
+ const m = /^(\d{4})[-./]\s*(\d{1,2})[-./]\s*(\d{1,2})\.?$/.exec(s.trim());
563
+ return m ? `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}` : null;
564
+ };
565
+ function parseFilterText(type, text2) {
566
+ const t = text2.trim();
567
+ if (!t) return null;
568
+ if (type === "text") return { type: "text", op: "contains", value: t };
569
+ const range = RANGE.exec(t);
570
+ if (type === "number") {
571
+ if (range) {
572
+ const a = toNumber(range[1]), b = toNumber(range[2]);
573
+ return a === null || b === null ? void 0 : { type: "number", op: "between", value: Math.min(a, b), valueTo: Math.max(a, b) };
574
+ }
575
+ const op2 = NUM_OPS.find(([p]) => t.startsWith(p));
576
+ const n = toNumber(op2 ? t.slice(op2[0].length) : t);
577
+ return n === null ? void 0 : { type: "number", op: op2 ? op2[1] : "eq", value: n };
578
+ }
579
+ if (range) {
580
+ const a = toDateKey(range[1]), b = toDateKey(range[2]);
581
+ return !a || !b ? void 0 : { type: "date", op: "between", value: a < b ? a : b, valueTo: a < b ? b : a };
582
+ }
583
+ const op = DATE_OPS.find(([p]) => t.startsWith(p));
584
+ const d = toDateKey(op ? t.slice(op[0].length) : t);
585
+ return d ? { type: "date", op: op ? op[1] : "equals", value: d } : void 0;
586
+ }
587
+ function filterText(model) {
588
+ if (!model) return "";
589
+ if (model.type === "text") return model.op === "contains" ? model.value ?? "" : "";
590
+ if (model.type === "number") {
591
+ if (model.op === "between") return `${model.value}~${model.valueTo}`;
592
+ const sym = NUM_OPS.find(([, op]) => op === model.op)?.[0];
593
+ return sym && model.value !== void 0 ? `${sym === "=" ? "" : sym}${model.value}` : "";
594
+ }
595
+ if (model.type === "date") {
596
+ if (model.op === "between") return `${model.value}~${model.valueTo}`;
597
+ const sym = DATE_OPS.find(([, op]) => op === model.op)?.[0];
598
+ return sym && model.value ? `${sym === "=" ? "" : sym}${model.value}` : "";
599
+ }
600
+ return "";
601
+ }
602
+ var FilterRow = class {
603
+ constructor(grid, panes) {
604
+ this.grid = grid;
605
+ this.layout = "";
606
+ this.timers = /* @__PURE__ */ new Map();
607
+ this.data = null;
608
+ this.synced = null;
609
+ this.rebuilt = false;
610
+ this.inners = {};
611
+ for (const r of REGIONS2) {
612
+ this.inners[r] = el("div", "fg-filter-row", { role: "row" });
613
+ panes[r].append(this.inners[r]);
614
+ }
615
+ }
616
+ get height() {
617
+ return Math.round(this.grid.renderer.opts.rowHeight * 0.95);
618
+ }
619
+ render() {
620
+ const { columns, store } = this.grid;
621
+ const h = this.height;
622
+ const cols = columns.displayed();
623
+ if (store.state.data !== this.data) {
624
+ this.data = store.state.data;
625
+ this.layout = "";
626
+ }
627
+ const key = `${h}|${store.state.locale.code}|${cols.map((c) => `${c.id}:${c.left}:${c.width}:${c.region}`).join(",")}`;
628
+ if (key !== this.layout) {
629
+ this.layout = key;
630
+ for (const r of REGIONS2) {
631
+ const inner = this.inners[r];
632
+ inner.style.width = `${columns.regionWidth(r)}px`;
633
+ inner.style.height = `${h}px`;
634
+ inner.replaceChildren(...columns.region(r).map((c) => this.cell(c, h)));
635
+ }
636
+ this.rebuilt = true;
637
+ }
638
+ this.sync();
639
+ }
640
+ /** Show filters set elsewhere (menu, API) in the boxes that are not being typed in. */
641
+ sync() {
642
+ const filters = this.grid.store.state.filters;
643
+ if (filters === this.synced && !this.rebuilt) return;
644
+ this.synced = filters;
645
+ this.rebuilt = false;
646
+ for (const r of REGIONS2) {
647
+ for (const box of this.inners[r].querySelectorAll("[data-col]")) {
648
+ const colId = box.dataset.col;
649
+ const model = filters[colId];
650
+ box.closest(".fg-filter-cell").classList.toggle("fg-filter-active", !!model);
651
+ if (box === box.ownerDocument.activeElement || box.getRootNode().activeElement === box) continue;
652
+ if (box instanceof HTMLSelectElement) {
653
+ const v = model?.type === "set" && model.values.length === 1 ? JSON.stringify(model.values[0]) : "";
654
+ if ([...box.options].some((o) => o.value === v)) box.value = v;
655
+ } else box.value = filterText(model);
656
+ box.classList.remove("fg-filter-bad");
657
+ }
658
+ }
659
+ }
660
+ cell(c, h) {
661
+ const cell = el("div", "fg-filter-cell");
662
+ cell.style.left = `${c.left}px`;
663
+ cell.style.width = `${c.width}px`;
664
+ cell.style.height = `${h}px`;
665
+ const type = isSystemCol(c.id) ? null : filterTypeOf(c.def);
666
+ if (!type) return cell;
667
+ const loc = this.grid.store.state.locale;
668
+ const label = `${loc.filter}: ${c.def.header ?? c.def.field ?? c.id}`;
669
+ if (type === "set") {
670
+ const select = el("select", "fg-filter-input", { "aria-label": label });
671
+ select.dataset.col = c.id;
672
+ const all2 = el("option", "", { value: "" });
673
+ all2.textContent = loc.selectAll;
674
+ select.append(all2);
675
+ for (const v of this.distinct(c.def)) {
676
+ const o = el("option", "", { value: JSON.stringify(v) });
677
+ o.textContent = v === true ? "\u2713" : v === false ? "\u2717" : String(v);
678
+ select.append(o);
679
+ }
680
+ select.addEventListener("change", () => {
681
+ this.grid.setFilter(c.id, select.value ? { type: "set", values: [JSON.parse(select.value)] } : null);
682
+ });
683
+ cell.append(select);
684
+ return cell;
685
+ }
686
+ const input = el("input", "fg-filter-input", { type: "text", "aria-label": label, spellcheck: "false", autocomplete: "off" });
687
+ input.dataset.col = c.id;
688
+ if (type === "number") input.placeholder = ">0, 10~20";
689
+ if (type === "date") input.placeholder = ">2026-01-01";
690
+ const apply = () => {
691
+ const model = parseFilterText(type, input.value);
692
+ input.classList.toggle("fg-filter-bad", model === void 0);
693
+ if (model === void 0) return;
694
+ this.grid.setFilter(c.id, model);
695
+ };
696
+ input.addEventListener("input", () => {
697
+ clearTimeout(this.timers.get(c.id));
698
+ this.timers.set(c.id, window.setTimeout(apply, 200));
699
+ });
700
+ input.addEventListener("keydown", (e) => {
701
+ e.stopPropagation();
702
+ if (e.key === "Enter") {
703
+ clearTimeout(this.timers.get(c.id));
704
+ apply();
705
+ } else if (e.key === "Escape") {
706
+ input.value = "";
707
+ clearTimeout(this.timers.get(c.id));
708
+ apply();
709
+ }
710
+ });
711
+ cell.append(input);
712
+ return cell;
713
+ }
714
+ /** Values for a set box: distinct, sorted, at most 200. */
715
+ distinct(def) {
716
+ const seen2 = /* @__PURE__ */ new Map();
717
+ for (const row of this.grid.store.state.data) {
718
+ const v = getCellValue(def, row);
719
+ if (v === null || v === void 0 || v === "") continue;
720
+ const k = JSON.stringify(v);
721
+ if (!seen2.has(k)) seen2.set(k, v);
722
+ if (seen2.size >= 200) break;
723
+ }
724
+ return [...seen2.values()].sort((a, b) => String(a).localeCompare(String(b)));
725
+ }
726
+ setTransform(x) {
727
+ this.inners.center.style.transform = `translateX(${-x}px)`;
728
+ }
729
+ destroy() {
730
+ for (const t of this.timers.values()) clearTimeout(t);
731
+ for (const r of REGIONS2) this.inners[r].remove();
732
+ }
733
+ };
734
+
735
+ // src/rows/filters.ts
736
+ function isBlank(v) {
737
+ return v == null || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
738
+ }
739
+ var pad = (n) => String(n).padStart(2, "0");
740
+ function toDateKey2(v) {
741
+ if (isBlank(v)) return null;
742
+ if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
743
+ const d = v instanceof Date ? v : new Date(v);
744
+ if (Number.isNaN(d.getTime())) return null;
745
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
746
+ }
747
+ function setKey(v) {
748
+ if (isBlank(v)) return "";
749
+ if (v instanceof Date) return toDateKey2(v) ?? "";
750
+ return String(v);
751
+ }
752
+ var all = () => true;
753
+ function buildPredicate(model) {
754
+ switch (model.type) {
755
+ case "combined": {
756
+ const parts = (Array.isArray(model.conditions) ? model.conditions : []).map(buildPredicate);
757
+ if (!parts.length) return all;
758
+ return model.operator === "or" ? (v) => parts.some((p) => p(v)) : (v) => parts.every((p) => p(v));
759
+ }
760
+ case "text": {
761
+ const q = (model.value ?? "").toLowerCase();
762
+ const s = (v) => isBlank(v) ? "" : String(v).toLowerCase();
763
+ switch (model.op) {
764
+ case "contains":
765
+ return (v) => s(v).includes(q);
766
+ case "equals":
767
+ return (v) => s(v) === q;
768
+ case "notEquals":
769
+ return (v) => s(v) !== q;
770
+ case "startsWith":
771
+ return (v) => s(v).startsWith(q);
772
+ case "endsWith":
773
+ return (v) => s(v).endsWith(q);
774
+ case "blank":
775
+ return isBlank;
776
+ case "notBlank":
777
+ return (v) => !isBlank(v);
778
+ }
779
+ return all;
780
+ }
781
+ case "number": {
782
+ const n = (v) => isBlank(v) ? NaN : Number(v);
783
+ if (model.op === "blank") return (v) => Number.isNaN(n(v));
784
+ if (model.op === "notBlank") return (v) => !Number.isNaN(n(v));
785
+ const a = model.value;
786
+ if (a == null || Number.isNaN(a)) return all;
787
+ switch (model.op) {
788
+ case "eq":
789
+ return (v) => n(v) === a;
790
+ case "ne":
791
+ return (v) => n(v) !== a;
792
+ case "lt":
793
+ return (v) => n(v) < a;
794
+ case "lte":
795
+ return (v) => n(v) <= a;
796
+ case "gt":
797
+ return (v) => n(v) > a;
798
+ case "gte":
799
+ return (v) => n(v) >= a;
800
+ case "between": {
801
+ const b = model.valueTo;
802
+ if (b == null || Number.isNaN(b)) return all;
803
+ const lo = Math.min(a, b), hi = Math.max(a, b);
804
+ return (v) => {
805
+ const x = n(v);
806
+ return x >= lo && x <= hi;
807
+ };
808
+ }
809
+ }
810
+ return all;
811
+ }
812
+ case "date": {
813
+ if (model.op === "blank") return (v) => toDateKey2(v) === null;
814
+ if (model.op === "notBlank") return (v) => toDateKey2(v) !== null;
815
+ const a = model.value;
816
+ if (!a) return all;
817
+ const k = (v) => toDateKey2(v);
818
+ switch (model.op) {
819
+ case "equals":
820
+ return (v) => k(v) === a;
821
+ case "before":
822
+ return (v) => {
823
+ const x = k(v);
824
+ return x !== null && x < a;
825
+ };
826
+ case "after":
827
+ return (v) => {
828
+ const x = k(v);
829
+ return x !== null && x > a;
830
+ };
831
+ case "between": {
832
+ const b = model.valueTo;
833
+ if (!b) return all;
834
+ const lo = a < b ? a : b, hi = a < b ? b : a;
835
+ return (v) => {
836
+ const x = k(v);
837
+ return x !== null && x >= lo && x <= hi;
838
+ };
839
+ }
840
+ }
841
+ return all;
842
+ }
843
+ case "set": {
844
+ const allowed = new Set(model.values.map(setKey));
845
+ return (v) => allowed.has(setKey(v));
676
846
  }
677
- if (/^(true|false)$/i.test(name)) return name.toLowerCase() === "true";
678
- return this.resolve(name);
679
847
  }
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!";
848
+ return all;
849
+ }
850
+ function quickTokens(q) {
851
+ const t = q.trim().toLowerCase();
852
+ return t ? t.split(/\s+/) : [];
853
+ }
854
+ function matchesQuick(tokens, text2) {
855
+ return tokens.every((t) => text2.includes(t));
856
+ }
857
+ function buildAdvanced(f, columnDef, valueOf) {
858
+ const parts = [];
859
+ for (const c of Array.isArray(f?.conditions) ? f.conditions : []) {
860
+ if (!c || typeof c !== "object") continue;
861
+ if ("conditions" in c) {
862
+ const sub = buildAdvanced(c, columnDef, valueOf);
863
+ if (sub) parts.push(sub);
864
+ continue;
865
+ }
866
+ const def = columnDef(c.colId);
867
+ if (!def || !c.model) continue;
868
+ const pred = buildPredicate(c.model);
869
+ parts.push((row) => pred(valueOf(def, row)));
700
870
  }
871
+ if (!parts.length) return null;
872
+ return f.operator === "or" ? (row) => parts.some((p) => p(row)) : (row) => parts.every((p) => p(row));
701
873
  }
702
- var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
703
874
 
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;
875
+ // src/i18n/format.ts
876
+ var cache = /* @__PURE__ */ new Map();
877
+ function parseNumberPattern(p) {
878
+ if (p === "integer") return { maximumFractionDigits: 0 };
879
+ if (p === "percent") return { style: "percent" };
880
+ if (p.startsWith("currency:")) return { style: "currency", currency: p.slice(9) };
881
+ if (!/^[#0,]*(\.[#0]+)?%?$/.test(p) || !/[#0]/.test(p)) return null;
882
+ const pct = p.endsWith("%");
883
+ const body = pct ? p.slice(0, -1) : p;
884
+ const frac = body.includes(".") ? body.split(".")[1] : "";
885
+ return {
886
+ useGrouping: body.includes(","),
887
+ minimumFractionDigits: (frac.match(/0/g) ?? []).length,
888
+ maximumFractionDigits: frac.length,
889
+ ...pct ? { style: "percent" } : {}
890
+ };
891
+ }
892
+ var DATE_PRESETS = {
893
+ date: { year: "numeric", month: "2-digit", day: "2-digit" },
894
+ datetime: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" },
895
+ time: { hour: "2-digit", minute: "2-digit" },
896
+ short: { dateStyle: "short" },
897
+ medium: { dateStyle: "medium" },
898
+ long: { dateStyle: "long" }
899
+ };
900
+ function toDate(v) {
901
+ if (isBlank(v)) return null;
902
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
903
+ if (typeof v === "string") {
904
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
905
+ if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
906
+ }
907
+ if (typeof v !== "string" && typeof v !== "number") return null;
908
+ const d = new Date(v);
909
+ return Number.isNaN(d.getTime()) ? null : d;
910
+ }
911
+ function create(type, format, locale) {
912
+ switch (type) {
913
+ case "number": {
914
+ let opts = {};
915
+ if (typeof format === "string") {
916
+ const parsed = parseNumberPattern(format);
917
+ if (!parsed) warn(`unknown number format "${format}"`);
918
+ opts = parsed ?? {};
919
+ } else if (format) opts = format;
920
+ const nf = new Intl.NumberFormat(locale, opts);
921
+ return (v) => {
922
+ if (isBlank(v)) return "";
923
+ const n = typeof v === "number" ? v : Number(v);
924
+ return Number.isNaN(n) ? String(v) : nf.format(n);
925
+ };
926
+ }
927
+ case "date": {
928
+ if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
929
+ const pad3 = (n) => String(n).padStart(2, "0");
930
+ const withTime = format === "datetime";
931
+ return (v) => {
932
+ const d = toDate(v);
933
+ if (!d) return isBlank(v) ? "" : String(v);
934
+ const day = `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}`;
935
+ return withTime ? `${day} ${pad3(d.getHours())}:${pad3(d.getMinutes())}` : day;
936
+ };
937
+ }
938
+ let opts = DATE_PRESETS.date;
939
+ if (typeof format === "string") {
940
+ if (!DATE_PRESETS[format]) warn(`unknown date format "${format}"`);
941
+ opts = DATE_PRESETS[format] ?? DATE_PRESETS.date;
942
+ } else if (format) opts = format;
943
+ const df = new Intl.DateTimeFormat(locale, opts);
944
+ return (v) => {
945
+ const d = toDate(v);
946
+ return d ? df.format(d) : isBlank(v) ? "" : String(v);
947
+ };
712
948
  }
949
+ case "boolean":
950
+ return (v) => v === true ? "\u2713" : v === false ? "\u2717" : "";
951
+ default:
952
+ return (v) => isBlank(v) ? "" : v instanceof Date ? v.toISOString() : String(v);
713
953
  }
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;
954
+ }
955
+ function getFormatter(def, locale) {
956
+ if (def.sparkline) return (v) => Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v);
957
+ const f = def.format;
958
+ const key = `${locale}|${def.type ?? "text"}|${typeof f === "string" ? f : JSON.stringify(f ?? null)}`;
959
+ let fmt = cache.get(key);
960
+ if (!fmt) {
961
+ fmt = create(def.type, f, locale);
962
+ cache.set(key, fmt);
963
+ }
964
+ return fmt;
717
965
  }
718
966
 
719
967
  // src/summary.ts
@@ -871,7 +1119,7 @@ function rowRangeByOffsets(offsets, scrollTop, height, count, overscan = 4) {
871
1119
  }
872
1120
 
873
1121
  // src/render/renderer.ts
874
- var REGIONS2 = ["left", "center", "right"];
1122
+ var REGIONS3 = ["left", "center", "right"];
875
1123
  var ROW_OVERSCAN = 6;
876
1124
  var MAX_COLSPAN_REACH = 8;
877
1125
  var COL_OVERSCAN = 2;
@@ -888,6 +1136,8 @@ var Renderer = class {
888
1136
  constructor(host, grid, opts) {
889
1137
  this.grid = grid;
890
1138
  this.opts = opts;
1139
+ /** Boxes under the header (filterRow option). */
1140
+ this.filterRow = null;
891
1141
  this.activeCellId = "";
892
1142
  this.idBase = `fg${Math.random().toString(36).slice(2, 8)}`;
893
1143
  this.painted = { left: /* @__PURE__ */ new Map(), center: /* @__PURE__ */ new Map(), right: /* @__PURE__ */ new Map() };
@@ -935,6 +1185,8 @@ var Renderer = class {
935
1185
  e.preventDefault();
936
1186
  this.scroller.scrollTop += e.deltaY;
937
1187
  };
1188
+ this.overlayKey = "";
1189
+ this.heightFn = null;
938
1190
  this.onCardClick = (e) => {
939
1191
  const target = e.target;
940
1192
  const card = target.closest(".fg-card");
@@ -964,10 +1216,10 @@ var Renderer = class {
964
1216
  });
965
1217
  this.headerPanes = panes("fg-header-pane");
966
1218
  this.headerInners = panes("fg-header-inner");
967
- for (const r of REGIONS2) this.headerInners[r].setAttribute("role", "row");
1219
+ for (const r of REGIONS3) this.headerInners[r].setAttribute("role", "row");
968
1220
  this.bodyPanes = panes("fg-body-pane");
969
1221
  this.canvases = panes("fg-canvas");
970
- for (const r of REGIONS2) {
1222
+ for (const r of REGIONS3) {
971
1223
  this.headerPanes[r].dataset.region = r;
972
1224
  this.bodyPanes[r].dataset.region = r;
973
1225
  this.headerPanes[r].append(this.headerInners[r]);
@@ -976,6 +1228,7 @@ var Renderer = class {
976
1228
  this.bodyEl.append(this.bodyPanes[r]);
977
1229
  }
978
1230
  this.scroller = this.bodyPanes.center;
1231
+ if (grid.options.filterRow) this.filterRow = new FilterRow(grid, this.headerPanes);
979
1232
  this.overlay = el("div", "fg-overlay", { "aria-live": "polite" });
980
1233
  this.main = el("div", "fg-main");
981
1234
  this.gridEl.append(this.headerEl, this.bodyEl);
@@ -1008,7 +1261,7 @@ var Renderer = class {
1008
1261
  }
1009
1262
  applyTheme() {
1010
1263
  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" : ""}`;
1264
+ 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
1265
  this.root.setAttribute("dir", rtl ? "rtl" : "ltr");
1013
1266
  }
1014
1267
  /** Right-to-left layout: the main area is mirrored, so screen deltas along x flip sign. */
@@ -1037,7 +1290,7 @@ var Renderer = class {
1037
1290
  }
1038
1291
  const count = rowModel.getRowCount();
1039
1292
  const auto = !!this.grid.options.autoRowHeight && !rowModel.server;
1040
- const lay = rowModel.layout(rh, this.detailHeight(), auto ? this.rowHeights : void 0, this.heightsVersion);
1293
+ const lay = rowModel.layout(rh, this.detailHeight(), auto ? this.rowHeights : void 0, this.heightsVersion, rowModel.server ? void 0 : this.heightOf);
1041
1294
  this.offsets = lay.uniform ? null : lay.offsets;
1042
1295
  this.firstDataId = columns.displayed().find((c) => !isSystemCol(c.id))?.id ?? "";
1043
1296
  this.groupLabelId = columns.region("center").find((c) => !isSystemCol(c.id))?.id ?? this.firstDataId;
@@ -1046,8 +1299,7 @@ var Renderer = class {
1046
1299
  this.renderStatusBar(rowModel.dataCount());
1047
1300
  this.renderPager();
1048
1301
  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;
1302
+ this.paintOverlay(count);
1051
1303
  return;
1052
1304
  }
1053
1305
  const summary = this.grid.summary();
@@ -1057,9 +1309,9 @@ var Renderer = class {
1057
1309
  this.topOffset = summary && !sumPinned && sumPos === "top" ? sumRows * rh : 0;
1058
1310
  const bottomExtra = summary && !sumPinned && sumPos === "bottom" ? sumRows * rh : 0;
1059
1311
  this.syncSummaryPane(summary && sumPinned ? sumPos : null, sumRows * rh);
1060
- const headerHeight = columns.depth * hh;
1312
+ const headerHeight = columns.depth * hh + (this.filterRow?.height ?? 0);
1061
1313
  this.headerEl.style.height = `${headerHeight}px`;
1062
- for (const r of REGIONS2) {
1314
+ for (const r of REGIONS3) {
1063
1315
  const w = columns.regionWidth(r);
1064
1316
  if (r !== "center") {
1065
1317
  this.headerPanes[r].style.width = this.bodyPanes[r].style.width = `${w}px`;
@@ -1068,17 +1320,22 @@ var Renderer = class {
1068
1320
  this.canvases[r].style.width = `${w}px`;
1069
1321
  this.canvases[r].style.height = `${this.rowsHeight(count) + this.topOffset + bottomExtra}px`;
1070
1322
  }
1323
+ if (options.autoHeight && !this.useCards()) {
1324
+ this.root.classList.add("fg-auto-height");
1325
+ this.bodyEl.style.height = `${this.rowsHeight(count) + this.topOffset + bottomExtra + Math.max(0, this.scroller.offsetHeight - this.scroller.clientHeight)}px`;
1326
+ }
1071
1327
  const hScrollbar = this.scroller.offsetHeight - this.scroller.clientHeight;
1072
1328
  this.bodyPanes.left.style.marginBottom = this.bodyPanes.right.style.marginBottom = `${hScrollbar}px`;
1073
1329
  const g = this.gridEl;
1074
1330
  g.setAttribute("role", options.treeData || this.grid.getGroupBy().length ? "treegrid" : "grid");
1075
- g.setAttribute("aria-rowcount", String(count + columns.depth));
1331
+ g.setAttribute("aria-rowcount", String(count + columns.depth + (this.filterRow ? 1 : 0)));
1076
1332
  g.setAttribute("aria-colcount", String(columns.displayed().length));
1077
1333
  if (options.rowSelection === "multiple") g.setAttribute("aria-multiselectable", "true");
1078
1334
  if (this.headerDirty) {
1079
1335
  this.headerDirty = false;
1080
1336
  this.renderHeader();
1081
1337
  }
1338
+ this.filterRow?.render();
1082
1339
  this.renderRows(count);
1083
1340
  this.updateActiveCell();
1084
1341
  if (auto) this.measureRows();
@@ -1088,12 +1345,12 @@ var Renderer = class {
1088
1345
  this.renderStatusBar(rowModel.dataCount());
1089
1346
  this.renderPager();
1090
1347
  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;
1348
+ this.paintOverlay(count);
1093
1349
  this.overlay.style.top = `${headerHeight}px`;
1094
1350
  const { scrollTop, scrollLeft } = this.scroller;
1095
1351
  this.bodyPanes.left.scrollTop = this.bodyPanes.right.scrollTop = scrollTop;
1096
1352
  this.headerInners.center.style.transform = `translateX(${-scrollLeft}px)`;
1353
+ this.filterRow?.setTransform(scrollLeft);
1097
1354
  if (this.summaryInners) this.summaryInners.center.style.transform = `translateX(${-scrollLeft}px)`;
1098
1355
  for (const s of [this.pinnedTop, this.pinnedBottom]) if (s) s.inners.center.style.transform = `translateX(${-scrollLeft}px)`;
1099
1356
  this.grid.fillHandle.update();
@@ -1120,6 +1377,37 @@ var Renderer = class {
1120
1377
  ref = b;
1121
1378
  }
1122
1379
  }
1380
+ /** "No rows" / "Loading" over the body, or the noRowsOverlay / loadingOverlay given in the options. */
1381
+ paintOverlay(count) {
1382
+ const { grid } = this;
1383
+ const { store, rowModel, options } = grid;
1384
+ const kind = grid.forcedOverlay ?? (count > 0 ? null : rowModel.loading() ? "loading" : "noRows");
1385
+ this.overlay.hidden = !kind;
1386
+ if (!kind) {
1387
+ this.overlayKey = "";
1388
+ return;
1389
+ }
1390
+ const filtered = !rowModel.server && store.state.data.length > 0;
1391
+ const custom = kind === "loading" ? options.loadingOverlay : options.noRowsOverlay;
1392
+ const key = `${kind}|${filtered}|${store.state.locale.code}`;
1393
+ if (key === this.overlayKey) return;
1394
+ this.overlayKey = key;
1395
+ this.overlay.classList.toggle("fg-overlay-custom", typeof custom === "function");
1396
+ if (custom === void 0 || custom === null) {
1397
+ this.overlay.textContent = kind === "loading" ? store.state.locale.loading : store.state.locale.noRows;
1398
+ } else if (typeof custom === "string") {
1399
+ this.overlay.textContent = custom;
1400
+ } else {
1401
+ try {
1402
+ const out = custom({ filtered });
1403
+ if (typeof out === "string") this.overlay.textContent = out;
1404
+ else this.overlay.replaceChildren(out);
1405
+ } catch {
1406
+ warn(`${kind === "loading" ? "loadingOverlay" : "noRowsOverlay"} threw`);
1407
+ this.overlay.textContent = kind === "loading" ? store.state.locale.loading : store.state.locale.noRows;
1408
+ }
1409
+ }
1410
+ }
1123
1411
  /** Point aria-activedescendant at the focused cell so screen readers announce it. */
1124
1412
  updateActiveCell() {
1125
1413
  const prev = this.activeCellId ? this.root.getRootNode().getElementById?.(this.activeCellId) ?? null : null;
@@ -1145,7 +1433,7 @@ var Renderer = class {
1145
1433
  s.size(columns, rows.length * rh, this.scroller.offsetWidth - this.scroller.clientWidth);
1146
1434
  const center = columns.region("center");
1147
1435
  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) {
1436
+ for (const r of REGIONS3) {
1149
1437
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
1150
1438
  s.inners[r].replaceChildren(...rows.map((data, k) => {
1151
1439
  const rowEl = el("div", "fg-row fg-row-pinned", { role: "row" });
@@ -1195,7 +1483,7 @@ var Renderer = class {
1195
1483
  renderHeader() {
1196
1484
  const { columns, store } = this.grid;
1197
1485
  const { sort, filters, locale } = store.state;
1198
- for (const r of REGIONS2) {
1486
+ for (const r of REGIONS3) {
1199
1487
  const inner = this.headerInners[r];
1200
1488
  inner.style.width = `${columns.regionWidth(r)}px`;
1201
1489
  inner.style.height = `${columns.depth * this.opts.headerHeight}px`;
@@ -1211,6 +1499,10 @@ var Renderer = class {
1211
1499
  cell.style.width = `${c.width}px`;
1212
1500
  cell.style.height = `${c.rowSpan * hh}px`;
1213
1501
  if (isSystemCol(c.id)) {
1502
+ if (c.id === ROWNUM_COL_ID) {
1503
+ cell.classList.add("fg-header-rownum");
1504
+ cell.textContent = c.label;
1505
+ }
1214
1506
  if (c.id === SELECT_COL_ID) {
1215
1507
  cell.classList.add("fg-header-select");
1216
1508
  cell.append(this.selectAllBox(locale));
@@ -1230,6 +1522,23 @@ var Renderer = class {
1230
1522
  label.textContent = c.label;
1231
1523
  }
1232
1524
  }
1525
+ let toggleEl = null;
1526
+ if (c.isGroup && this.grid.columns.isCollapsible(c.node)) {
1527
+ const open = c.node.open;
1528
+ const t = el("button", open ? "fg-colgroup-toggle fg-expanded" : "fg-colgroup-toggle", {
1529
+ type: "button",
1530
+ tabindex: "-1",
1531
+ "aria-expanded": String(open),
1532
+ "aria-label": open ? locale.collapseAll : locale.expandAll
1533
+ });
1534
+ t.textContent = open ? "\u2212" : "+";
1535
+ t.addEventListener("click", (e) => {
1536
+ e.stopPropagation();
1537
+ this.grid.setColumnGroupOpened(c.id, !open);
1538
+ });
1539
+ cell.classList.add("fg-header-collapsible");
1540
+ toggleEl = t;
1541
+ }
1233
1542
  if (hdef.headerClass) {
1234
1543
  try {
1235
1544
  const extra = typeof hdef.headerClass === "function" ? hdef.headerClass(hdef) : hdef.headerClass;
@@ -1239,6 +1548,7 @@ var Renderer = class {
1239
1548
  }
1240
1549
  }
1241
1550
  cell.append(label);
1551
+ if (toggleEl) cell.append(toggleEl);
1242
1552
  if (c.isGroup || !c.column) return cell;
1243
1553
  const def = c.node.def;
1244
1554
  if (def.wrapHeaderText ?? this.grid.options.wrapHeaderText) cell.classList.add("fg-header-wrap");
@@ -1283,12 +1593,12 @@ var Renderer = class {
1283
1593
  const allBounds = this.grid.allRangeBounds();
1284
1594
  const spans = this.grid.mergeSpans();
1285
1595
  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);
1596
+ 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
1597
  this.lastRange = range;
1288
1598
  const center = columns.region("center");
1289
1599
  const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
1290
1600
  if (center.some((c) => c.def.colSpan)) cr.first = Math.max(0, cr.first - MAX_COLSPAN_REACH);
1291
- for (const r of REGIONS2) {
1601
+ for (const r of REGIONS3) {
1292
1602
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
1293
1603
  const map = this.painted[r];
1294
1604
  for (const [i, row] of map) {
@@ -1307,7 +1617,7 @@ var Renderer = class {
1307
1617
  const inRange = hits.map((b) => `${b.c1}-${b.c2}`).join(",");
1308
1618
  const merge2 = this.mergeState(spans, cols, i, range.first);
1309
1619
  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}`;
1620
+ const key = `${node.id}|${this.version}|${colKey}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
1311
1621
  let row = map.get(i);
1312
1622
  if (!row) {
1313
1623
  const rowEl = this.pool[r].pop() ?? el("div", "fg-row", { role: "row" });
@@ -1342,7 +1652,7 @@ var Renderer = class {
1342
1652
  }
1343
1653
  paintRow(rowEl, node, i, cols, selected, focusCol, bounds, spans = /* @__PURE__ */ new Map(), first = 0) {
1344
1654
  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" : ""}`;
1655
+ 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
1656
  this.placeRow(rowEl, i);
1347
1657
  if (this.grid.options.rowSelection) rowEl.setAttribute("aria-selected", String(selected));
1348
1658
  if (node.level !== void 0) rowEl.setAttribute("aria-level", String(node.level + 1));
@@ -1396,6 +1706,12 @@ var Renderer = class {
1396
1706
  cell.dataset.col = col.id;
1397
1707
  cell.setAttribute("aria-colindex", String(col.index + 1));
1398
1708
  let cls = focused ? "fg-cell fg-cell-focused" : "fg-cell";
1709
+ if (col.id === ROWNUM_COL_ID) {
1710
+ cell.className = `${cls} fg-cell-rownum`;
1711
+ const n = i >= 0 && !node.kind ? this.grid.rowNumber(i) : null;
1712
+ cell.textContent = n === null ? "" : String(n);
1713
+ return;
1714
+ }
1399
1715
  if (col.id === DRAG_COL_ID) {
1400
1716
  cell.className = `${cls} fg-cell-drag`;
1401
1717
  const handle = el("span", this.grid.canMoveRows() ? "fg-drag-handle" : "fg-drag-handle fg-drag-disabled", { "aria-hidden": "true" });
@@ -1431,7 +1747,15 @@ var Renderer = class {
1431
1747
  if (def.wrapText) cls += " fg-cell-wrap";
1432
1748
  if (typeof def.editable === "function" && i >= 0 && !cellEditable(def, node.data, i)) cls += " fg-cell-readonly";
1433
1749
  if (i >= 0) cls += this.grid.finder.classFor(i, col.id);
1750
+ if (this.grid.isFlashing(node.id, col.id)) cls += " fg-cell-flash";
1434
1751
  cell.className = cls;
1752
+ if (def.sparkline && !def.renderer) {
1753
+ const nums = Array.isArray(value) ? value.map(Number).filter(Number.isFinite) : [];
1754
+ if (nums.length) cell.replaceChildren(sparkline(nums, def.sparkline === true ? {} : def.sparkline, col.width, this.opts.rowHeight));
1755
+ else cell.replaceChildren();
1756
+ cell.setAttribute("aria-label", Array.isArray(value) ? value.join(", ") : "");
1757
+ return;
1758
+ }
1435
1759
  if (!def.renderer) {
1436
1760
  cell.textContent = formatted;
1437
1761
  return;
@@ -1479,14 +1803,14 @@ var Renderer = class {
1479
1803
  this.summaryEl = el("div", "fg-summary", { role: "rowgroup" });
1480
1804
  this.summaryPanes = mk("fg-summary-pane");
1481
1805
  this.summaryInners = mk("fg-summary-inner");
1482
- for (const r of REGIONS2) {
1806
+ for (const r of REGIONS3) {
1483
1807
  this.summaryPanes[r].append(this.summaryInners[r]);
1484
1808
  this.summaryEl.append(this.summaryPanes[r]);
1485
1809
  }
1486
1810
  }
1487
1811
  this.summaryEl.className = `fg-summary fg-summary-${position}`;
1488
1812
  this.summaryEl.style.height = `${height}px`;
1489
- for (const r of REGIONS2) {
1813
+ for (const r of REGIONS3) {
1490
1814
  const w = this.grid.columns.regionWidth(r);
1491
1815
  if (r !== "center") {
1492
1816
  this.summaryPanes[r].style.width = `${w}px`;
@@ -1499,7 +1823,7 @@ var Renderer = class {
1499
1823
  this.summaryPanes.center.style.marginRight = `${vScrollbar}px`;
1500
1824
  }
1501
1825
  renderSummary(count, defs, values, pinned, position) {
1502
- for (const r of REGIONS2) {
1826
+ for (const r of REGIONS3) {
1503
1827
  for (const old of this.canvasSummary[r]) old.remove();
1504
1828
  this.canvasSummary[r] = [];
1505
1829
  }
@@ -1509,7 +1833,7 @@ var Renderer = class {
1509
1833
  const center = columns.region("center");
1510
1834
  const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
1511
1835
  const dataCols = columns.displayed().filter((c) => !isSystemCol(c.id));
1512
- for (const r of REGIONS2) {
1836
+ for (const r of REGIONS3) {
1513
1837
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
1514
1838
  const rowsEls = defs.map((def, k) => {
1515
1839
  const labelCol = def.label != null ? dataCols.find((c) => !(c.id in (def.columns ?? {})))?.id ?? null : null;
@@ -1557,8 +1881,21 @@ var Renderer = class {
1557
1881
  cell.textContent = s.prefix && text2 ? `${s.prefix} ${text2}` : text2;
1558
1882
  return cell;
1559
1883
  }
1884
+ /** getRowHeight, guarded: a throwing function means the default height. */
1885
+ get heightOf() {
1886
+ const fn = this.grid.options.getRowHeight;
1887
+ if (!fn) return void 0;
1888
+ return this.heightFn ?? (this.heightFn = (row) => {
1889
+ try {
1890
+ return fn(row) ?? void 0;
1891
+ } catch {
1892
+ return void 0;
1893
+ }
1894
+ });
1895
+ }
1560
1896
  /** Forget measured row heights (data, widths or fonts changed); rows are measured again on screen. */
1561
1897
  invalidateHeights() {
1898
+ if (this.heightOf) this.heightsVersion++;
1562
1899
  if (!this.rowHeights.size) return;
1563
1900
  this.rowHeights.clear();
1564
1901
  this.heightsVersion++;
@@ -1567,7 +1904,7 @@ var Renderer = class {
1567
1904
  measureRows() {
1568
1905
  const base = this.opts.rowHeight;
1569
1906
  const need = /* @__PURE__ */ new Map();
1570
- for (const r of REGIONS2) {
1907
+ for (const r of REGIONS3) {
1571
1908
  for (const [i, row] of this.painted[r]) {
1572
1909
  const node = this.grid.rowModel.getRow(i);
1573
1910
  if (!node || node.kind) continue;
@@ -1613,7 +1950,7 @@ var Renderer = class {
1613
1950
  rowEl.style.transform = `translateY(${this.rowTop(i)}px)`;
1614
1951
  rowEl.style.height = `${this.rowH(i)}px`;
1615
1952
  rowEl.dataset.row = String(i);
1616
- rowEl.setAttribute("aria-rowindex", String(i + 1 + this.grid.columns.depth));
1953
+ rowEl.setAttribute("aria-rowindex", String(i + 1 + this.grid.columns.depth + (this.filterRow ? 1 : 0)));
1617
1954
  }
1618
1955
  /** Tree indentation and chevron, detail toggle, group-leaf indentation in the first data column. */
1619
1956
  decorate(cell, node) {
@@ -1673,6 +2010,19 @@ var Renderer = class {
1673
2010
  cell.dataset.col = col.id;
1674
2011
  cell.setAttribute("aria-colindex", String(col.index + 1));
1675
2012
  const focused = focusCol === col.id ? " fg-cell-focused" : "";
2013
+ if (col.id === SELECT_COL_ID) {
2014
+ cell.className = `fg-cell fg-cell-select${focused}`;
2015
+ if (grid.options.rowSelection !== "multiple") {
2016
+ cell.replaceChildren();
2017
+ return;
2018
+ }
2019
+ const state = grid.groupSelection(node);
2020
+ const box = el("input", "fg-checkbox", { type: "checkbox", tabindex: "-1", "aria-label": loc.selectAllRows });
2021
+ box.checked = state === "all";
2022
+ box.indeterminate = state === "some";
2023
+ cell.replaceChildren(box);
2024
+ return;
2025
+ }
1676
2026
  if (col.id === this.groupLabelId) {
1677
2027
  cell.className = `fg-cell fg-group-cell${focused}`;
1678
2028
  cell.style.setProperty("--fg-level", String(node.level ?? 0));
@@ -1687,7 +2037,8 @@ var Renderer = class {
1687
2037
  const label = el("span", "fg-group-label");
1688
2038
  const value2 = isBlank(g.value) ? loc.blanks : def2 ? getFormatter(def2, loc.code)(g.value) : String(g.value);
1689
2039
  const nf = new Intl.NumberFormat(loc.code);
1690
- label.textContent = `${def2?.header ?? def2?.field ?? g.colId}: ${value2} (${nf.format(g.rows.length)})`;
2040
+ const count = g.count ?? (g.aggregates ? null : g.rows.length);
2041
+ label.textContent = `${def2?.header ?? def2?.field ?? g.colId}: ${value2}${count == null ? "" : ` (${nf.format(count)})`}`;
1691
2042
  cell.replaceChildren(toggle, label);
1692
2043
  return;
1693
2044
  }
@@ -1936,6 +2287,7 @@ var Renderer = class {
1936
2287
  return Math.max(1, Math.floor(this.scroller.clientHeight / this.opts.rowHeight));
1937
2288
  }
1938
2289
  destroy() {
2290
+ this.filterRow?.destroy();
1939
2291
  if (this.frame) cancelAnimationFrame(this.frame);
1940
2292
  this.scroller.removeEventListener("scroll", this.onScroll);
1941
2293
  this.root.remove();
@@ -1951,14 +2303,14 @@ var Section = class {
1951
2303
  this.el = el("div", cls, { role: "rowgroup" });
1952
2304
  this.panes = mk("fg-section-pane");
1953
2305
  this.inners = mk("fg-section-inner");
1954
- for (const r of REGIONS2) {
2306
+ for (const r of REGIONS3) {
1955
2307
  this.panes[r].append(this.inners[r]);
1956
2308
  this.el.append(this.panes[r]);
1957
2309
  }
1958
2310
  }
1959
2311
  size(columns, height, vScrollbar) {
1960
2312
  this.el.style.height = `${height}px`;
1961
- for (const r of REGIONS2) {
2313
+ for (const r of REGIONS3) {
1962
2314
  const w = columns.regionWidth(r);
1963
2315
  if (r !== "center") {
1964
2316
  this.panes[r].style.width = `${w}px`;
@@ -1970,6 +2322,44 @@ var Section = class {
1970
2322
  this.panes.center.style.marginRight = `${vScrollbar}px`;
1971
2323
  }
1972
2324
  };
2325
+ var SVG_NS = "http://www.w3.org/2000/svg";
2326
+ function sparkline(nums, opt, width, rowHeight) {
2327
+ const w = Math.max(20, width - 16), h = Math.max(10, rowHeight - 14);
2328
+ const svgEl = document.createElementNS(SVG_NS, "svg");
2329
+ svgEl.setAttribute("class", "fg-sparkline");
2330
+ svgEl.setAttribute("width", String(w));
2331
+ svgEl.setAttribute("height", String(h));
2332
+ svgEl.setAttribute("viewBox", `0 0 ${w} ${h}`);
2333
+ svgEl.setAttribute("aria-hidden", "true");
2334
+ const color = opt.color ?? "var(--fg-accent)";
2335
+ const bars = opt.type === "bar";
2336
+ let min = bars ? Math.min(0, ...nums) : Math.min(...nums);
2337
+ let max = bars ? Math.max(0, ...nums) : Math.max(...nums);
2338
+ if (min === max) {
2339
+ min -= 1;
2340
+ max += 1;
2341
+ }
2342
+ const span = max - min;
2343
+ const y = (v) => h - 1 - (v - min) / span * (h - 2);
2344
+ const mk = (tag, attrs) => {
2345
+ const n = document.createElementNS(SVG_NS, tag);
2346
+ for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, String(v));
2347
+ svgEl.append(n);
2348
+ return n;
2349
+ };
2350
+ if (opt.type === "bar") {
2351
+ const bw = w / nums.length;
2352
+ 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 }));
2353
+ return svgEl;
2354
+ }
2355
+ const step = nums.length > 1 ? (w - 2) / (nums.length - 1) : 0;
2356
+ const pts = nums.map((v, i) => `${1 + i * step},${y(v)}`).join(" ");
2357
+ if (opt.type === "area") mk("polygon", { points: `1,${h} ${pts} ${1 + (nums.length - 1) * step},${h}`, fill: color, "fill-opacity": 0.2 });
2358
+ mk("polyline", { points: pts, fill: "none", stroke: color, "stroke-width": 1.5, "stroke-linejoin": "round" });
2359
+ const last = nums.length - 1;
2360
+ mk("circle", { cx: 1 + last * step, cy: y(nums[last]), r: 2, fill: color });
2361
+ return svgEl;
2362
+ }
1973
2363
 
1974
2364
  // src/interactions/groupPanel.ts
1975
2365
  var DRAG_THRESHOLD = 4;
@@ -2119,11 +2509,16 @@ var en = {
2119
2509
  hideColumn: "Hide column",
2120
2510
  close: "Close",
2121
2511
  copy: "Copy",
2512
+ cut: "Cut",
2122
2513
  copyWithHeaders: "Copy with headers",
2123
2514
  paste: "Paste",
2124
2515
  pasteHint: "Clipboard access was blocked. Press Ctrl+V to paste.",
2125
2516
  required: "This field is required",
2126
2517
  invalidValue: "Invalid value",
2518
+ and: "And",
2519
+ or: "Or",
2520
+ advancedFilter: "Advanced filter",
2521
+ addCondition: "Add condition",
2127
2522
  duplicate: "This value is already used",
2128
2523
  undo: "Undo",
2129
2524
  redo: "Redo",
@@ -2171,6 +2566,9 @@ var en = {
2171
2566
  chartLine: "Line chart",
2172
2567
  chartArea: "Area chart",
2173
2568
  chartPie: "Pie chart",
2569
+ chartDonut: "Donut chart",
2570
+ chartCombo: "Bar and line chart",
2571
+ chartScatter: "Scatter chart",
2174
2572
  freeBadge: "Free version",
2175
2573
  ops: {
2176
2574
  contains: "Contains",
@@ -2202,7 +2600,11 @@ function matchCode(tag) {
2202
2600
  const lower = tag.toLowerCase();
2203
2601
  for (const k of registry.keys()) if (k.toLowerCase() === lower) return k;
2204
2602
  const lang = lower.split("-")[0];
2205
- if (lang === "zh" && registry.has("zh-CN")) return "zh-CN";
2603
+ if (lang === "zh") {
2604
+ const traditional = /-(tw|hk|mo|hant)(-|$)/.test(lower);
2605
+ if (traditional && registry.has("zh-TW")) return "zh-TW";
2606
+ if (registry.has("zh-CN")) return "zh-CN";
2607
+ }
2206
2608
  for (const k of registry.keys()) if (k.toLowerCase().split("-")[0] === lang) return k;
2207
2609
  return void 0;
2208
2610
  }
@@ -2344,6 +2746,9 @@ function attachBody(grid) {
2344
2746
  const before = grid.store.state.focus;
2345
2747
  const hit = renderer.cellAt(e.target);
2346
2748
  if (hit && grid.rowModel.getRow(hit.rowIndex)?.kind) {
2749
+ if (hit.colId === SELECT_COL_ID && grid.rowModel.getRow(hit.rowIndex).kind === "group") {
2750
+ grid.toggleGroupSelection(grid.rowModel.getRow(hit.rowIndex));
2751
+ }
2347
2752
  renderer.gridEl.focus({ preventScroll: true });
2348
2753
  grid.setFocus(hit.rowIndex, hit.colId);
2349
2754
  grid.setRange(null);
@@ -2352,6 +2757,14 @@ function attachBody(grid) {
2352
2757
  const ev = toEvent(e);
2353
2758
  if (!ev) return;
2354
2759
  renderer.gridEl.focus({ preventScroll: true });
2760
+ if (ev.colId === ROWNUM_COL_ID) {
2761
+ const data = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
2762
+ if (data.length) {
2763
+ grid.setFocus(ev.rowIndex, data[0].id);
2764
+ grid.setRange({ start: { rowIndex: ev.rowIndex, colId: data[0].id }, end: { rowIndex: ev.rowIndex, colId: data[data.length - 1].id } });
2765
+ }
2766
+ return;
2767
+ }
2355
2768
  const dragged = suppressClick;
2356
2769
  suppressClick = false;
2357
2770
  const prev = grid.store.state.focus;
@@ -2418,6 +2831,9 @@ function attachBody(grid) {
2418
2831
  case "copy":
2419
2832
  grid.copyToClipboard(false);
2420
2833
  return true;
2834
+ case "cut":
2835
+ grid.cutToClipboard();
2836
+ return true;
2421
2837
  case "selectAll":
2422
2838
  grid.selectAllCells();
2423
2839
  return true;
@@ -2653,6 +3069,14 @@ function attachBody(grid) {
2653
3069
  e.clipboardData.setData("text/plain", text2);
2654
3070
  e.preventDefault();
2655
3071
  };
3072
+ const onCut = (e) => {
3073
+ if (fromField(e) || nested(e)) return;
3074
+ const text2 = grid.selectionTSV(false);
3075
+ if (!text2 || !e.clipboardData) return;
3076
+ e.clipboardData.setData("text/plain", text2);
3077
+ e.preventDefault();
3078
+ grid.clearCells();
3079
+ };
2656
3080
  const onPaste = (e) => {
2657
3081
  if (fromField(e) || nested(e)) return;
2658
3082
  const text2 = e.clipboardData?.getData("text/plain");
@@ -2666,6 +3090,7 @@ function attachBody(grid) {
2666
3090
  root.addEventListener("keydown", onKeyDown);
2667
3091
  root.addEventListener("copy", onCopy);
2668
3092
  root.addEventListener("paste", onPaste);
3093
+ root.addEventListener("cut", onCut);
2669
3094
  return () => {
2670
3095
  body.removeEventListener("click", onClick);
2671
3096
  body.removeEventListener("dblclick", onDblClick);
@@ -2673,6 +3098,7 @@ function attachBody(grid) {
2673
3098
  root.removeEventListener("keydown", onKeyDown);
2674
3099
  root.removeEventListener("copy", onCopy);
2675
3100
  root.removeEventListener("paste", onPaste);
3101
+ root.removeEventListener("cut", onCut);
2676
3102
  };
2677
3103
  }
2678
3104
 
@@ -3147,6 +3573,8 @@ var ContextMenu = class {
3147
3573
  const items = [
3148
3574
  { label: loc.copy, shortcut: shortcut("C"), action: () => grid.copyToClipboard(false) },
3149
3575
  { label: loc.copyWithHeaders, action: () => grid.copyToClipboard(true) },
3576
+ // after the copy items, so defaultItems indexes from earlier versions still point at the same entries
3577
+ { label: loc.cut, shortcut: shortcut("X"), action: () => grid.cutToClipboard() },
3150
3578
  ...grid.store.state.selected.size ? [{ label: loc.copyRows, action: () => grid.copyToClipboard(false, true) }] : [],
3151
3579
  { label: loc.paste, shortcut: shortcut("V"), action: () => this.pasteFromClipboard() }
3152
3580
  ];
@@ -3186,6 +3614,7 @@ var ContextMenu = class {
3186
3614
  { label: loc.exportCsv, action: () => grid.api.exportCsv() },
3187
3615
  { label: loc.exportExcel, action: () => grid.api.exportXlsx() }
3188
3616
  );
3617
+ if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
3189
3618
  return items;
3190
3619
  }
3191
3620
  pasteFromClipboard() {
@@ -3532,7 +3961,7 @@ function fillSeries(source, count) {
3532
3961
  const asDate = source[source.length - 1] instanceof Date;
3533
3962
  return Array.from({ length: count }, (_, i) => {
3534
3963
  const d = new Date(lastDate.getFullYear(), lastDate.getMonth(), lastDate.getDate() + step * (i + 1));
3535
- return asDate ? d : toDateKey(d);
3964
+ return asDate ? d : toDateKey2(d);
3536
3965
  });
3537
3966
  }
3538
3967
  return repeat();
@@ -3616,13 +4045,14 @@ function parseDelimited(text2, sep) {
3616
4045
  }
3617
4046
  function copyValue(def, value, locale) {
3618
4047
  if (isBlank(value)) return "";
4048
+ if (Array.isArray(value)) return value.join(", ");
3619
4049
  switch (def.type) {
3620
4050
  case "number": {
3621
4051
  const n = typeof value === "number" ? value : Number(value);
3622
4052
  return Number.isNaN(n) ? String(value) : String(n);
3623
4053
  }
3624
4054
  case "date":
3625
- return toDateKey(value) ?? String(value);
4055
+ return toDateKey2(value) ?? String(value);
3626
4056
  case "boolean":
3627
4057
  return value === true ? "TRUE" : value === false ? "FALSE" : String(value);
3628
4058
  default:
@@ -3661,7 +4091,7 @@ function parseDate(text2, previous) {
3661
4091
  else if (m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s)) date = validDate(+m[3], +m[1], +m[2]);
3662
4092
  else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date = toDate(s);
3663
4093
  if (!date) return { ok: false };
3664
- return { ok: true, value: previous instanceof Date ? date : toDateKey(date) };
4094
+ return { ok: true, value: previous instanceof Date ? date : toDateKey2(date) };
3665
4095
  }
3666
4096
  function parseValue(def, text2, previous) {
3667
4097
  switch (def.type) {
@@ -3698,6 +4128,49 @@ var CellEditor = class {
3698
4128
  constructor(grid) {
3699
4129
  this.grid = grid;
3700
4130
  this.cur = null;
4131
+ /** editType 'row': the row being edited and its values before the first change. */
4132
+ this.session = null;
4133
+ grid.events.on("focusChange", ({ rowIndex }) => {
4134
+ if (this.session && !this.cur && this.grid.rowModel.getRow(rowIndex)?.data !== this.session.row) this.endRow(true);
4135
+ });
4136
+ }
4137
+ get rowMode() {
4138
+ return this.grid.options.editType === "row";
4139
+ }
4140
+ /** Is this row id being edited as a whole row? */
4141
+ editingRow(id) {
4142
+ return this.session?.id === id;
4143
+ }
4144
+ beginRow(row, rowIndex) {
4145
+ if (this.session?.row === row) return;
4146
+ if (this.session) this.endRow(true);
4147
+ this.session = { row, id: this.grid.rowModel.idOf(row), before: { ...row } };
4148
+ this.grid.renderer.invalidate({ rows: true });
4149
+ this.grid.events.emit("rowEditStart", { row, rowIndex });
4150
+ }
4151
+ /** Finish the row session: keep the values, or put the row back as it was. */
4152
+ endRow(commit) {
4153
+ const s = this.session;
4154
+ if (!s) return;
4155
+ if (this.cur) {
4156
+ if (commit) this.commit(null);
4157
+ else this.close(false);
4158
+ }
4159
+ this.session = null;
4160
+ const { grid } = this;
4161
+ const rowIndex = grid.rowModel.indexOfId(s.id);
4162
+ const cols = grid.columns.displayed().filter((c) => c.def.field && !isSystemCol(c.id));
4163
+ if (!commit) {
4164
+ const changes2 = [];
4165
+ for (const c of cols) {
4166
+ const f = c.def.field;
4167
+ if (!Object.is(s.row[f], s.before[f])) grid.writeCell(changes2, s.row, f, c.id, rowIndex, s.before[f]);
4168
+ }
4169
+ grid.endCellTx(changes2);
4170
+ }
4171
+ 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] }));
4172
+ grid.renderer.invalidate({ rows: true });
4173
+ grid.events.emit("rowEditStop", { row: s.row, rowIndex, committed: commit, changes });
3701
4174
  }
3702
4175
  get active() {
3703
4176
  return this.cur && { rowIndex: this.cur.rowIndex, colId: this.cur.colId };
@@ -3719,6 +4192,7 @@ var CellEditor = class {
3719
4192
  const def = col.def;
3720
4193
  const row = grid.rowModel.getRow(rowIndex).data;
3721
4194
  const value = row[def.field];
4195
+ if (this.rowMode) this.beginRow(row, rowIndex);
3722
4196
  grid.setFocus(rowIndex, colId);
3723
4197
  renderer.ensureVisible(rowIndex, colId);
3724
4198
  renderer.render();
@@ -3774,7 +4248,7 @@ var CellEditor = class {
3774
4248
  input.type = def.type === "date" && initialText === void 0 ? "date" : "text";
3775
4249
  if (def.type === "number") input.inputMode = "decimal";
3776
4250
  }
3777
- const start = def.type === "date" ? toDateKey(value) ?? "" : isBlank(value) ? "" : String(value);
4251
+ const start = def.type === "date" ? toDateKey2(value) ?? "" : isBlank(value) ? "" : String(value);
3778
4252
  input.value = initialText ?? start;
3779
4253
  control = input;
3780
4254
  read = () => ({ text: input.value });
@@ -3848,11 +4322,15 @@ var CellEditor = class {
3848
4322
  if (error !== null) return this.fail(value, error);
3849
4323
  this.close(true);
3850
4324
  grid.applyEdit(cur.rowIndex, cur.colId, value);
4325
+ if (this.session && (move === "down" || move === "up")) {
4326
+ this.endRow(true);
4327
+ }
3851
4328
  this.move(cur.rowIndex, cur.colId, move);
3852
4329
  return true;
3853
4330
  }
3854
4331
  cancel() {
3855
4332
  this.close(false);
4333
+ if (this.session) this.endRow(false);
3856
4334
  }
3857
4335
  fail(value, message) {
3858
4336
  const cur = this.cur;
@@ -3886,11 +4364,11 @@ var CellEditor = class {
3886
4364
  let i = cols.findIndex((c) => c.id === colId) + step;
3887
4365
  if (i >= cols.length) {
3888
4366
  i = 0;
3889
- r++;
4367
+ if (!this.session) r++;
3890
4368
  }
3891
4369
  if (i < 0) {
3892
4370
  i = cols.length - 1;
3893
- r--;
4371
+ if (!this.session) r--;
3894
4372
  }
3895
4373
  if (r < 0 || r >= count) return;
3896
4374
  this.start(r, cols[i].id);
@@ -4490,6 +4968,79 @@ function exportRows(grid, mode) {
4490
4968
  return grid.rowModel.dataRows().map((n) => n.data);
4491
4969
  }
4492
4970
  var header = (def, id) => def.header ?? def.field ?? id;
4971
+ var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4972
+ function buildPrintHtml(grid, opts = {}) {
4973
+ const cols = exportColumns(grid, opts.columns);
4974
+ const code = grid.store.state.locale.code;
4975
+ const fmts = cols.map((c) => getFormatter(c.def, code));
4976
+ const chains = cols.map((c) => {
4977
+ const chain = [];
4978
+ for (let n = c.node; n; n = n.parent) chain.unshift(n);
4979
+ return chain;
4980
+ });
4981
+ const depth = Math.max(1, ...chains.map((ch) => ch.length));
4982
+ const head = [];
4983
+ for (let level = 0; level < depth; level++) {
4984
+ let tr = "";
4985
+ for (let ci = 0; ci < cols.length; ci++) {
4986
+ const chain = chains[ci];
4987
+ const leaf = chain.length - 1;
4988
+ if (level < leaf) {
4989
+ const g = chain[level];
4990
+ if (ci > 0 && chains[ci - 1][level] === g) continue;
4991
+ let span = 1;
4992
+ while (ci + span < cols.length && chains[ci + span][level] === g) span++;
4993
+ tr += `<th colspan="${span}">${escHtml(header(g.def, g.id))}</th>`;
4994
+ } else if (level === leaf) {
4995
+ tr += `<th colspan="1" rowspan="${depth - level}">${escHtml(header(cols[ci].def, cols[ci].id))}</th>`;
4996
+ }
4997
+ }
4998
+ head.push(`<tr>${tr}</tr>`);
4999
+ }
5000
+ const align = (def) => def.align ?? (def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left");
5001
+ const body = exportRows(grid, opts.rows).map((r) => `<tr>${cols.map((c, i) => {
5002
+ const v = getCellValue(c.def, r);
5003
+ return `<td style="text-align:${align(c.def)}">${escHtml(isBlank(v) ? "" : fmts[i](v))}</td>`;
5004
+ }).join("")}</tr>`).join("");
5005
+ const title = escHtml(opts.title ?? document.title ?? "");
5006
+ return `<!doctype html><html lang="${escHtml(code)}"><head><meta charset="utf-8"><title>${title}</title><style>
5007
+ @page { size: auto; margin: 12mm; }
5008
+ body { font: 11px/1.4 system-ui, 'Pretendard', 'Malgun Gothic', sans-serif; color: #111; margin: 0; }
5009
+ h1 { font-size: 15px; margin: 0 0 8px; }
5010
+ table { border-collapse: collapse; width: 100%; }
5011
+ th, td { border: 1px solid #999; padding: 3px 6px; }
5012
+ th { background: #eee; font-weight: 600; }
5013
+ thead { display: table-header-group; }
5014
+ tr { page-break-inside: avoid; }
5015
+ </style></head><body>${opts.title ? `<h1>${title}</h1>` : ""}<table><thead>${opts.headers === false ? "" : head.join("")}</thead><tbody>${body}</tbody></table></body></html>`;
5016
+ }
5017
+ function printHtml(html) {
5018
+ if (typeof document === "undefined") return;
5019
+ const frame = document.createElement("iframe");
5020
+ frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0";
5021
+ document.body.append(frame);
5022
+ const doc = frame.contentDocument;
5023
+ if (!doc || !frame.contentWindow) {
5024
+ frame.remove();
5025
+ warn("printing is not supported in this environment");
5026
+ return;
5027
+ }
5028
+ doc.open();
5029
+ doc.write(html);
5030
+ doc.close();
5031
+ const win = frame.contentWindow;
5032
+ const done = () => setTimeout(() => frame.remove(), 1e3);
5033
+ win.addEventListener("afterprint", done);
5034
+ setTimeout(() => {
5035
+ try {
5036
+ win.focus();
5037
+ win.print();
5038
+ } catch {
5039
+ warn("printing is not supported in this environment");
5040
+ }
5041
+ done();
5042
+ }, 50);
5043
+ }
4493
5044
  function buildCsv(grid, opts = {}) {
4494
5045
  const cols = exportColumns(grid, opts.columns);
4495
5046
  const code = grid.store.state.locale.code;
@@ -4569,6 +5120,9 @@ function classStyles(grid) {
4569
5120
  };
4570
5121
  }
4571
5122
  function buildXlsx(grid, opts = {}) {
5123
+ return writeXlsx([buildXlsxSheet(grid, opts)]);
5124
+ }
5125
+ function buildXlsxSheet(grid, opts = {}) {
4572
5126
  const cols = exportColumns(grid, opts.columns);
4573
5127
  const code = grid.store.state.locale.code;
4574
5128
  const rows = [];
@@ -4674,14 +5228,14 @@ function buildXlsx(grid, opts = {}) {
4674
5228
  const s = computeSpans(data, c.def);
4675
5229
  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
5230
  });
4677
- return writeXlsx([{
5231
+ return {
4678
5232
  name: opts.sheetName ?? "Sheet1",
4679
5233
  rows,
4680
5234
  widths: cols.map((c) => Math.round(c.width / 7)),
4681
5235
  merges,
4682
5236
  freezeRows: headRows,
4683
5237
  autoFilter: headRows > 0
4684
- }]);
5238
+ };
4685
5239
  }
4686
5240
  function download(filename, parts, type) {
4687
5241
  if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function" || typeof document === "undefined") {
@@ -4703,7 +5257,7 @@ function asText(v) {
4703
5257
  if (v === null || v === void 0) return "";
4704
5258
  if (v instanceof Date) {
4705
5259
  const hasTime = v.getHours() || v.getMinutes() || v.getSeconds();
4706
- return hasTime ? v.toISOString() : toDateKey(v) ?? "";
5260
+ return hasTime ? v.toISOString() : toDateKey2(v) ?? "";
4707
5261
  }
4708
5262
  return String(v);
4709
5263
  }
@@ -4811,7 +5365,8 @@ function renderChart(el2, spec) {
4811
5365
  top = 36;
4812
5366
  }
4813
5367
  const legend = svg("g", {}, "fg-chart-legend");
4814
- const names = spec.type === "pie" ? cats : series.map((s) => s.name);
5368
+ const round2 = spec.type === "pie" || spec.type === "donut";
5369
+ const names = round2 ? cats : spec.type === "scatter" ? series.slice(1).map((s) => s.name) : series.map((s) => s.name);
4815
5370
  let lx = 16;
4816
5371
  names.forEach((name, i) => {
4817
5372
  const item = svg("g", { transform: `translate(${lx},${H - 10})` }, "fg-chart-legend-item");
@@ -4823,7 +5378,13 @@ function renderChart(el2, spec) {
4823
5378
  lx += 24 + Math.min(140, name.length * 7);
4824
5379
  });
4825
5380
  const bottom = H - 52;
4826
- if (spec.type === "pie") {
5381
+ if (spec.type === "scatter") {
5382
+ drawScatter(root, spec, series, top, bottom, nf, short);
5383
+ root.append(legend);
5384
+ el2.replaceChildren(root);
5385
+ return;
5386
+ }
5387
+ if (round2) {
4827
5388
  const values = (series[0]?.values ?? []).map((v) => v !== null && v > 0 ? v : 0);
4828
5389
  const total = values.reduce((a, b) => a + b, 0);
4829
5390
  const cx = W / 2, cy = (top + bottom) / 2, r = Math.max(10, (bottom - top) / 2 - 6);
@@ -4839,6 +5400,7 @@ function renderChart(el2, spec) {
4839
5400
  root.append(slice);
4840
5401
  angle = a2;
4841
5402
  });
5403
+ if (spec.type === "donut") root.append(svg("circle", { cx, cy, r: r * 0.58, fill: "var(--fg-bg, #fff)" }, "fg-chart-donut-hole"));
4842
5404
  root.append(legend);
4843
5405
  el2.replaceChildren(root);
4844
5406
  return;
@@ -4879,11 +5441,25 @@ function renderChart(el2, spec) {
4879
5441
  cats.forEach((c, i) => {
4880
5442
  if (i % every) return;
4881
5443
  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;
5444
+ const fit = Math.max(3, Math.min(12, Math.floor(band * every / 10)));
5445
+ label.textContent = c.length > fit ? `${c.slice(0, fit - 1)}\u2026` : c;
4883
5446
  axis.append(label);
4884
5447
  });
4885
5448
  root.append(axis);
4886
5449
  const plot = svg("g", {}, "fg-chart-plot");
5450
+ const combo = spec.type === "combo";
5451
+ if (combo) {
5452
+ const inner = band * 0.6;
5453
+ const s = series[0];
5454
+ cats.forEach((c, ci) => {
5455
+ const v = s?.values[ci];
5456
+ if (v === null || v === void 0 || !Number.isFinite(v)) return;
5457
+ const x = left + band * ci + (band - inner) / 2;
5458
+ 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");
5459
+ tip(rect, `${s.name} \xB7 ${c}: ${nf.format(v)}`);
5460
+ plot.append(rect);
5461
+ });
5462
+ }
4887
5463
  if (spec.type === "bar" || stacked) {
4888
5464
  const inner = band * 0.72;
4889
5465
  const bw = stacked ? inner : inner / Math.max(1, series.length);
@@ -4907,6 +5483,7 @@ function renderChart(el2, spec) {
4907
5483
  });
4908
5484
  } else {
4909
5485
  series.forEach((s, si) => {
5486
+ if (combo && si === 0) return;
4910
5487
  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
5488
  let d = "";
4912
5489
  let pen = false;
@@ -4934,14 +5511,56 @@ function renderChart(el2, spec) {
4934
5511
  });
4935
5512
  });
4936
5513
  }
4937
- root.append(plot, legend);
4938
- el2.replaceChildren(root);
5514
+ root.append(plot, legend);
5515
+ el2.replaceChildren(root);
5516
+ }
5517
+ function drawScatter(root, spec, series, top, bottom, nf, short) {
5518
+ const xs = series[0]?.values ?? [];
5519
+ const others = series.slice(1);
5520
+ const finite = (v) => v !== null && v !== void 0 && Number.isFinite(v);
5521
+ const allX = xs.filter(finite);
5522
+ const allY = others.flatMap((s) => s.values.filter(finite));
5523
+ if (!allX.length || !allY.length) return;
5524
+ const xt = niceTicks(Math.min(...allX), Math.max(...allX));
5525
+ const yt = niceTicks(Math.min(...allY), Math.max(...allY));
5526
+ const left = 56, right = W - 16;
5527
+ const x = (v) => left + (v - xt[0]) / (xt[xt.length - 1] - xt[0]) * (right - left);
5528
+ const y = (v) => bottom - (v - yt[0]) / (yt[yt.length - 1] - yt[0]) * (bottom - top);
5529
+ const axis = svg("g", {}, "fg-chart-axis");
5530
+ for (const t of yt) {
5531
+ axis.append(svg("line", { x1: left, x2: right, y1: y(t), y2: y(t) }, t === 0 ? "fg-chart-zero" : "fg-chart-grid"));
5532
+ const label = svg("text", { x: left - 6, y: y(t) + 4, "text-anchor": "end" });
5533
+ label.textContent = short.format(t);
5534
+ axis.append(label);
5535
+ }
5536
+ xt.forEach((t, i) => {
5537
+ const anchor = i === 0 ? "start" : i === xt.length - 1 ? "end" : "middle";
5538
+ const label = svg("text", { x: x(t), y: bottom + 16, "text-anchor": anchor });
5539
+ label.textContent = short.format(t);
5540
+ axis.append(label);
5541
+ });
5542
+ const xName = svg("text", { x: right, y: bottom + 32, "text-anchor": "end" }, "fg-chart-axis-name");
5543
+ xName.textContent = `${series[0]?.name ?? ""} \u2192`;
5544
+ axis.append(xName);
5545
+ root.append(axis);
5546
+ const plot = svg("g", {}, "fg-chart-plot");
5547
+ xs.forEach((xv, i) => {
5548
+ if (!finite(xv)) return;
5549
+ others.forEach((s, si) => {
5550
+ const yv = s.values[i];
5551
+ if (!finite(yv)) return;
5552
+ const dot = svg("circle", { cx: x(xv), cy: y(yv), r: 4, fill: chartColor(si), "fill-opacity": 0.8 }, "fg-chart-point");
5553
+ tip(dot, `${spec.categories[i] ?? ""}: ${series[0].name} ${nf.format(xv)}, ${s.name} ${nf.format(yv)}`);
5554
+ plot.append(dot);
5555
+ });
5556
+ });
5557
+ root.append(plot);
4939
5558
  }
4940
5559
 
4941
5560
  // src/interactions/charts.ts
4942
5561
  var MAX_SERIES = 12;
4943
5562
  var MAX_ROWS = 500;
4944
- var TYPES = ["bar", "stackedBar", "line", "area", "pie"];
5563
+ var TYPES = ["bar", "stackedBar", "line", "area", "combo", "pie", "donut", "scatter"];
4945
5564
  var GridCharts = class {
4946
5565
  constructor(grid) {
4947
5566
  this.grid = grid;
@@ -4993,7 +5612,7 @@ var GridCharts = class {
4993
5612
  const body = el("div", "fg-chart-body");
4994
5613
  let panel = null;
4995
5614
  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];
5615
+ 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
5616
  const syncButtons = () => panel?.querySelectorAll(".fg-chart-type").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.type === type)));
4998
5617
  if (opts.container) opts.container.append(body);
4999
5618
  else {
@@ -5217,7 +5836,7 @@ function buildPivot(source, config, defOf, labels, locale) {
5217
5836
  }
5218
5837
 
5219
5838
  // src/version.ts
5220
- var version = "0.8.1";
5839
+ var version = "1.0.0";
5221
5840
 
5222
5841
  // src/license.ts
5223
5842
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -5475,18 +6094,23 @@ var ColumnMenu = class {
5475
6094
  t.textContent = loc.filter;
5476
6095
  return t;
5477
6096
  }
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");
6097
+ /** One condition: operator, value and (for between) a second value. */
6098
+ conditionRow(type, loc, cur, optional, apply) {
6099
+ const row = el("div", "fg-filter-cond");
5482
6100
  const select = el("select", "fg-filter-op", { "aria-label": loc.filter });
6101
+ if (optional) {
6102
+ const none = el("option");
6103
+ none.value = "";
6104
+ none.textContent = "\u2014";
6105
+ select.append(none);
6106
+ }
5483
6107
  for (const op of OPS[type]) {
5484
6108
  const o = el("option");
5485
6109
  o.value = op;
5486
6110
  o.textContent = loc.ops[op];
5487
6111
  select.append(o);
5488
6112
  }
5489
- select.value = cur?.op ?? OPS[type][0];
6113
+ select.value = cur?.op ?? (optional ? "" : OPS[type][0]);
5490
6114
  const inputType = type === "text" ? "text" : type;
5491
6115
  const v1 = el("input", "fg-input fg-filter-value", { type: inputType, placeholder: loc.value, "aria-label": loc.value });
5492
6116
  const v2 = el("input", "fg-input fg-filter-value-to", { type: inputType, placeholder: loc.valueTo, "aria-label": loc.valueTo });
@@ -5495,27 +6119,55 @@ var ColumnMenu = class {
5495
6119
  v2.value = "valueTo" in cur && cur.valueTo != null ? String(cur.valueTo) : "";
5496
6120
  }
5497
6121
  const sync = () => {
5498
- v1.hidden = NO_VALUE.includes(select.value);
6122
+ v1.hidden = !select.value || NO_VALUE.includes(select.value);
5499
6123
  v2.hidden = select.value !== "between";
5500
6124
  };
5501
6125
  select.addEventListener("change", sync);
5502
6126
  sync();
5503
- const apply = () => {
6127
+ for (const input of [v1, v2]) input.addEventListener("keydown", (e) => e.key === "Enter" && apply());
6128
+ row.append(select, v1, v2);
6129
+ const read = () => {
5504
6130
  const op = select.value;
6131
+ if (!op) return null;
5505
6132
  const needsValue = !NO_VALUE.includes(op);
5506
- let model = null;
5507
6133
  if (type === "number") {
5508
6134
  const a = v1.value === "" ? void 0 : Number(v1.value);
5509
6135
  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 };
6136
+ return !needsValue || a !== void 0 ? { type, op, value: a, valueTo: b } : null;
6137
+ }
6138
+ if (type === "date") {
6139
+ return !needsValue || v1.value ? { type, op, value: v1.value || void 0, valueTo: v2.value || void 0 } : null;
6140
+ }
6141
+ return !needsValue || v1.value ? { type, op, value: v1.value } : null;
6142
+ };
6143
+ return { row, read };
6144
+ }
6145
+ conditionFilter(colId, type, loc, done) {
6146
+ const raw = this.grid.store.state.filters[colId];
6147
+ const parts = raw?.type === "combined" ? raw.conditions : raw ? [raw] : [];
6148
+ const first = parts[0]?.type === type ? parts[0] : void 0;
6149
+ const second = parts[1]?.type === type ? parts[1] : void 0;
6150
+ const box = el("div", "fg-menu-section fg-filter");
6151
+ const apply = () => {
6152
+ const a = c1.read();
6153
+ const b = c2.read();
6154
+ const operator = join.querySelector("input:checked")?.value === "or" ? "or" : "and";
6155
+ const model = a && b ? { type: "combined", operator, conditions: [a, b] } : a ?? b;
5514
6156
  this.grid.setFilter(colId, model);
5515
6157
  done();
5516
6158
  };
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));
6159
+ const c1 = this.conditionRow(type, loc, first, false, apply);
6160
+ const c2 = this.conditionRow(type, loc, second, true, apply);
6161
+ const join = el("div", "fg-filter-join", { role: "radiogroup" });
6162
+ const name = `fg-join-${colId}-${Math.random().toString(36).slice(2, 7)}`;
6163
+ for (const op of ["and", "or"]) {
6164
+ const label = el("label");
6165
+ const r = el("input", "", { type: "radio", name, value: op });
6166
+ r.checked = (raw?.type === "combined" ? raw.operator : "and") === op;
6167
+ label.append(r, ` ${loc[op]}`);
6168
+ join.append(label);
6169
+ }
6170
+ box.append(this.title(loc), c1.row, join, c2.row, this.actions(colId, loc, apply, done));
5519
6171
  return box;
5520
6172
  }
5521
6173
  setFilter(colId, loc, done) {
@@ -5619,7 +6271,7 @@ var RowModel = class {
5619
6271
  const d = input.display;
5620
6272
  const treeChanged = !!d?.getParentId !== !!this.last.display?.getParentId;
5621
6273
  const dataChanged = input.data !== this.last.data;
5622
- const filterChanged = dataChanged || treeChanged || input.filters !== this.last.filters || input.quickFilter !== this.last.quickFilter;
6274
+ const filterChanged = dataChanged || treeChanged || input.filters !== this.last.filters || input.quickFilter !== this.last.quickFilter || input.advancedFilter !== this.last.advancedFilter;
5623
6275
  const sortChanged = filterChanged || input.sort !== this.last.sort || collator !== this.last.collator;
5624
6276
  const displayChanged = sortChanged || d?.version !== this.last.displayVersion || d?.groupBy !== this.last.display?.groupBy;
5625
6277
  if (dataChanged) {
@@ -5685,7 +6337,7 @@ var RowModel = class {
5685
6337
  * Prefix-sum offsets of the display rows: detail rows use `detailHeight`, rows with a
5686
6338
  * measured height (auto row height) use it, everything else `rowHeight`.
5687
6339
  */
5688
- layout(rowHeight, detailHeight, heights, heightsVersion = 0) {
6340
+ layout(rowHeight, detailHeight, heights, heightsVersion = 0, heightOf) {
5689
6341
  const c = this.layoutCache;
5690
6342
  if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight && c.hv === heightsVersion) return c;
5691
6343
  const n = this.display.length;
@@ -5696,7 +6348,11 @@ var RowModel = class {
5696
6348
  const node = this.display[i];
5697
6349
  let h = rowHeight;
5698
6350
  if (node.kind === "detail") h = detailHeight;
5699
- else if (measured && !node.kind) h = measured.get(node.id) ?? rowHeight;
6351
+ else if (!node.kind) {
6352
+ if (measured) h = measured.get(node.id) ?? rowHeight;
6353
+ const fixed = heightOf?.(node.data);
6354
+ if (typeof fixed === "number" && fixed > 0) h = measured ? Math.max(h, fixed) : fixed;
6355
+ }
5700
6356
  if (h !== rowHeight) uniform = false;
5701
6357
  offsets[i + 1] = offsets[i] + h;
5702
6358
  }
@@ -5707,9 +6363,11 @@ var RowModel = class {
5707
6363
  const preds = Object.entries(input.filters).map(([colId, model]) => ({ def: this.deps.columnDef(colId), pred: buildPredicate(model) })).filter((p) => !!p.def);
5708
6364
  const tokens = quickTokens(input.quickFilter);
5709
6365
  const quickCols = tokens.length ? this.deps.quickColumns() : [];
5710
- if (!preds.length && !tokens.length) return this.nodes;
6366
+ const advanced = input.advancedFilter ? buildAdvanced(input.advancedFilter, (id) => this.deps.columnDef(id), getCellValue) : null;
6367
+ if (!preds.length && !tokens.length && !advanced) return this.nodes;
5711
6368
  const matches = this.nodes.filter((n) => {
5712
6369
  for (const p of preds) if (!p.pred(getCellValue(p.def, n.data))) return false;
6370
+ if (advanced && !advanced(n.data)) return false;
5713
6371
  if (tokens.length) {
5714
6372
  const text2 = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
5715
6373
  if (!matchesQuick(tokens, text2)) return false;
@@ -5819,6 +6477,149 @@ var RowModel = class {
5819
6477
  }
5820
6478
  };
5821
6479
 
6480
+ // src/rows/serverGroups.ts
6481
+ var LEVEL_ROWS = 1e4;
6482
+ var ServerGroups = class {
6483
+ constructor(datasource, getRowId, groupBy, query, onChange) {
6484
+ this.datasource = datasource;
6485
+ this.getRowId = getRowId;
6486
+ this.groupBy = groupBy;
6487
+ this.query = query;
6488
+ this.onChange = onChange;
6489
+ this.levels = /* @__PURE__ */ new Map();
6490
+ this.gen = 0;
6491
+ this.display = null;
6492
+ this.ids = null;
6493
+ this.version = 0;
6494
+ this.builtFor = -1;
6495
+ }
6496
+ matches(groupBy, q) {
6497
+ 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;
6498
+ }
6499
+ purge() {
6500
+ this.gen++;
6501
+ this.levels.clear();
6502
+ this.touch();
6503
+ }
6504
+ touch() {
6505
+ this.version++;
6506
+ this.display = null;
6507
+ this.ids = null;
6508
+ }
6509
+ load(path, keys, level) {
6510
+ if (this.levels.has(path)) return;
6511
+ const gen = this.gen;
6512
+ this.levels.set(path, { state: "loading", nodes: [] });
6513
+ const last = level >= this.groupBy.length;
6514
+ const q = this.query;
6515
+ let result;
6516
+ try {
6517
+ result = Promise.resolve(this.datasource.getRows({
6518
+ start: 0,
6519
+ end: LEVEL_ROWS,
6520
+ sort: [...q.sort],
6521
+ filters: { ...q.filters },
6522
+ quickFilter: q.quickFilter,
6523
+ advancedFilter: q.advancedFilter,
6524
+ groupBy: [...this.groupBy],
6525
+ groupKeys: [...keys]
6526
+ }));
6527
+ } catch (e) {
6528
+ result = Promise.reject(e);
6529
+ }
6530
+ result.then((res) => {
6531
+ if (gen !== this.gen) return;
6532
+ const rows = Array.isArray(res?.rows) ? res.rows.filter((r) => r !== null && typeof r === "object") : [];
6533
+ if (!Array.isArray(res?.rows)) warn("datasource.getRows must resolve to { rows: Row[] }");
6534
+ const nodes = last ? rows.map((data, i) => ({ id: this.getRowId ? String(this.getRowId(data)) : autoId(data), data, sourceIndex: i, level })) : rows.map((data) => {
6535
+ const colId = this.groupBy[level];
6536
+ const value = data[colId];
6537
+ const key = `${path ? `${path}|` : ""}${colId}:${setKey(value)}`;
6538
+ const count = typeof data.count === "number" ? data.count : typeof data.childCount === "number" ? data.childCount : void 0;
6539
+ return {
6540
+ id: `group:${key}`,
6541
+ data: {},
6542
+ sourceIndex: -1,
6543
+ kind: "group",
6544
+ level,
6545
+ group: { key, colId, value, rows: [], expanded: false, count, aggregates: data, keys: [...keys, value] }
6546
+ };
6547
+ });
6548
+ this.levels.set(path, { state: "loaded", nodes });
6549
+ this.touch();
6550
+ this.onChange();
6551
+ }, () => {
6552
+ if (gen !== this.gen) return;
6553
+ this.levels.set(path, { state: "failed", nodes: [] });
6554
+ this.touch();
6555
+ this.onChange();
6556
+ });
6557
+ }
6558
+ isOpen(d, key, level) {
6559
+ const explicit = d.expanded.get(key);
6560
+ if (explicit !== void 0) return explicit;
6561
+ return d.defaultExpanded === -1 || level < d.defaultExpanded;
6562
+ }
6563
+ /** Display rows: open groups show their loaded children, or a loading row while they arrive. */
6564
+ rows(d, displayVersion) {
6565
+ if (this.display && this.builtFor === displayVersion) return this.display;
6566
+ const out = [];
6567
+ const walk = (path, keys, level) => {
6568
+ const lv = this.levels.get(path);
6569
+ if (!lv) {
6570
+ this.load(path, keys, level);
6571
+ out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6572
+ return;
6573
+ }
6574
+ if (lv.state !== "loaded") {
6575
+ if (lv.state === "loading") out.push({ id: `loading:${path}`, data: {}, sourceIndex: -1, kind: "loading", level });
6576
+ return;
6577
+ }
6578
+ for (const n of lv.nodes) {
6579
+ if (n.kind !== "group") {
6580
+ out.push(n);
6581
+ continue;
6582
+ }
6583
+ const g = n.group;
6584
+ const open = this.isOpen(d, g.key, level);
6585
+ const node = { ...n, group: { ...g, expanded: open } };
6586
+ out.push(node);
6587
+ if (open) walk(g.key, g.keys ?? [], level + 1);
6588
+ }
6589
+ };
6590
+ walk("", [], 0);
6591
+ this.display = out;
6592
+ this.builtFor = displayVersion;
6593
+ this.ids = null;
6594
+ return out;
6595
+ }
6596
+ dataRows() {
6597
+ const out = [];
6598
+ for (const lv of this.levels.values()) for (const n of lv.nodes) if (!n.kind) out.push(n);
6599
+ return out;
6600
+ }
6601
+ /** Rows matching the filters, from the counts of the top level when the server sent them. */
6602
+ dataCount() {
6603
+ const top = this.levels.get("");
6604
+ if (top?.state === "loaded" && top.nodes.every((n) => typeof n.group?.count === "number")) {
6605
+ return top.nodes.reduce((s, n) => s + (n.group?.count ?? 0), 0);
6606
+ }
6607
+ return this.dataRows().length;
6608
+ }
6609
+ loading() {
6610
+ for (const lv of this.levels.values()) if (lv.state === "loading") return true;
6611
+ return false;
6612
+ }
6613
+ indexOf(id) {
6614
+ if (!this.display) return -1;
6615
+ if (!this.ids) {
6616
+ this.ids = /* @__PURE__ */ new Map();
6617
+ this.display.forEach((n, i) => this.ids.set(n.id, i));
6618
+ }
6619
+ return this.ids.get(id) ?? -1;
6620
+ }
6621
+ };
6622
+
5822
6623
  // src/rows/serverRowModel.ts
5823
6624
  var RETRY_MS = 3e3;
5824
6625
  var NO_OFFSETS = new Float64Array(0);
@@ -5839,6 +6640,9 @@ var ServerRowModel = class {
5839
6640
  this.lastBlock = 0;
5840
6641
  this.version = 0;
5841
6642
  this.cache = null;
6643
+ /** Server-side grouping, while groupBy has columns. */
6644
+ this.groups = null;
6645
+ this.displayIn = null;
5842
6646
  }
5843
6647
  get bs() {
5844
6648
  return this.page ? this.page.size : this.deps.blockSize;
@@ -5847,11 +6651,27 @@ var ServerRowModel = class {
5847
6651
  return this.page ? this.page.index * this.page.size : 0;
5848
6652
  }
5849
6653
  update(input) {
6654
+ const groupBy = input.display?.groupBy ?? [];
6655
+ if (groupBy.length && !input.display?.getParentId) {
6656
+ const q2 = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter, advancedFilter: input.advancedFilter ?? null };
6657
+ if (!this.groups || !this.groups.matches(groupBy, q2)) {
6658
+ this.groups = new ServerGroups(this.deps.datasource, this.deps.getRowId, [...groupBy], q2, () => this.deps.onLoaded(0, 0, null));
6659
+ this.touch();
6660
+ }
6661
+ this.displayIn = input.display ?? null;
6662
+ this.query = null;
6663
+ return;
6664
+ }
6665
+ if (this.groups) {
6666
+ this.groups = null;
6667
+ this.query = null;
6668
+ this.touch();
6669
+ }
5850
6670
  const q = this.query;
5851
- const queryChanged = !q || q.sort !== input.sort || q.filters !== input.filters || q.quickFilter !== input.quickFilter;
6671
+ const queryChanged = !q || q.sort !== input.sort || q.filters !== input.filters || q.quickFilter !== input.quickFilter || q.advancedFilter !== (input.advancedFilter ?? null);
5852
6672
  const p = input.page ?? null;
5853
6673
  const sizeChanged = (p?.size ?? 0) !== (this.page?.size ?? 0);
5854
- this.query = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter };
6674
+ this.query = { sort: input.sort, filters: input.filters, quickFilter: input.quickFilter, advancedFilter: input.advancedFilter ?? null };
5855
6675
  this.page = p ? { ...p } : null;
5856
6676
  if (queryChanged || sizeChanged) this.purge();
5857
6677
  this.ensure(Math.floor(this.base / this.bs));
@@ -5860,6 +6680,10 @@ var ServerRowModel = class {
5860
6680
  }
5861
6681
  /** Forget every row and start over (sort/filter change or refreshRows). */
5862
6682
  purge() {
6683
+ if (this.groups) {
6684
+ this.groups.purge();
6685
+ return;
6686
+ }
5863
6687
  this.gen++;
5864
6688
  this.blocks.clear();
5865
6689
  this.total = null;
@@ -5879,13 +6703,16 @@ var ServerRowModel = class {
5879
6703
  return this.loadedEnd + (last && last.nodes.length < this.bs ? 0 : this.bs);
5880
6704
  }
5881
6705
  getRowCount() {
6706
+ if (this.groups) return this.groupRows().length;
5882
6707
  const all2 = this.fullCount();
5883
6708
  return this.page ? Math.max(0, Math.min(this.page.size, all2 - this.base)) : all2;
5884
6709
  }
5885
6710
  dataCount() {
6711
+ if (this.groups) return this.groups.dataCount();
5886
6712
  return this.fullCount();
5887
6713
  }
5888
6714
  loading() {
6715
+ if (this.groups) return this.groups.loading();
5889
6716
  for (const b of this.blocks.values()) if (b.state === "loading") return true;
5890
6717
  return false;
5891
6718
  }
@@ -5897,7 +6724,11 @@ var ServerRowModel = class {
5897
6724
  const b = this.blocks.get(k);
5898
6725
  return b?.state === "loaded" ? b.nodes[abs - k * this.bs] ?? null : null;
5899
6726
  }
6727
+ groupRows() {
6728
+ return this.groups.rows(this.displayIn, this.displayIn.version * 1e6 + this.groups.version);
6729
+ }
5900
6730
  getRow(index) {
6731
+ if (this.groups) return this.groupRows()[index];
5901
6732
  const abs = this.base + index;
5902
6733
  const hit = this.peek(abs);
5903
6734
  if (hit) return hit;
@@ -5920,7 +6751,7 @@ var ServerRowModel = class {
5920
6751
  };
5921
6752
  let result;
5922
6753
  try {
5923
- result = Promise.resolve(this.deps.datasource.getRows({ start, end, sort: [...q.sort], filters: { ...q.filters }, quickFilter: q.quickFilter }));
6754
+ result = Promise.resolve(this.deps.datasource.getRows({ start, end, sort: [...q.sort], filters: { ...q.filters }, quickFilter: q.quickFilter, advancedFilter: q.advancedFilter }));
5924
6755
  } catch (e) {
5925
6756
  result = Promise.reject(e);
5926
6757
  }
@@ -5967,6 +6798,7 @@ var ServerRowModel = class {
5967
6798
  }
5968
6799
  /** Every row of the window, with placeholders for those not loaded (does not request anything). */
5969
6800
  rows() {
6801
+ if (this.groups) return this.groupRows();
5970
6802
  const c = this.built();
5971
6803
  if (!c.rows) {
5972
6804
  const n = this.getRowCount();
@@ -5976,15 +6808,21 @@ var ServerRowModel = class {
5976
6808
  return c.rows;
5977
6809
  }
5978
6810
  dataRows() {
6811
+ if (this.groups) return this.groups.dataRows();
5979
6812
  return this.built().data;
5980
6813
  }
5981
6814
  allNodes() {
6815
+ if (this.groups) return this.groups.dataRows();
5982
6816
  return this.built().data;
5983
6817
  }
5984
6818
  idOf(row) {
5985
6819
  return this.deps.getRowId ? String(this.deps.getRowId(row)) : autoId(row);
5986
6820
  }
5987
6821
  indexOfId(id) {
6822
+ if (this.groups) {
6823
+ this.groupRows();
6824
+ return this.groups.indexOf(id);
6825
+ }
5988
6826
  const abs = this.built().ids.get(id);
5989
6827
  if (abs === void 0) return -1;
5990
6828
  const i = abs - this.base;
@@ -6035,10 +6873,272 @@ var Store = class {
6035
6873
  }
6036
6874
  };
6037
6875
 
6876
+ // src/interactions/tooltip.ts
6877
+ var DELAY = 500;
6878
+ function attachTooltips(grid) {
6879
+ const root = grid.renderer.root;
6880
+ let timer = 0;
6881
+ let tipEl = null;
6882
+ let target = null;
6883
+ const hide = () => {
6884
+ clearTimeout(timer);
6885
+ tipEl?.remove();
6886
+ tipEl = null;
6887
+ target = null;
6888
+ };
6889
+ const textFor = (cell) => {
6890
+ const colId = cell.dataset.col;
6891
+ if (!colId || isSystemCol(colId)) return null;
6892
+ const node = grid.columns.getNode(colId);
6893
+ if (!node) return null;
6894
+ if (cell.classList.contains("fg-header-cell")) return node.def.headerTooltip ?? null;
6895
+ const rowEl = cell.closest(".fg-row[data-row]");
6896
+ if (!rowEl || !cell.closest(".fg-body")) return null;
6897
+ const rowIndex = Number(rowEl.dataset.row);
6898
+ const row = grid.rowModel.getRow(rowIndex);
6899
+ if (!row || row.kind) return null;
6900
+ const def = node.def;
6901
+ const value = getCellValue(def, row.data);
6902
+ const formatted = getFormatter(def, grid.store.state.locale.code)(value);
6903
+ if (typeof def.tooltip === "function") {
6904
+ try {
6905
+ const t = def.tooltip({ value, formatted, row: row.data, rowIndex, column: def });
6906
+ return t ? String(t) : null;
6907
+ } catch {
6908
+ warn(`tooltip of column "${colId}" threw`);
6909
+ return null;
6910
+ }
6911
+ }
6912
+ if (def.tooltip === true) return formatted || null;
6913
+ if (grid.options.overflowTooltip !== false && def.tooltip !== false && cell.scrollWidth > cell.clientWidth + 1) return cell.textContent || null;
6914
+ return null;
6915
+ };
6916
+ const show = (cell, text2) => {
6917
+ tipEl?.remove();
6918
+ tipEl = el("div", `fg-tooltip fg-theme-${grid.store.state.theme}`, { role: "tooltip" });
6919
+ tipEl.textContent = text2;
6920
+ grid.applyFontTo(tipEl);
6921
+ popupParent(root).append(tipEl);
6922
+ const box = cell.getBoundingClientRect();
6923
+ const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
6924
+ const vw = window.innerWidth || 1024, vh = window.innerHeight || 768;
6925
+ const top = box.bottom + 6 + h > vh ? box.top - h - 6 : box.bottom + 6;
6926
+ tipEl.style.left = `${Math.max(4, Math.min(vw - w - 4, box.left))}px`;
6927
+ tipEl.style.top = `${Math.max(4, top)}px`;
6928
+ };
6929
+ const onOver = (e) => {
6930
+ const cell = e.target.closest?.(".fg-cell, .fg-header-cell");
6931
+ if (!cell || cell === target || !root.contains(cell)) return;
6932
+ hide();
6933
+ target = cell;
6934
+ timer = window.setTimeout(() => {
6935
+ if (target !== cell || !cell.isConnected) return;
6936
+ const text2 = textFor(cell);
6937
+ if (text2) show(cell, text2);
6938
+ }, DELAY);
6939
+ };
6940
+ const onOut = (e) => {
6941
+ const to = e.relatedTarget;
6942
+ if (target && to && target.contains(to)) return;
6943
+ hide();
6944
+ };
6945
+ root.addEventListener("pointerover", onOver);
6946
+ root.addEventListener("pointerout", onOut);
6947
+ root.addEventListener("pointerdown", hide);
6948
+ root.addEventListener("keydown", hide);
6949
+ root.addEventListener("wheel", hide, { passive: true });
6950
+ return () => {
6951
+ hide();
6952
+ root.removeEventListener("pointerover", onOver);
6953
+ root.removeEventListener("pointerout", onOut);
6954
+ root.removeEventListener("pointerdown", hide);
6955
+ root.removeEventListener("keydown", hide);
6956
+ root.removeEventListener("wheel", hide);
6957
+ };
6958
+ }
6959
+
6960
+ // src/interactions/filterBuilder.ts
6961
+ var NO_VALUE2 = ["blank", "notBlank"];
6962
+ var FilterBuilder = class {
6963
+ constructor(grid) {
6964
+ this.grid = grid;
6965
+ this.popup = null;
6966
+ this.off = [];
6967
+ }
6968
+ get isOpen() {
6969
+ return !!this.popup;
6970
+ }
6971
+ /** Columns that can be filtered, with the condition type the builder uses for them. */
6972
+ columns() {
6973
+ 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) => {
6974
+ const ft = filterTypeOf(n.def);
6975
+ const kind = ft === "number" ? "number" : ft === "date" ? "date" : "text";
6976
+ return { id: n.id, label: n.def.header ?? n.def.field ?? n.id, kind };
6977
+ });
6978
+ }
6979
+ open() {
6980
+ this.close();
6981
+ const { grid } = this;
6982
+ const loc = grid.store.state.locale;
6983
+ const cols = this.columns();
6984
+ const popup = el("div", `fg-menu fg-filter-builder fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": loc.advancedFilter });
6985
+ grid.applyFontTo(popup);
6986
+ const head = el("div", "fg-chooser-head");
6987
+ const title = el("span", "fg-menu-title");
6988
+ title.textContent = loc.advancedFilter;
6989
+ const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
6990
+ x.textContent = "\xD7";
6991
+ x.addEventListener("click", () => this.close());
6992
+ head.append(title, x);
6993
+ const cur = grid.store.state.advancedFilter;
6994
+ const join = el("div", "fg-filter-join", { role: "radiogroup" });
6995
+ const name = `fg-fb-join-${Math.random().toString(36).slice(2, 7)}`;
6996
+ for (const op of ["and", "or"]) {
6997
+ const label = el("label");
6998
+ const r2 = el("input", "", { type: "radio", name, value: op });
6999
+ r2.checked = (cur?.operator ?? "and") === op;
7000
+ label.append(r2, ` ${loc[op]}`);
7001
+ join.append(label);
7002
+ }
7003
+ const list = el("div", "fg-fb-list");
7004
+ const rows = [];
7005
+ const remove = (row) => {
7006
+ rows.splice(rows.indexOf(row), 1);
7007
+ row.el.remove();
7008
+ };
7009
+ const conditionRow = (start) => {
7010
+ const rowEl = el("div", "fg-fb-row");
7011
+ const colSel = el("select", "fg-filter-op fg-fb-col", { "aria-label": loc.columns });
7012
+ for (const c of cols) {
7013
+ const o = el("option");
7014
+ o.value = c.id;
7015
+ o.textContent = c.label;
7016
+ colSel.append(o);
7017
+ }
7018
+ const opSel = el("select", "fg-filter-op fg-fb-op", { "aria-label": loc.filter });
7019
+ const value = el("input", "fg-input fg-fb-value", { type: "text", placeholder: loc.value, "aria-label": loc.value });
7020
+ const valueTo = el("input", "fg-input fg-fb-value-to", { type: "text", placeholder: loc.valueTo, "aria-label": loc.valueTo });
7021
+ const del = el("button", "fg-chooser-close fg-fb-remove", { type: "button", "aria-label": loc.close });
7022
+ del.textContent = "\xD7";
7023
+ const kindOf = () => cols.find((c) => c.id === colSel.value)?.kind ?? "text";
7024
+ const fillOps = (keep) => {
7025
+ const kind = kindOf();
7026
+ opSel.replaceChildren(...OPS[kind].map((op) => {
7027
+ const o = el("option");
7028
+ o.value = op;
7029
+ o.textContent = loc.ops[op];
7030
+ return o;
7031
+ }));
7032
+ if (keep && OPS[kind].includes(keep)) opSel.value = keep;
7033
+ const type = kind === "text" ? "text" : kind;
7034
+ value.type = valueTo.type = type;
7035
+ sync();
7036
+ };
7037
+ const sync = () => {
7038
+ value.hidden = NO_VALUE2.includes(opSel.value);
7039
+ valueTo.hidden = opSel.value !== "between";
7040
+ };
7041
+ colSel.addEventListener("change", () => fillOps(opSel.value));
7042
+ opSel.addEventListener("change", sync);
7043
+ if (start && cols.some((c) => c.id === start.colId)) colSel.value = start.colId;
7044
+ const m = start?.model;
7045
+ fillOps(m?.op);
7046
+ if (m?.value != null) value.value = String(m.value);
7047
+ if (m?.valueTo != null) valueTo.value = String(m.valueTo);
7048
+ rowEl.append(colSel, opSel, value, valueTo, del);
7049
+ const state = {
7050
+ el: rowEl,
7051
+ read: () => {
7052
+ const kind = kindOf();
7053
+ const op = opSel.value;
7054
+ const needs = !NO_VALUE2.includes(op);
7055
+ let model = null;
7056
+ if (kind === "number") {
7057
+ const a = value.value === "" ? void 0 : Number(value.value);
7058
+ const b = valueTo.value === "" ? void 0 : Number(valueTo.value);
7059
+ if (!needs || a !== void 0 && !Number.isNaN(a)) model = op === "between" ? { type: "number", op, value: a, valueTo: b } : { type: "number", op, value: a };
7060
+ } else if (kind === "date") {
7061
+ if (!needs || value.value) model = op === "between" ? { type: "date", op, value: value.value, valueTo: valueTo.value } : { type: "date", op, value: value.value || void 0 };
7062
+ } else if (!needs || value.value) model = { type: "text", op, value: value.value };
7063
+ return model ? { colId: colSel.value, model } : null;
7064
+ }
7065
+ };
7066
+ del.addEventListener("click", () => remove(state));
7067
+ return state;
7068
+ };
7069
+ const groupRow = (g) => {
7070
+ const rowEl = el("div", "fg-fb-row fg-fb-group");
7071
+ const text2 = el("span", "fg-fb-group-label");
7072
+ text2.textContent = `( ${g.conditions.length} \xD7 ${loc[g.operator]} )`;
7073
+ const del = el("button", "fg-chooser-close fg-fb-remove", { type: "button", "aria-label": loc.close });
7074
+ del.textContent = "\xD7";
7075
+ rowEl.append(text2, del);
7076
+ const state = { el: rowEl, read: () => g };
7077
+ del.addEventListener("click", () => remove(state));
7078
+ return state;
7079
+ };
7080
+ const add = (row) => {
7081
+ rows.push(row);
7082
+ list.append(row.el);
7083
+ };
7084
+ for (const c of cur?.conditions ?? []) add("conditions" in c ? groupRow(c) : conditionRow(c));
7085
+ const addBtn = el("button", "fg-btn fg-fb-add", { type: "button" });
7086
+ addBtn.textContent = `+ ${loc.addCondition}`;
7087
+ addBtn.addEventListener("click", () => {
7088
+ const row = conditionRow();
7089
+ add(row);
7090
+ row.el.querySelector(".fg-fb-value").focus();
7091
+ });
7092
+ const actions = el("div", "fg-menu-actions");
7093
+ const reset = el("button", "fg-btn", { type: "button" });
7094
+ reset.textContent = loc.reset;
7095
+ reset.addEventListener("click", () => {
7096
+ grid.setAdvancedFilter(null);
7097
+ this.close();
7098
+ });
7099
+ const ok = el("button", "fg-btn fg-btn-primary", { type: "button" });
7100
+ ok.textContent = loc.apply;
7101
+ ok.addEventListener("click", () => {
7102
+ const conditions = rows.map((r2) => r2.read()).filter((c) => !!c);
7103
+ const operator = join.querySelector("input:checked")?.value === "or" ? "or" : "and";
7104
+ grid.setAdvancedFilter(conditions.length ? { operator, conditions } : null);
7105
+ this.close();
7106
+ });
7107
+ actions.append(reset, ok);
7108
+ popup.append(head, join, list, addBtn, actions);
7109
+ popupParent(grid.renderer.root).append(popup);
7110
+ const r = grid.renderer.root.getBoundingClientRect();
7111
+ const w = popup.offsetWidth || 460;
7112
+ popup.style.left = `${Math.max(4, Math.min(r.left + 24, (window.innerWidth || 1024) - w - 4))}px`;
7113
+ popup.style.top = `${Math.max(4, r.top + 24)}px`;
7114
+ this.popup = popup;
7115
+ if (!rows.length) addBtn.click();
7116
+ const onDown = (e) => {
7117
+ if (!eventInside(e, popup)) this.close();
7118
+ };
7119
+ const onKey = (e) => {
7120
+ if (e.key === "Escape") this.close();
7121
+ };
7122
+ document.addEventListener("pointerdown", onDown, true);
7123
+ document.addEventListener("keydown", onKey, true);
7124
+ this.off.push(
7125
+ () => document.removeEventListener("pointerdown", onDown, true),
7126
+ () => document.removeEventListener("keydown", onKey, true)
7127
+ );
7128
+ }
7129
+ close() {
7130
+ for (const f of this.off) f();
7131
+ this.off = [];
7132
+ this.popup?.remove();
7133
+ this.popup = null;
7134
+ }
7135
+ };
7136
+
6038
7137
  // src/grid.ts
6039
7138
  var autoRowHeight = (size) => Math.round(size * 2.75);
6040
7139
  var autoHeaderHeight = (size) => Math.round(size * 3.1);
6041
7140
  var DEFAULT_FONT_SIZE = 13;
7141
+ var DENSITY = { compact: [0.8, 0.85], normal: [1, 1], comfortable: [1.25, 1.15] };
6042
7142
  var NO_GROUPS = [];
6043
7143
  var DEFAULT_PAGE_SIZE = 100;
6044
7144
  var NO_SPANS = /* @__PURE__ */ new Map();
@@ -6078,6 +7178,18 @@ var Grid = class {
6078
7178
  /** 0-based page and rows per page (pagination only). */
6079
7179
  this.page = 0;
6080
7180
  this.pageSize = DEFAULT_PAGE_SIZE;
7181
+ // ── pagination and server rows ──────────────────────────────
7182
+ /** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
7183
+ /** showOverlay: an overlay the app asked for, shown whatever the row count. */
7184
+ this.forcedOverlay = null;
7185
+ this.filterBuilder = new FilterBuilder(this);
7186
+ this.flashing = /* @__PURE__ */ new Map();
7187
+ this.flashTimer = 0;
7188
+ this.txQueue = [];
7189
+ this.txTimer = 0;
7190
+ /** A transaction is replacing the data: keep the selected range. */
7191
+ this.keepRange = false;
7192
+ this.rowNumberCache = null;
6081
7193
  /** Tree rows whose children were loaded (or are loading) through treeData.loadChildren. */
6082
7194
  this.treeLoaded = /* @__PURE__ */ new Set();
6083
7195
  this.treeLoading = /* @__PURE__ */ new Set();
@@ -6089,6 +7201,9 @@ var Grid = class {
6089
7201
  return false;
6090
7202
  }
6091
7203
  };
7204
+ this.density = "normal";
7205
+ /** Bumped on every selection change (group row checkboxes depend on their rows). */
7206
+ this.selectionVersion = 0;
6092
7207
  if (!Array.isArray(options.columns)) warn("options.columns must be an array");
6093
7208
  if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
6094
7209
  this.store = new Store({
@@ -6101,6 +7216,7 @@ var Grid = class {
6101
7216
  focus: null,
6102
7217
  range: null,
6103
7218
  extraRanges: [],
7219
+ advancedFilter: null,
6104
7220
  locale: resolveLocale(options.locale),
6105
7221
  theme: options.theme ?? "light"
6106
7222
  });
@@ -6137,9 +7253,11 @@ var Grid = class {
6137
7253
  this.font = { size: explicit, family: options.fontFamily ?? null };
6138
7254
  const size = explicit ?? DEFAULT_FONT_SIZE;
6139
7255
  this.renderer = new Renderer(host, this, {
6140
- rowHeight: options.rowHeight ?? autoRowHeight(size),
6141
- headerHeight: options.headerHeight ?? autoHeaderHeight(size)
7256
+ rowHeight: options.rowHeight ?? Math.round(autoRowHeight(size) * DENSITY[this.densityOf(options.density)][0]),
7257
+ headerHeight: options.headerHeight ?? Math.round(autoHeaderHeight(size) * DENSITY[this.densityOf(options.density)][1])
6142
7258
  });
7259
+ this.density = this.densityOf(options.density);
7260
+ this.renderer.applyTheme();
6143
7261
  this.renderer.applyFont(this.font.size, this.font.family);
6144
7262
  this.menu = new ColumnMenu(this);
6145
7263
  this.chooser = new ColumnChooserDialog(this);
@@ -6164,7 +7282,9 @@ var Grid = class {
6164
7282
  () => this.editor.cancel(),
6165
7283
  () => this.fillHandle.destroy(),
6166
7284
  () => this.groupPanel?.destroy(),
6167
- () => this.charts.destroy()
7285
+ () => this.charts.destroy(),
7286
+ () => this.filterBuilder.close(),
7287
+ attachTooltips(this)
6168
7288
  );
6169
7289
  if (typeof ResizeObserver !== "undefined") {
6170
7290
  const ro = new ResizeObserver(() => this.renderer.schedule());
@@ -6313,7 +7433,181 @@ var Grid = class {
6313
7433
  this.renderer.invalidate({ header: true, rows: true });
6314
7434
  this.refreshChoosers();
6315
7435
  }
6316
- // ── pagination and server rows ──────────────────────────────
7436
+ isFlashing(rowId, colId) {
7437
+ return this.flashing.size > 0 && this.flashing.has(`${rowId}\0${colId}`);
7438
+ }
7439
+ flash(keys) {
7440
+ const until = Date.now() + 1e3;
7441
+ for (const k of keys) this.flashing.set(k, until);
7442
+ if (this.flashTimer) return;
7443
+ const tick = () => {
7444
+ const now = Date.now();
7445
+ for (const [k, t] of this.flashing) if (t <= now) this.flashing.delete(k);
7446
+ this.renderer.invalidate({ rows: true });
7447
+ this.flashTimer = this.flashing.size && !this.destroyed ? window.setTimeout(tick, 250) : 0;
7448
+ };
7449
+ this.flashTimer = window.setTimeout(tick, 1e3);
7450
+ }
7451
+ applyTransaction(tx) {
7452
+ const result = { added: [], updated: [], removed: [], notFound: [] };
7453
+ if (this.clientOnly("applyTransaction") || !tx || typeof tx !== "object") return result;
7454
+ const data = this.store.state.data;
7455
+ const getId = this.options.getRowId;
7456
+ const keyOf = (r) => {
7457
+ if (!r || typeof r !== "object") return null;
7458
+ if (data.includes(r)) return this.rowModel.idOf(r);
7459
+ try {
7460
+ return getId ? String(getId(r)) : null;
7461
+ } catch {
7462
+ return null;
7463
+ }
7464
+ };
7465
+ const byId = new Map(data.map((r) => [this.rowModel.idOf(r), r]));
7466
+ const gone = /* @__PURE__ */ new Set();
7467
+ for (const r of tx.remove ?? []) {
7468
+ const k = keyOf(r);
7469
+ const row = k === null ? void 0 : byId.get(k);
7470
+ if (row && !gone.has(row)) {
7471
+ gone.add(row);
7472
+ result.removed.push(row);
7473
+ } else if (!row) result.notFound.push(r);
7474
+ }
7475
+ const colsByField = /* @__PURE__ */ new Map();
7476
+ for (const st of this.columns.getState()) {
7477
+ const f = this.columns.getNode(st.colId)?.def.field;
7478
+ if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
7479
+ }
7480
+ const flashKeys = [];
7481
+ for (const r of tx.update ?? []) {
7482
+ const k = keyOf(r);
7483
+ const row = k === null ? void 0 : byId.get(k);
7484
+ if (!row || gone.has(row)) {
7485
+ result.notFound.push(r);
7486
+ continue;
7487
+ }
7488
+ for (const [field, value] of Object.entries(r)) {
7489
+ if (Object.is(row[field], value)) continue;
7490
+ row[field] = value;
7491
+ for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
7492
+ }
7493
+ result.updated.push(row);
7494
+ }
7495
+ const added = cleanRows(Array.isArray(tx.add) ? tx.add : []);
7496
+ result.added.push(...added);
7497
+ if (!result.added.length && !result.updated.length && !result.removed.length) {
7498
+ if (result.notFound.length) this.events.emit("transactionApplied", result);
7499
+ return result;
7500
+ }
7501
+ const next = gone.size ? data.filter((r) => !gone.has(r)) : [...data];
7502
+ const at = typeof tx.addIndex === "number" ? Math.max(0, Math.min(next.length, Math.floor(tx.addIndex))) : next.length;
7503
+ next.splice(at, 0, ...added);
7504
+ this.editVersion++;
7505
+ this.renderer.invalidateHeights();
7506
+ this.keepRange = true;
7507
+ try {
7508
+ this.replaceData(next);
7509
+ } finally {
7510
+ this.keepRange = false;
7511
+ }
7512
+ if (flashKeys.length && this.options.flashCells !== false) this.flash(flashKeys);
7513
+ this.events.emit("transactionApplied", result);
7514
+ return result;
7515
+ }
7516
+ applyTransactionAsync(tx) {
7517
+ if (!tx || typeof tx !== "object") return;
7518
+ this.txQueue.push(tx);
7519
+ if (this.txTimer) return;
7520
+ this.txTimer = window.setTimeout(() => {
7521
+ this.txTimer = 0;
7522
+ const queue = this.txQueue;
7523
+ this.txQueue = [];
7524
+ if (this.destroyed || !queue.length) return;
7525
+ this.applyTransaction({
7526
+ add: queue.flatMap((t) => t.add ?? []),
7527
+ addIndex: queue.find((t) => typeof t.addIndex === "number")?.addIndex,
7528
+ update: queue.flatMap((t) => t.update ?? []),
7529
+ remove: queue.flatMap((t) => t.remove ?? [])
7530
+ });
7531
+ }, 50);
7532
+ }
7533
+ setAdvancedFilter(filter) {
7534
+ const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
7535
+ if (next === this.store.state.advancedFilter) return;
7536
+ this.store.set({ advancedFilter: next });
7537
+ this.events.emit("advancedFilterChange", { advancedFilter: next });
7538
+ }
7539
+ setColumnGroupOpened(groupId, open) {
7540
+ if (!this.columns.setGroupOpen(groupId, !!open)) return;
7541
+ this.layoutChanged();
7542
+ this.events.emit("columnGroupOpened", { groupId, open: !!open });
7543
+ }
7544
+ getState() {
7545
+ const s = this.store.state;
7546
+ const state = {
7547
+ columns: this.columns.getState(),
7548
+ columnGroups: this.columns.groupOpenState(),
7549
+ advancedFilter: s.advancedFilter ? JSON.parse(JSON.stringify(s.advancedFilter)) : null,
7550
+ sort: s.sort.map((x) => ({ ...x })),
7551
+ filters: JSON.parse(JSON.stringify(s.filters)),
7552
+ quickFilter: s.quickFilter,
7553
+ groupBy: [...this.groupBy],
7554
+ pivot: this.pivotCfg ? JSON.parse(JSON.stringify(this.pivotCfg)) : null,
7555
+ expanded: Object.fromEntries(this.expanded),
7556
+ selected: [...s.selected]
7557
+ };
7558
+ if (this.options.pagination) {
7559
+ state.page = this.page;
7560
+ state.pageSize = this.pageSize;
7561
+ }
7562
+ return state;
7563
+ }
7564
+ setState(state, options = {}) {
7565
+ if (!state || typeof state !== "object") return;
7566
+ const want = (k) => state[k] !== void 0 && (!options.parts || options.parts.includes(k));
7567
+ const known = (id) => !!this.columns.getNode(id);
7568
+ if (want("columns") && Array.isArray(state.columns)) {
7569
+ this.columns.applyState(state.columns.filter((c) => c && known(c.colId)));
7570
+ this.columnsChanged();
7571
+ }
7572
+ if (want("columnGroups") && state.columnGroups && typeof state.columnGroups === "object") {
7573
+ for (const [id, open] of Object.entries(state.columnGroups)) if (typeof open === "boolean") this.setColumnGroupOpened(id, open);
7574
+ }
7575
+ if (want("groupBy") && Array.isArray(state.groupBy)) this.setGroupBy(state.groupBy.filter(known));
7576
+ if (want("pivot")) this.setPivot(state.pivot ?? null);
7577
+ if (want("sort") && Array.isArray(state.sort)) this.setSort(state.sort.filter((x) => x && known(x.colId)));
7578
+ if (want("filters") && state.filters && typeof state.filters === "object") {
7579
+ const filters = Object.fromEntries(Object.entries(state.filters).filter(([id]) => known(id)));
7580
+ this.store.set({ filters });
7581
+ this.events.emit("filterChange", { filters: { ...filters } });
7582
+ }
7583
+ if (want("advancedFilter")) this.setAdvancedFilter(state.advancedFilter ?? null);
7584
+ if (want("quickFilter") && typeof state.quickFilter === "string") this.store.set({ quickFilter: state.quickFilter });
7585
+ if (want("expanded") && state.expanded && typeof state.expanded === "object") {
7586
+ this.expanded = new Map(Object.entries(state.expanded).filter(([, v]) => typeof v === "boolean"));
7587
+ this.displayChanged();
7588
+ }
7589
+ if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
7590
+ if (want("page") && typeof state.page === "number") this.setPage(state.page);
7591
+ if (want("selected") && Array.isArray(state.selected)) {
7592
+ const ids = new Set(this.rowModel.allNodes().map((n) => n.id));
7593
+ this.setSelected(new Set(state.selected.map(String).filter((id) => ids.has(id))));
7594
+ }
7595
+ }
7596
+ /** Number shown in the row-number column for display row `i` (data rows only; continues across pages). */
7597
+ rowNumber(i) {
7598
+ const rows = this.rowModel.rows();
7599
+ if (this.rowNumberCache?.rows !== rows) {
7600
+ const nums = new Int32Array(rows.length);
7601
+ let n = 0;
7602
+ rows.forEach((r, k) => {
7603
+ nums[k] = r.kind ? 0 : ++n;
7604
+ });
7605
+ this.rowNumberCache = { rows, nums };
7606
+ }
7607
+ const v = this.rowNumberCache.nums[i];
7608
+ if (!v) return null;
7609
+ return v + (this.options.pagination && !this.rowModel.server ? this.page * this.pageSize : 0);
7610
+ }
6317
7611
  getPage() {
6318
7612
  return this.options.pagination ? this.page : 0;
6319
7613
  }
@@ -6428,7 +7722,6 @@ var Grid = class {
6428
7722
  }
6429
7723
  validGroupBy(ids) {
6430
7724
  if (!Array.isArray(ids)) return [];
6431
- if (ids.length && this.clientOnly("grouping")) return [];
6432
7725
  return [...new Set(ids)].filter((id) => {
6433
7726
  const node = this.columns.getNode(id);
6434
7727
  if (!node || node.children || isSystemCol(id)) {
@@ -6515,6 +7808,7 @@ var Grid = class {
6515
7808
  groupAggregate(node, colId) {
6516
7809
  const def = this.columns.getNode(colId)?.def;
6517
7810
  if (!node.group || !def?.aggFunc) return void 0;
7811
+ if (node.group.aggregates) return node.group.aggregates[colId] ?? (def.field ? node.group.aggregates[def.field] : void 0);
6518
7812
  if (this.aggCache.ver !== this.editVersion) this.aggCache = { ver: this.editVersion, map: /* @__PURE__ */ new WeakMap() };
6519
7813
  let m = this.aggCache.map.get(node.group);
6520
7814
  if (!m) this.aggCache.map.set(node.group, m = /* @__PURE__ */ new Map());
@@ -6688,13 +7982,30 @@ var Grid = class {
6688
7982
  const family = font.family === void 0 ? this.font.family : font.family || null;
6689
7983
  this.font = { size, family };
6690
7984
  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);
7985
+ this.applyAutoHeights(effective);
6693
7986
  this.renderer.applyFont(size, family);
6694
7987
  this.renderer.invalidateHeights();
6695
7988
  this.menu.close();
6696
7989
  this.layoutChanged();
6697
7990
  }
7991
+ densityOf(d) {
7992
+ return d === "compact" || d === "comfortable" ? d : "normal";
7993
+ }
7994
+ /** Row and header heights from the font size and density, unless the options fix them. */
7995
+ applyAutoHeights(fontSize) {
7996
+ const [r, h] = DENSITY[this.density];
7997
+ if (this.options.rowHeight == null) this.renderer.opts.rowHeight = Math.round(autoRowHeight(fontSize) * r);
7998
+ if (this.options.headerHeight == null) this.renderer.opts.headerHeight = Math.round(autoHeaderHeight(fontSize) * h);
7999
+ }
8000
+ setDensity(d) {
8001
+ const next = this.densityOf(d);
8002
+ if (next === this.density) return;
8003
+ this.density = next;
8004
+ this.renderer.applyTheme();
8005
+ this.applyAutoHeights(this.font.size ?? DEFAULT_FONT_SIZE);
8006
+ this.renderer.invalidateHeights();
8007
+ this.layoutChanged();
8008
+ }
6698
8009
  /** Copy the grid font variables onto an element rendered outside the root (popups, drag ghost). */
6699
8010
  applyFontTo(target) {
6700
8011
  if (this.font.size) target.style.setProperty("--fg-font-size", `${this.font.size}px`);
@@ -6857,6 +8168,12 @@ var Grid = class {
6857
8168
  }
6858
8169
  return copyValue(def, value, code);
6859
8170
  }
8171
+ /** Copy, then clear the editable cells of the selection. */
8172
+ cutToClipboard() {
8173
+ const text2 = this.copyToClipboard(false);
8174
+ if (text2) this.clearCells();
8175
+ return text2;
8176
+ }
6860
8177
  copyToClipboard(headers, rows = false) {
6861
8178
  const text2 = this.selectionTSV(headers, rows);
6862
8179
  if (text2 && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
@@ -7346,6 +8663,7 @@ var Grid = class {
7346
8663
  this.events.emit("focusChange", { rowIndex, colId });
7347
8664
  }
7348
8665
  setSelected(selected, anchorId = this.store.state.anchorId) {
8666
+ this.selectionVersion++;
7349
8667
  this.store.set({ selected, anchorId });
7350
8668
  this.events.emit("selectionChange", { selected: this.selectedRows() });
7351
8669
  }
@@ -7359,6 +8677,55 @@ var Grid = class {
7359
8677
  return false;
7360
8678
  }
7361
8679
  }
8680
+ /** Selectable data row ids under a group row, or the descendants of a tree row. */
8681
+ groupMemberIds(node) {
8682
+ if (node.group) return node.group.rows.filter((r) => this.rowSelectable(r)).map((r) => this.rowModel.idOf(r));
8683
+ const getParent = this.options.treeData?.getParentId;
8684
+ if (!getParent) return [];
8685
+ const kids = /* @__PURE__ */ new Map();
8686
+ for (const r of this.store.state.data) {
8687
+ let p = null;
8688
+ try {
8689
+ p = getParent(r);
8690
+ } catch {
8691
+ p = null;
8692
+ }
8693
+ if (p == null) continue;
8694
+ const k = String(p);
8695
+ const list = kids.get(k);
8696
+ if (list) list.push(r);
8697
+ else kids.set(k, [r]);
8698
+ }
8699
+ const out = [];
8700
+ const seen2 = /* @__PURE__ */ new Set([node.id]);
8701
+ const queue = [node.id];
8702
+ while (queue.length) {
8703
+ for (const r of kids.get(queue.shift()) ?? []) {
8704
+ const id = this.rowModel.idOf(r);
8705
+ if (seen2.has(id)) continue;
8706
+ seen2.add(id);
8707
+ queue.push(id);
8708
+ if (this.rowSelectable(r)) out.push(id);
8709
+ }
8710
+ }
8711
+ return out;
8712
+ }
8713
+ /** 'all', 'some' or 'none' of a group row's rows are selected. */
8714
+ groupSelection(node) {
8715
+ const ids = this.groupMemberIds(node);
8716
+ const sel = this.store.state.selected;
8717
+ const n = ids.filter((id) => sel.has(id)).length;
8718
+ return n === 0 ? "none" : n === ids.length ? "all" : "some";
8719
+ }
8720
+ /** Group row checkbox: select every row of the group, or clear them when all were selected. */
8721
+ toggleGroupSelection(node) {
8722
+ if (this.options.rowSelection !== "multiple") return;
8723
+ const ids = this.groupMemberIds(node);
8724
+ const next = new Set(this.store.state.selected);
8725
+ const all2 = ids.length > 0 && ids.every((id) => next.has(id));
8726
+ for (const id of ids) all2 ? next.delete(id) : next.add(id);
8727
+ this.setSelected(next);
8728
+ }
7362
8729
  selectFromClick(index, toggle, range) {
7363
8730
  const mode = this.options.rowSelection;
7364
8731
  if (!mode || index < 0 || index >= this.rowModel.getRowCount()) return;
@@ -7381,6 +8748,10 @@ var Grid = class {
7381
8748
  if (n && (n.kind || !this.rowSelectable(n.data))) r.selected.delete(id);
7382
8749
  }
7383
8750
  }
8751
+ if (mode === "multiple" && this.options.groupSelectsChildren && this.options.treeData && target.hasChildren) {
8752
+ const on = r.selected.has(target.id);
8753
+ for (const id of this.groupMemberIds(target)) on ? r.selected.add(id) : r.selected.delete(id);
8754
+ }
7384
8755
  this.setSelected(r.selected, this.rowModel.getRow(r.anchorIndex)?.id ?? null);
7385
8756
  }
7386
8757
  /** Displayed index of the selection anchor, or null when it is not displayed (filtered out or removed). */
@@ -7412,6 +8783,11 @@ var Grid = class {
7412
8783
  }
7413
8784
  withSelect(defs) {
7414
8785
  const sys = [];
8786
+ const rn = this.options.rowNumbers;
8787
+ if (rn) {
8788
+ const o = typeof rn === "object" ? rn : {};
8789
+ sys.push(rowNumberColumn(typeof o.width === "number" && o.width > 0 ? o.width : void 0, typeof o.header === "string" ? o.header : void 0));
8790
+ }
7415
8791
  if (this.options.rowDrag) sys.push(DRAG_COLUMN);
7416
8792
  if (this.options.rowSelection) sys.push(SELECT_COLUMN);
7417
8793
  return [...sys, ...defs];
@@ -7478,21 +8854,21 @@ var Grid = class {
7478
8854
  this.sideBar?.refresh();
7479
8855
  this.rowModel.invalidate();
7480
8856
  }
7481
- const resetPage = has("sort", "filters", "quickFilter") && this.page !== 0;
8857
+ const resetPage = has("sort", "filters", "quickFilter", "advancedFilter") && this.page !== 0;
7482
8858
  if (resetPage) this.page = 0;
7483
- if (has("data", "sort", "filters", "quickFilter", "locale")) this.updateRows();
8859
+ if (has("data", "sort", "filters", "quickFilter", "advancedFilter", "locale")) this.updateRows();
7484
8860
  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") {
8861
+ if (has("data", "sort", "filters", "quickFilter", "advancedFilter") && this.store.state.range && !this.keepRange) this.setRange(null);
8862
+ if (has("data", "sort", "filters", "quickFilter", "advancedFilter", "locale")) this.finder.refresh();
8863
+ if (has("data", "filters", "quickFilter", "advancedFilter", "locale") || has("selected") && this.summaryOpts?.scope === "selected") {
7488
8864
  this.refreshSummary();
7489
8865
  }
7490
8866
  if (has("locale")) this.groupPanel?.refresh();
7491
8867
  if (has("filters")) this.sideBar?.refresh();
7492
8868
  if (has("theme")) this.renderer.applyTheme();
7493
8869
  this.renderer.invalidate({
7494
- header: has("sort", "filters", "quickFilter", "locale", "selected", "data"),
7495
- rows: has("data", "sort", "filters", "quickFilter", "locale")
8870
+ header: has("sort", "filters", "quickFilter", "advancedFilter", "locale", "selected", "data"),
8871
+ rows: has("data", "sort", "filters", "quickFilter", "advancedFilter", "locale")
7496
8872
  });
7497
8873
  }
7498
8874
  createApi() {
@@ -7523,6 +8899,24 @@ var Grid = class {
7523
8899
  getDisplayedRowCount: guard(() => this.rowModel.getRowCount()),
7524
8900
  scrollToRow: guard((i) => this.renderer.scrollToRow(i)),
7525
8901
  validate: guard(() => this.validateAll()),
8902
+ setDensity: guard((d) => this.setDensity(d)),
8903
+ getDensity: guard(() => this.density),
8904
+ showOverlay: guard((kind) => {
8905
+ this.forcedOverlay = kind === "loading" || kind === "noRows" ? kind : null;
8906
+ this.renderer.invalidate();
8907
+ }),
8908
+ getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
8909
+ print: guard((o) => printHtml(buildPrintHtml(this, o))),
8910
+ getPrintHtml: guard((o) => buildPrintHtml(this, o)),
8911
+ setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
8912
+ setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
8913
+ getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
8914
+ openFilterBuilder: guard(() => this.filterBuilder.open()),
8915
+ isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
8916
+ applyTransaction: guard((tx) => this.applyTransaction(tx)),
8917
+ applyTransactionAsync: guard((tx) => this.applyTransactionAsync(tx)),
8918
+ getState: guard(() => this.getState()),
8919
+ setState: guard((s, o) => this.setState(s, o)),
7526
8920
  getScrollPosition: guard(() => this.renderer.getScroll()),
7527
8921
  setScrollPosition: guard((p) => this.renderer.setScroll(p ?? {})),
7528
8922
  syncScroll: guard((other, axis = "both") => this.syncScroll(other, axis)),
@@ -7546,6 +8940,8 @@ var Grid = class {
7546
8940
  setRanges: guard((list) => this.setRanges(list)),
7547
8941
  copySelection: guard((o) => this.copyToClipboard(!!o?.headers, !!o?.rows)),
7548
8942
  pasteText: guard((text2) => this.pasteText(text2)),
8943
+ cutSelection: guard(() => this.cutToClipboard()),
8944
+ stopRowEdit: guard((cancel) => this.editor.endRow(!cancel)),
7549
8945
  startEdit: guard((rowIndex, colId) => this.editor.start(rowIndex, colId)),
7550
8946
  stopEdit: guard((cancel) => cancel ? this.editor.cancel() : void this.editor.commit(null)),
7551
8947
  addRows: guard((rows, index) => this.addRows(rows, index)),
@@ -7597,6 +8993,8 @@ var Grid = class {
7597
8993
  if (this.destroyed) return;
7598
8994
  this.destroyed = true;
7599
8995
  liveGrids.delete(this.renderer.root);
8996
+ clearTimeout(this.flashTimer);
8997
+ clearTimeout(this.txTimer);
7600
8998
  for (const d of this.disposers) d();
7601
8999
  this.renderer.destroy();
7602
9000
  this.events.clear();
@@ -7605,14 +9003,95 @@ var Grid = class {
7605
9003
  function createGrid(host, options) {
7606
9004
  return new Grid(host, options).api;
7607
9005
  }
9006
+
9007
+ // src/workbook.ts
9008
+ function buildWorkbook(sheets) {
9009
+ const list = (Array.isArray(sheets) ? sheets : []).filter((s) => s && s.grid && typeof s.grid.getXlsxSheet === "function");
9010
+ if (!list.length) {
9011
+ warn("buildWorkbook: no grids given; the file has one empty sheet");
9012
+ return writeXlsx([{ name: "Sheet1", rows: [] }]);
9013
+ }
9014
+ return writeXlsx(list.map((s, i) => s.grid.getXlsxSheet({ ...s.options, sheetName: s.sheetName ?? s.options?.sheetName ?? `Sheet${i + 1}` })));
9015
+ }
9016
+ function exportWorkbook(filename, sheets) {
9017
+ download(filename || "export.xlsx", [buildWorkbook(sheets)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
9018
+ }
9019
+
9020
+ // src/rest.ts
9021
+ var headersOf = (h) => typeof h === "function" ? h() : h ?? {};
9022
+ async function send(url, method, body, opts) {
9023
+ let target = url;
9024
+ const init = { method, credentials: opts.credentials ?? "same-origin", headers: { Accept: "application/json", ...headersOf(opts.headers) } };
9025
+ if (method === "GET") {
9026
+ const q = new URLSearchParams();
9027
+ for (const [k, v] of Object.entries(body)) q.set(k, typeof v === "string" ? v : JSON.stringify(v));
9028
+ target += (url.includes("?") ? "&" : "?") + q.toString();
9029
+ } else {
9030
+ init.headers["Content-Type"] = "application/json";
9031
+ init.body = JSON.stringify(body);
9032
+ }
9033
+ const res = await fetch(target, init);
9034
+ if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
9035
+ const text2 = await res.text();
9036
+ return text2 ? JSON.parse(text2) : null;
9037
+ }
9038
+ function readRows(json) {
9039
+ if (Array.isArray(json)) return { rows: json };
9040
+ const o = json ?? {};
9041
+ const rows = [o.rows, o.list, o.content, o.data, o.items, o.resultList].find(Array.isArray);
9042
+ const total = [o.total, o.totalCount, o.totalElements, o.totCnt, o.count].find((v) => typeof v === "number");
9043
+ return { rows: rows ?? [], total };
9044
+ }
9045
+ function restDatasource(url, options = {}) {
9046
+ return {
9047
+ async getRows(p) {
9048
+ const size = Math.max(1, p.end - p.start);
9049
+ const body = {
9050
+ start: p.start,
9051
+ end: p.end,
9052
+ page: Math.floor(p.start / size),
9053
+ size,
9054
+ sort: p.sort,
9055
+ filters: p.filters,
9056
+ quickFilter: p.quickFilter,
9057
+ ...options.params?.() ?? {}
9058
+ };
9059
+ const json = await send(url, options.method ?? "POST", body, options);
9060
+ return (options.transform ?? readRows)(json);
9061
+ }
9062
+ };
9063
+ }
9064
+ async function saveChanges(grid, url, options = {}) {
9065
+ if (!grid.hasChanges()) return null;
9066
+ const { created, updated, deleted } = grid.getModifiedRows();
9067
+ const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
9068
+ grid.commitChanges();
9069
+ return json;
9070
+ }
9071
+ async function loadData(grid, url, options = {}) {
9072
+ const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });
9073
+ grid.setData(rows);
9074
+ return rows;
9075
+ }
9076
+ function csrfHeaders(doc = document) {
9077
+ const token = doc.querySelector('meta[name="_csrf"]')?.content;
9078
+ const header2 = doc.querySelector('meta[name="_csrf_header"]')?.content || "X-CSRF-TOKEN";
9079
+ return token ? { [header2]: token } : {};
9080
+ }
7608
9081
  export {
9082
+ buildWorkbook,
7609
9083
  createGrid,
9084
+ csrfHeaders,
7610
9085
  en_default as en,
9086
+ exportWorkbook,
9087
+ loadData,
7611
9088
  parseCsv,
7612
9089
  readXlsx,
7613
9090
  registerLocale,
7614
9091
  renderChart,
7615
9092
  resolveLocale,
9093
+ restDatasource,
9094
+ saveChanges,
7616
9095
  setLicenseKey,
7617
9096
  verifyLicense,
7618
9097
  version,