@freegridjs/core 0.7.1 → 0.8.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
@@ -50,6 +50,15 @@ function filterTypeOf(def) {
50
50
  return "text";
51
51
  }
52
52
  }
53
+ function cellEditable(def, row, rowIndex) {
54
+ const e = def.editable;
55
+ if (typeof e !== "function") return !!e;
56
+ try {
57
+ return !!e({ value: def.valueGetter ? def.valueGetter(row) : def.field ? row[def.field] : void 0, row, rowIndex, column: def });
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
53
62
  var isSortable = (def) => def.id !== SELECT_COL_ID && def.sortable !== false;
54
63
  var isResizable = (def) => def.id !== SELECT_COL_ID && def.resizable !== false;
55
64
  function clampWidth(def, w) {
@@ -387,8 +396,8 @@ function quickTokens(q) {
387
396
  const t = q.trim().toLowerCase();
388
397
  return t ? t.split(/\s+/) : [];
389
398
  }
390
- function matchesQuick(tokens, text) {
391
- return tokens.every((t) => text.includes(t));
399
+ function matchesQuick(tokens, text2) {
400
+ return tokens.every((t) => text2.includes(t));
392
401
  }
393
402
 
394
403
  // src/i18n/format.ts
@@ -445,13 +454,13 @@ function create(type, format, locale) {
445
454
  }
446
455
  case "date": {
447
456
  if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
448
- const pad2 = (n) => String(n).padStart(2, "0");
457
+ const pad3 = (n) => String(n).padStart(2, "0");
449
458
  const withTime = format === "datetime";
450
459
  return (v) => {
451
460
  const d = toDate(v);
452
461
  if (!d) return isBlank(v) ? "" : String(v);
453
- const day = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
454
- return withTime ? `${day} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` : day;
462
+ const day = `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}`;
463
+ return withTime ? `${day} ${pad3(d.getHours())}:${pad3(d.getMinutes())}` : day;
455
464
  };
456
465
  }
457
466
  let opts = DATE_PRESETS.date;
@@ -482,6 +491,216 @@ function getFormatter(def, locale) {
482
491
  return fmt;
483
492
  }
484
493
 
494
+ // src/formula.ts
495
+ var FormulaError = class extends Error {
496
+ constructor(code) {
497
+ super(code);
498
+ this.code = code;
499
+ }
500
+ };
501
+ function tokenize(src) {
502
+ const out = [];
503
+ let i = 0;
504
+ while (i < src.length) {
505
+ const c = src[i];
506
+ if (/\s/.test(c)) {
507
+ i++;
508
+ } else if (/[0-9.]/.test(c)) {
509
+ const m = /^(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/i.exec(src.slice(i));
510
+ if (!m) throw new FormulaError("#ERROR!");
511
+ out.push({ t: "num", v: Number(m[0]) });
512
+ i += m[0].length;
513
+ } else if (c === '"') {
514
+ let s = "";
515
+ i++;
516
+ for (; ; ) {
517
+ if (i >= src.length) throw new FormulaError("#ERROR!");
518
+ if (src[i] === '"') {
519
+ if (src[i + 1] === '"') {
520
+ s += '"';
521
+ i += 2;
522
+ continue;
523
+ }
524
+ i++;
525
+ break;
526
+ }
527
+ s += src[i++];
528
+ }
529
+ out.push({ t: "str", v: s });
530
+ } else if (c === "[") {
531
+ const end = src.indexOf("]", i);
532
+ if (end < 0) throw new FormulaError("#ERROR!");
533
+ out.push({ t: "id", v: src.slice(i + 1, end) });
534
+ i = end + 1;
535
+ } else if (/[A-Za-z_À-￿]/.test(c)) {
536
+ const m = /^[A-Za-z_À-￿][\wÀ-￿]*/.exec(src.slice(i));
537
+ out.push({ t: "id", v: m[0] });
538
+ i += m[0].length;
539
+ } else {
540
+ const two = src.slice(i, i + 2);
541
+ if (two === "<>" || two === "<=" || two === ">=") {
542
+ out.push({ t: "op", v: two });
543
+ i += 2;
544
+ } else if ("+-*/%^&=<>(),".includes(c)) {
545
+ out.push({ t: "op", v: c });
546
+ i++;
547
+ } else throw new FormulaError("#ERROR!");
548
+ }
549
+ }
550
+ return out;
551
+ }
552
+ var isBlankValue = (v) => v === null || v === void 0 || v === "";
553
+ function num(v) {
554
+ if (typeof v === "number") return v;
555
+ if (typeof v === "boolean") return v ? 1 : 0;
556
+ if (isBlankValue(v)) return 0;
557
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
558
+ throw new FormulaError("#VALUE!");
559
+ }
560
+ var text = (v) => isBlankValue(v) ? "" : typeof v === "boolean" ? v ? "TRUE" : "FALSE" : String(v);
561
+ function round(x, digits) {
562
+ const d = Math.trunc(digits);
563
+ const r = Number(`${Math.round(Number(`${Math.abs(x)}e${d}`))}e${-d}`);
564
+ return x < 0 ? -r : r;
565
+ }
566
+ var FUNCS = {
567
+ ROUND: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0),
568
+ ABS: (a) => Math.abs(num(a[0])),
569
+ MIN: (a) => a.length ? Math.min(...a.map(num)) : 0,
570
+ MAX: (a) => a.length ? Math.max(...a.map(num)) : 0,
571
+ SUM: (a) => a.reduce((s, v) => s + num(v), 0),
572
+ AVG: (a) => {
573
+ if (!a.length) throw new FormulaError("#DIV/0!");
574
+ return a.reduce((s, v) => s + num(v), 0) / a.length;
575
+ }
576
+ };
577
+ function compare(a, b) {
578
+ const bothNum = (typeof a === "number" || isBlankValue(a)) && (typeof b === "number" || isBlankValue(b));
579
+ if (bothNum) return num(a) - num(b);
580
+ return text(a).localeCompare(text(b), void 0, { sensitivity: "accent" });
581
+ }
582
+ var Parser = class {
583
+ constructor(tokens, resolve) {
584
+ this.tokens = tokens;
585
+ this.resolve = resolve;
586
+ this.i = 0;
587
+ }
588
+ parse() {
589
+ const v = this.comparison();
590
+ if (this.i < this.tokens.length) throw new FormulaError("#ERROR!");
591
+ return v;
592
+ }
593
+ peek(v) {
594
+ const t = this.tokens[this.i];
595
+ return !!t && t.t === "op" && (v === void 0 || t.v === v);
596
+ }
597
+ take(v) {
598
+ if (!this.peek(v)) return false;
599
+ this.i++;
600
+ return true;
601
+ }
602
+ comparison() {
603
+ const a = this.concat();
604
+ for (const op of ["=", "<>", "<=", ">=", "<", ">"]) {
605
+ if (this.take(op)) {
606
+ const c = compare(a, this.concat());
607
+ return op === "=" ? c === 0 : op === "<>" ? c !== 0 : op === "<" ? c < 0 : op === ">" ? c > 0 : op === "<=" ? c <= 0 : c >= 0;
608
+ }
609
+ }
610
+ return a;
611
+ }
612
+ concat() {
613
+ let v = this.additive();
614
+ while (this.take("&")) v = text(v) + text(this.additive());
615
+ return v;
616
+ }
617
+ additive() {
618
+ let v = this.multiplicative();
619
+ for (; ; ) {
620
+ if (this.take("+")) v = num(v) + num(this.multiplicative());
621
+ else if (this.take("-")) v = num(v) - num(this.multiplicative());
622
+ else return v;
623
+ }
624
+ }
625
+ multiplicative() {
626
+ let v = this.unary();
627
+ for (; ; ) {
628
+ if (this.take("*")) v = num(v) * num(this.unary());
629
+ else if (this.take("/") || this.take("%")) {
630
+ const op = this.tokens[this.i - 1].v;
631
+ const d = num(this.unary());
632
+ if (d === 0) throw new FormulaError("#DIV/0!");
633
+ v = op === "/" ? num(v) / d : num(v) % d;
634
+ } else return v;
635
+ }
636
+ }
637
+ unary() {
638
+ if (this.take("-")) return -num(this.unary());
639
+ if (this.take("+")) return num(this.unary());
640
+ return this.power();
641
+ }
642
+ power() {
643
+ const base = this.primary();
644
+ if (this.take("^")) return num(base) ** num(this.unary());
645
+ return base;
646
+ }
647
+ primary() {
648
+ const t = this.tokens[this.i++];
649
+ if (!t) throw new FormulaError("#ERROR!");
650
+ if (t.t === "num" || t.t === "str") return t.v;
651
+ if (t.t === "op") {
652
+ if (t.v !== "(") throw new FormulaError("#ERROR!");
653
+ const v = this.comparison();
654
+ if (!this.take(")")) throw new FormulaError("#ERROR!");
655
+ return v;
656
+ }
657
+ const name = t.v;
658
+ if (this.peek("(")) {
659
+ this.i++;
660
+ const args = [];
661
+ if (!this.take(")")) {
662
+ do
663
+ args.push(this.comparison());
664
+ while (this.take(","));
665
+ if (!this.take(")")) throw new FormulaError("#ERROR!");
666
+ }
667
+ const upper = name.toUpperCase();
668
+ if (upper === "IF") {
669
+ if (args.length < 2) throw new FormulaError("#ERROR!");
670
+ const cond = typeof args[0] === "boolean" ? args[0] : num(args[0]) !== 0;
671
+ return cond ? args[1] : args.length > 2 ? args[2] : false;
672
+ }
673
+ const fn = Object.hasOwn(FUNCS, upper) ? FUNCS[upper] : void 0;
674
+ if (!fn) throw new FormulaError("#NAME?");
675
+ return fn(args);
676
+ }
677
+ if (/^(true|false)$/i.test(name)) return name.toLowerCase() === "true";
678
+ return this.resolve(name);
679
+ }
680
+ };
681
+ var MAX_DEPTH = 32;
682
+ function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0) {
683
+ try {
684
+ const resolve = (name) => {
685
+ if (!row || !Object.hasOwn(row, name)) throw new FormulaError("#REF!");
686
+ const v2 = row[name];
687
+ if (typeof v2 === "string" && v2.startsWith("=")) {
688
+ if (seen2.has(name) || depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
689
+ const r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1);
690
+ if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
691
+ return r;
692
+ }
693
+ return v2;
694
+ };
695
+ const v = new Parser(tokenize(formula.slice(1)), resolve).parse();
696
+ if (typeof v === "number" && !Number.isFinite(v)) return "#NUM!";
697
+ return v;
698
+ } catch (e) {
699
+ return e instanceof FormulaError ? e.code : "#ERROR!";
700
+ }
701
+ }
702
+ var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
703
+
485
704
  // src/rows/values.ts
486
705
  function getCellValue(def, row) {
487
706
  if (def.valueGetter) {
@@ -492,7 +711,9 @@ function getCellValue(def, row) {
492
711
  return void 0;
493
712
  }
494
713
  }
495
- return def.field != null ? row[def.field] : void 0;
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;
496
717
  }
497
718
 
498
719
  // src/summary.ts
@@ -553,6 +774,57 @@ function computeSummary(defs, rows, columnDef) {
553
774
  });
554
775
  }
555
776
 
777
+ // src/merge.ts
778
+ function spanOf(fn, def, row, rowIndex, max) {
779
+ if (!fn) return 1;
780
+ let n = 1;
781
+ try {
782
+ n = Math.floor(Number(fn({ value: getCellValue(def, row), row, rowIndex })));
783
+ } catch {
784
+ n = 1;
785
+ }
786
+ return Number.isFinite(n) ? Math.max(1, Math.min(max, n)) : 1;
787
+ }
788
+ function computeSpans(rows, def) {
789
+ const n = rows.length;
790
+ const start = new Int32Array(n);
791
+ const end = new Int32Array(n);
792
+ if (def.rowSpan) {
793
+ for (let i = 0; i < n; ) {
794
+ const len = spanOf(def.rowSpan, def, rows[i], i, n - i);
795
+ for (let k = i; k < i + len; k++) {
796
+ start[k] = i;
797
+ end[k] = i + len - 1;
798
+ }
799
+ i += len;
800
+ }
801
+ return { start, end };
802
+ }
803
+ const same2 = typeof def.merge === "function" ? (a, b) => {
804
+ try {
805
+ return !!def.merge(getCellValue(def, rows[a]), getCellValue(def, rows[b]), rows[a], rows[b]);
806
+ } catch {
807
+ return false;
808
+ }
809
+ } : (() => {
810
+ const keys = rows.map((r) => {
811
+ const v = getCellValue(def, r);
812
+ return isBlank(v) ? null : setKey(v);
813
+ });
814
+ return (a, b) => keys[a] !== null && keys[a] === keys[b];
815
+ })();
816
+ let s = 0;
817
+ for (let i = 1; i <= n; i++) {
818
+ if (i < n && same2(i - 1, i)) continue;
819
+ for (let k = s; k < i; k++) {
820
+ start[k] = s;
821
+ end[k] = i - 1;
822
+ }
823
+ s = i;
824
+ }
825
+ return { start, end };
826
+ }
827
+
556
828
  // src/viewport.ts
557
829
  function rowRange(scrollTop, height, rowHeight, count, overscan = 4) {
558
830
  if (count <= 0 || rowHeight <= 0) return { first: 0, last: -1 };
@@ -601,6 +873,7 @@ function rowRangeByOffsets(offsets, scrollTop, height, count, overscan = 4) {
601
873
  // src/render/renderer.ts
602
874
  var REGIONS2 = ["left", "center", "right"];
603
875
  var ROW_OVERSCAN = 6;
876
+ var MAX_COLSPAN_REACH = 8;
604
877
  var COL_OVERSCAN = 2;
605
878
  var MENU_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M3 4h10M3 8h10M3 12h10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
606
879
  var CHEVRON = '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M6 3.5 10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
@@ -615,6 +888,8 @@ var Renderer = class {
615
888
  constructor(host, grid, opts) {
616
889
  this.grid = grid;
617
890
  this.opts = opts;
891
+ this.activeCellId = "";
892
+ this.idBase = `fg${Math.random().toString(36).slice(2, 8)}`;
618
893
  this.painted = { left: /* @__PURE__ */ new Map(), center: /* @__PURE__ */ new Map(), right: /* @__PURE__ */ new Map() };
619
894
  this.pool = { left: [], center: [], right: [] };
620
895
  this.frame = 0;
@@ -634,6 +909,9 @@ var Renderer = class {
634
909
  this.statusEl = null;
635
910
  this.statusKey = "";
636
911
  this.pagerEl = null;
912
+ /** Card view (narrow screens): scroller and canvas of absolutely placed cards. */
913
+ this.cards = null;
914
+ this.cardMode = false;
637
915
  this.pagerKey = "";
638
916
  /** Prefix-sum row tops when rows differ in height (detail rows); null when every row is rowHeight. */
639
917
  this.offsets = null;
@@ -641,19 +919,42 @@ var Renderer = class {
641
919
  this.details = /* @__PURE__ */ new Map();
642
920
  /** First non-system displayed column: holds group labels, tree chevrons and detail toggles. */
643
921
  this.firstDataId = "";
922
+ /** Measured heights per row id (autoRowHeight); cleared when content or widths change. */
923
+ this.rowHeights = /* @__PURE__ */ new Map();
924
+ this.heightsVersion = 0;
644
925
  /** Column holding group labels: the first scrolling data column, so long labels have room. */
645
926
  this.groupLabelId = "";
646
927
  this.onScroll = () => {
647
928
  if (this.frame) cancelAnimationFrame(this.frame);
648
929
  this.frame = 0;
649
930
  this.render();
931
+ this.grid.events.emit("scroll", this.getScroll());
650
932
  };
651
933
  this.onSideWheel = (e) => {
652
934
  if (!e.deltaY) return;
653
935
  e.preventDefault();
654
936
  this.scroller.scrollTop += e.deltaY;
655
937
  };
656
- this.root = el("div", "", { role: "grid", tabindex: "0" });
938
+ this.onCardClick = (e) => {
939
+ const target = e.target;
940
+ const card = target.closest(".fg-card");
941
+ if (!card) return;
942
+ const i = Number(card.dataset.row);
943
+ const node = this.grid.rowModel.getRow(i);
944
+ if (!node || node.kind) return;
945
+ if (target.closest(".fg-card-check")) {
946
+ this.grid.selectFromClick(i, true, false);
947
+ this.invalidate({ rows: true });
948
+ return;
949
+ }
950
+ const colId = target.closest(".fg-card-value")?.dataset.col;
951
+ const col = colId ? this.grid.columns.getColumn(colId) : void 0;
952
+ if (!col) return;
953
+ this.grid.events.emit("cellClick", { row: node.data, rowIndex: i, colId: col.id, value: getCellValue(col.def, node.data), event: e });
954
+ };
955
+ this.root = el("div", "", { tabindex: "-1" });
956
+ this.root.addEventListener("focus", () => this.gridEl.focus({ preventScroll: true }));
957
+ this.gridEl = el("div", "fg-grid", { role: "grid", tabindex: "0" });
657
958
  this.headerEl = el("div", "fg-header", { role: "rowgroup" });
658
959
  this.bodyEl = el("div", "fg-body", { role: "rowgroup" });
659
960
  const panes = (cls) => ({
@@ -663,6 +964,7 @@ var Renderer = class {
663
964
  });
664
965
  this.headerPanes = panes("fg-header-pane");
665
966
  this.headerInners = panes("fg-header-inner");
967
+ for (const r of REGIONS2) this.headerInners[r].setAttribute("role", "row");
666
968
  this.bodyPanes = panes("fg-body-pane");
667
969
  this.canvases = panes("fg-canvas");
668
970
  for (const r of REGIONS2) {
@@ -674,9 +976,10 @@ var Renderer = class {
674
976
  this.bodyEl.append(this.bodyPanes[r]);
675
977
  }
676
978
  this.scroller = this.bodyPanes.center;
677
- this.overlay = el("div", "fg-overlay", { role: "status" });
979
+ this.overlay = el("div", "fg-overlay", { "aria-live": "polite" });
678
980
  this.main = el("div", "fg-main");
679
- this.main.append(this.headerEl, this.bodyEl, this.overlay);
981
+ this.gridEl.append(this.headerEl, this.bodyEl);
982
+ this.main.append(this.gridEl, this.overlay);
680
983
  this.root.append(this.main);
681
984
  host.append(this.root);
682
985
  this.applyTheme();
@@ -684,6 +987,19 @@ var Renderer = class {
684
987
  this.bodyPanes.left.addEventListener("wheel", this.onSideWheel, { passive: false });
685
988
  this.bodyPanes.right.addEventListener("wheel", this.onSideWheel, { passive: false });
686
989
  }
990
+ /** The element that scrolls the rows: the card list when cards are shown. */
991
+ get activeScroller() {
992
+ return this.cards?.scroller ?? this.scroller;
993
+ }
994
+ getScroll() {
995
+ const s = this.activeScroller;
996
+ return { top: s.scrollTop, left: s.scrollLeft };
997
+ }
998
+ setScroll(p) {
999
+ const s = this.activeScroller;
1000
+ if (typeof p?.top === "number" && Number.isFinite(p.top)) s.scrollTop = Math.max(0, p.top);
1001
+ if (typeof p?.left === "number" && Number.isFinite(p.left)) s.scrollLeft = Math.max(0, p.left);
1002
+ }
687
1003
  applyFont(size, family) {
688
1004
  if (size) this.root.style.setProperty("--fg-font-size", `${size}px`);
689
1005
  else this.root.style.removeProperty("--fg-font-size");
@@ -691,7 +1007,13 @@ var Renderer = class {
691
1007
  else this.root.style.removeProperty("--fg-font");
692
1008
  }
693
1009
  applyTheme() {
694
- this.root.className = `fg-root fg-theme-${this.grid.store.state.theme}`;
1010
+ 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" : ""}`;
1012
+ this.root.setAttribute("dir", rtl ? "rtl" : "ltr");
1013
+ }
1014
+ /** Right-to-left layout: the main area is mirrored, so screen deltas along x flip sign. */
1015
+ get rtl() {
1016
+ return this.grid.options.direction === "rtl";
695
1017
  }
696
1018
  invalidate(what = {}) {
697
1019
  if (what.header) this.headerDirty = true;
@@ -714,10 +1036,20 @@ var Renderer = class {
714
1036
  this.version++;
715
1037
  }
716
1038
  const count = rowModel.getRowCount();
717
- const lay = rowModel.layout(rh, this.detailHeight());
1039
+ const auto = !!this.grid.options.autoRowHeight && !rowModel.server;
1040
+ const lay = rowModel.layout(rh, this.detailHeight(), auto ? this.rowHeights : void 0, this.heightsVersion);
718
1041
  this.offsets = lay.uniform ? null : lay.offsets;
719
1042
  this.firstDataId = columns.displayed().find((c) => !isSystemCol(c.id))?.id ?? "";
720
1043
  this.groupLabelId = columns.region("center").find((c) => !isSystemCol(c.id))?.id ?? this.firstDataId;
1044
+ if (this.useCards()) {
1045
+ this.renderCards(count);
1046
+ this.renderStatusBar(rowModel.dataCount());
1047
+ this.renderPager();
1048
+ 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;
1051
+ return;
1052
+ }
721
1053
  const summary = this.grid.summary();
722
1054
  const sumRows = summary ? summary.opts.rows.length : 0;
723
1055
  const sumPos = summary?.opts.position ?? "bottom";
@@ -738,14 +1070,18 @@ var Renderer = class {
738
1070
  }
739
1071
  const hScrollbar = this.scroller.offsetHeight - this.scroller.clientHeight;
740
1072
  this.bodyPanes.left.style.marginBottom = this.bodyPanes.right.style.marginBottom = `${hScrollbar}px`;
741
- this.root.setAttribute("aria-rowcount", String(count + columns.depth));
742
- this.root.setAttribute("aria-colcount", String(columns.displayed().length));
743
- if (options.rowSelection === "multiple") this.root.setAttribute("aria-multiselectable", "true");
1073
+ const g = this.gridEl;
1074
+ g.setAttribute("role", options.treeData || this.grid.getGroupBy().length ? "treegrid" : "grid");
1075
+ g.setAttribute("aria-rowcount", String(count + columns.depth));
1076
+ g.setAttribute("aria-colcount", String(columns.displayed().length));
1077
+ if (options.rowSelection === "multiple") g.setAttribute("aria-multiselectable", "true");
744
1078
  if (this.headerDirty) {
745
1079
  this.headerDirty = false;
746
1080
  this.renderHeader();
747
1081
  }
748
1082
  this.renderRows(count);
1083
+ this.updateActiveCell();
1084
+ if (auto) this.measureRows();
749
1085
  this.renderSummary(count, summary && summary.opts.rows, summary?.values ?? [], sumPinned, sumPos);
750
1086
  this.pinnedTop = this.renderPinned(this.pinnedTop, "top", this.grid.pinnedRows.top);
751
1087
  this.pinnedBottom = this.renderPinned(this.pinnedBottom, "bottom", this.grid.pinnedRows.bottom);
@@ -765,11 +1101,12 @@ var Renderer = class {
765
1101
  /** Place optional sections around the body without ever moving the body (which would reset its scroll). */
766
1102
  orderSections(summaryPos) {
767
1103
  const tops = [summaryPos === "top" ? this.summaryEl : null, this.pinnedTop?.el ?? null].filter((x) => !!x);
768
- const bottoms = [this.pinnedBottom?.el ?? null, summaryPos === "bottom" ? this.summaryEl : null, this.statusEl, this.pagerEl].filter((x) => !!x);
769
- for (const t of tops) if (t.nextElementSibling !== this.bodyEl && !tops.includes(t.nextElementSibling)) this.main.insertBefore(t, this.bodyEl);
1104
+ const bottoms = [this.pinnedBottom?.el ?? null, summaryPos === "bottom" ? this.summaryEl : null].filter((x) => !!x);
1105
+ const after = [this.statusEl, this.pagerEl].filter((x) => !!x);
1106
+ for (const t of tops) if (t.nextElementSibling !== this.bodyEl && !tops.includes(t.nextElementSibling)) this.gridEl.insertBefore(t, this.bodyEl);
770
1107
  let ref = this.bodyEl;
771
1108
  for (let i = tops.length - 1; i >= 0; i--) {
772
- if (ref.previousElementSibling !== tops[i]) this.main.insertBefore(tops[i], ref);
1109
+ if (ref.previousElementSibling !== tops[i]) this.gridEl.insertBefore(tops[i], ref);
773
1110
  ref = tops[i];
774
1111
  }
775
1112
  ref = this.bodyEl;
@@ -777,6 +1114,25 @@ var Renderer = class {
777
1114
  if (ref.nextElementSibling !== b) ref.after(b);
778
1115
  ref = b;
779
1116
  }
1117
+ ref = this.gridEl;
1118
+ for (const b of after) {
1119
+ if (ref.nextElementSibling !== b) ref.after(b);
1120
+ ref = b;
1121
+ }
1122
+ }
1123
+ /** Point aria-activedescendant at the focused cell so screen readers announce it. */
1124
+ updateActiveCell() {
1125
+ const prev = this.activeCellId ? this.root.getRootNode().getElementById?.(this.activeCellId) ?? null : null;
1126
+ const cell = this.gridEl.querySelector(".fg-body .fg-cell-focused, .fg-body .fg-row-group .fg-cell-focused");
1127
+ if (prev && prev !== cell) prev.removeAttribute("id");
1128
+ if (!cell) {
1129
+ this.gridEl.removeAttribute("aria-activedescendant");
1130
+ this.activeCellId = "";
1131
+ return;
1132
+ }
1133
+ if (!cell.id) cell.id = `${this.idBase}-cell`;
1134
+ this.activeCellId = cell.id;
1135
+ this.gridEl.setAttribute("aria-activedescendant", cell.id);
780
1136
  }
781
1137
  renderPinned(section, position, rows) {
782
1138
  if (!rows.length) {
@@ -862,7 +1218,26 @@ var Renderer = class {
862
1218
  return cell;
863
1219
  }
864
1220
  const label = el("span", "fg-header-label");
1221
+ const hdef = c.node.def;
865
1222
  label.textContent = c.label;
1223
+ if (hdef.headerRenderer) {
1224
+ try {
1225
+ const out = hdef.headerRenderer({ header: c.label, column: hdef });
1226
+ if (typeof out === "string") label.innerHTML = out;
1227
+ else label.replaceChildren(out);
1228
+ } catch {
1229
+ warn(`headerRenderer of column "${c.id}" threw; showing the plain header`);
1230
+ label.textContent = c.label;
1231
+ }
1232
+ }
1233
+ if (hdef.headerClass) {
1234
+ try {
1235
+ const extra = typeof hdef.headerClass === "function" ? hdef.headerClass(hdef) : hdef.headerClass;
1236
+ if (extra) cell.classList.add(...extra.split(/\s+/).filter(Boolean));
1237
+ } catch {
1238
+ warn(`headerClass of column "${c.id}" threw`);
1239
+ }
1240
+ }
866
1241
  cell.append(label);
867
1242
  if (c.isGroup || !c.column) return cell;
868
1243
  const def = c.node.def;
@@ -892,26 +1267,27 @@ var Renderer = class {
892
1267
  return cell;
893
1268
  }
894
1269
  selectAllBox(locale) {
895
- const rows = this.grid.rowModel.dataRows();
1270
+ const rows = this.grid.selectableRows();
896
1271
  const sel = this.grid.store.state.selected;
897
1272
  let n = 0;
898
1273
  for (const r of rows) if (sel.has(r.id)) n++;
899
1274
  const box = el("input", "fg-checkbox fg-select-all", { type: "checkbox", tabindex: "-1", "aria-label": locale.selectAllRows });
900
1275
  box.checked = rows.length > 0 && n === rows.length;
901
1276
  box.indeterminate = n > 0 && n < rows.length;
902
- box.hidden = this.grid.options.rowSelection !== "multiple";
1277
+ box.hidden = this.grid.options.rowSelection !== "multiple" || this.grid.options.headerCheckbox === false;
903
1278
  return box;
904
1279
  }
905
1280
  renderRows(count) {
906
1281
  const { columns, rowModel, store } = this.grid;
907
1282
  const { selected, focus } = store.state;
908
- const bounds = this.grid.rangeBounds();
1283
+ const allBounds = this.grid.allRangeBounds();
909
1284
  const spans = this.grid.mergeSpans();
910
1285
  const top = Math.max(0, this.scroller.scrollTop - this.topOffset);
911
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);
912
1287
  this.lastRange = range;
913
1288
  const center = columns.region("center");
914
1289
  const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
1290
+ if (center.some((c) => c.def.colSpan)) cr.first = Math.max(0, cr.first - MAX_COLSPAN_REACH);
915
1291
  for (const r of REGIONS2) {
916
1292
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
917
1293
  const map = this.painted[r];
@@ -927,7 +1303,8 @@ var Renderer = class {
927
1303
  const node = rowModel.getRow(i);
928
1304
  const isSel = selected.has(node.id);
929
1305
  const focusCol = focus && focus.rowIndex === i ? focus.colId : "";
930
- const inRange = bounds && i >= bounds.r1 && i <= bounds.r2 ? `${bounds.c1}-${bounds.c2}` : "";
1306
+ const hits = allBounds.filter((b) => i >= b.r1 && i <= b.r2);
1307
+ const inRange = hits.map((b) => `${b.c1}-${b.c2}`).join(",");
931
1308
  const merge2 = this.mergeState(spans, cols, i, range.first);
932
1309
  const pos = node.kind === "detail" ? `${this.scroller.scrollLeft}:${this.scroller.clientWidth}` : "";
933
1310
  const key = `${node.id}|${this.version}|${colKey}|${isSel ? 1 : 0}|${focusCol}|${inRange}|${merge2}|${pos}`;
@@ -947,7 +1324,7 @@ var Renderer = class {
947
1324
  if (node.kind === "group") this.paintGroupRow(row.el, node, i, cols, focusCol);
948
1325
  else if (node.kind === "detail") this.paintDetailRow(row.el, node, i, r);
949
1326
  else if (node.kind === "loading") this.paintLoadingRow(row.el, i, cols);
950
- else this.paintRow(row.el, node, i, cols, isSel, focusCol, inRange ? bounds : null, spans, range.first);
1327
+ else this.paintRow(row.el, node, i, cols, isSel, focusCol, hits, spans, range.first);
951
1328
  }
952
1329
  }
953
1330
  }
@@ -975,22 +1352,40 @@ var Renderer = class {
975
1352
  while (rowEl.childElementCount > cols.length) rowEl.lastElementChild.remove();
976
1353
  while (rowEl.childElementCount < cols.length) rowEl.append(el("div", "fg-cell", { role: "gridcell" }));
977
1354
  let drawsMerge = false;
1355
+ const area = cols.length ? this.grid.columns.region(cols[0].region) : [];
1356
+ const spanning = area.some((c) => c.def.colSpan);
1357
+ let coveredUntil = -1;
1358
+ const posOf = spanning ? new Map(area.map((c, p) => [c.id, p])) : null;
978
1359
  cols.forEach((col, k) => {
979
1360
  const cellEl = rowEl.children[k];
980
1361
  this.paintCell(cellEl, node, i, col, selected, focusCol === col.id);
1362
+ if (posOf) {
1363
+ const p = posOf.get(col.id);
1364
+ if (p <= coveredUntil) cellEl.classList.add("fg-cell-span-hidden");
1365
+ else if (col.def.colSpan && !isSystemCol(col.id)) {
1366
+ const n = spanOf(col.def.colSpan, col.def, node.data, i, area.length - p);
1367
+ if (n > 1) {
1368
+ coveredUntil = p + n - 1;
1369
+ let w = 0;
1370
+ for (let q = p; q <= coveredUntil; q++) w += area[q].width;
1371
+ cellEl.style.width = `${w}px`;
1372
+ cellEl.classList.add("fg-cell-colspan");
1373
+ }
1374
+ }
1375
+ }
981
1376
  if (col.id === this.firstDataId) this.decorate(cellEl, node);
982
1377
  cellEl.style.top = cellEl.style.height = cellEl.style.bottom = "";
983
1378
  const s = spans.get(col.id);
984
1379
  if (s && s.start[i] !== s.end[i]) {
985
1380
  if (i === Math.max(s.start[i], first)) {
986
1381
  cellEl.classList.add("fg-cell-merged");
987
- cellEl.style.top = `${-(i - s.start[i]) * rh}px`;
988
- cellEl.style.height = `${(s.end[i] - s.start[i] + 1) * rh}px`;
1382
+ cellEl.style.top = `${-(this.rowTop(i) - this.rowTop(s.start[i]))}px`;
1383
+ cellEl.style.height = `${this.rowTop(s.end[i]) + this.rowH(s.end[i]) - this.rowTop(s.start[i])}px`;
989
1384
  cellEl.style.bottom = "auto";
990
1385
  drawsMerge = true;
991
1386
  } else cellEl.classList.add("fg-cell-merge-hidden");
992
1387
  }
993
- if (bounds && col.index >= bounds.c1 && col.index <= bounds.c2 && !isSystemCol(col.id)) cellEl.classList.add("fg-cell-range");
1388
+ if (!isSystemCol(col.id) && bounds.some((b) => col.index >= b.c1 && col.index <= b.c2)) cellEl.classList.add("fg-cell-range");
994
1389
  });
995
1390
  rowEl.style.zIndex = drawsMerge ? "1" : "";
996
1391
  }
@@ -1012,6 +1407,10 @@ var Renderer = class {
1012
1407
  cell.className = `${cls} fg-cell-select`;
1013
1408
  const box = el("input", "fg-checkbox", { type: "checkbox", tabindex: "-1", "aria-label": locale.selectRow });
1014
1409
  box.checked = selected;
1410
+ if (i >= 0 && !node.kind && !this.grid.rowSelectable(node.data)) {
1411
+ box.disabled = true;
1412
+ cell.classList.add("fg-cell-select-locked");
1413
+ }
1015
1414
  cell.replaceChildren(box);
1016
1415
  return;
1017
1416
  }
@@ -1029,6 +1428,8 @@ var Renderer = class {
1029
1428
  }
1030
1429
  }
1031
1430
  if (this.grid.tracker.isDirty(node.data, def.field)) cls += " fg-cell-dirty";
1431
+ if (def.wrapText) cls += " fg-cell-wrap";
1432
+ if (typeof def.editable === "function" && i >= 0 && !cellEditable(def, node.data, i)) cls += " fg-cell-readonly";
1032
1433
  if (i >= 0) cls += this.grid.finder.classFor(i, col.id);
1033
1434
  cell.className = cls;
1034
1435
  if (!def.renderer) {
@@ -1145,17 +1546,55 @@ var Renderer = class {
1145
1546
  const value = values[col.id];
1146
1547
  const s = cellSpec(spec);
1147
1548
  const code = this.grid.store.state.locale.code;
1148
- let text;
1149
- if (value == null || value === "") text = "";
1150
- else if (typeof value === "string") text = value;
1151
- else if (s.agg === "count") text = new Intl.NumberFormat(code).format(value);
1549
+ let text2;
1550
+ if (value == null || value === "") text2 = "";
1551
+ else if (typeof value === "string") text2 = value;
1552
+ else if (s.agg === "count") text2 = new Intl.NumberFormat(code).format(value);
1152
1553
  else {
1153
1554
  const type = cd.type ?? (typeof value === "number" ? "number" : value instanceof Date ? "date" : void 0);
1154
- text = getFormatter({ ...cd, type, format: s.format ?? cd.format }, code)(value);
1555
+ text2 = getFormatter({ ...cd, type, format: s.format ?? cd.format }, code)(value);
1155
1556
  }
1156
- cell.textContent = s.prefix && text ? `${s.prefix} ${text}` : text;
1557
+ cell.textContent = s.prefix && text2 ? `${s.prefix} ${text2}` : text2;
1157
1558
  return cell;
1158
1559
  }
1560
+ /** Forget measured row heights (data, widths or fonts changed); rows are measured again on screen. */
1561
+ invalidateHeights() {
1562
+ if (!this.rowHeights.size) return;
1563
+ this.rowHeights.clear();
1564
+ this.heightsVersion++;
1565
+ }
1566
+ /** Measure wrapped cells of the painted rows; when a height changes, lay out again next frame. */
1567
+ measureRows() {
1568
+ const base = this.opts.rowHeight;
1569
+ const need = /* @__PURE__ */ new Map();
1570
+ for (const r of REGIONS2) {
1571
+ for (const [i, row] of this.painted[r]) {
1572
+ const node = this.grid.rowModel.getRow(i);
1573
+ if (!node || node.kind) continue;
1574
+ let h = need.get(i) ?? base;
1575
+ for (const cell of row.el.querySelectorAll(".fg-cell-wrap")) {
1576
+ if (cell.classList.contains("fg-cell-merged") || cell.classList.contains("fg-cell-merge-hidden")) continue;
1577
+ h = Math.max(h, Math.ceil(cell.scrollHeight));
1578
+ }
1579
+ need.set(i, h);
1580
+ }
1581
+ }
1582
+ let changed = false;
1583
+ for (const [i, h] of need) {
1584
+ const id = this.grid.rowModel.getRow(i).id;
1585
+ const cur = this.rowHeights.get(id) ?? base;
1586
+ if (Math.abs(cur - h) > 1) {
1587
+ if (h === base) this.rowHeights.delete(id);
1588
+ else this.rowHeights.set(id, h);
1589
+ changed = true;
1590
+ }
1591
+ }
1592
+ if (changed) {
1593
+ this.heightsVersion++;
1594
+ this.version++;
1595
+ this.schedule();
1596
+ }
1597
+ }
1159
1598
  detailHeight() {
1160
1599
  const h = this.grid.options.detailHeight;
1161
1600
  return typeof h === "number" && h > 0 ? h : 240;
@@ -1189,12 +1628,13 @@ var Renderer = class {
1189
1628
  const lead = [];
1190
1629
  if (tree) {
1191
1630
  if (node.hasChildren) {
1192
- const b = el("button", node.expanded ? "fg-tree-toggle fg-expanded" : "fg-tree-toggle", {
1631
+ const b = el("button", node.expanded ? "fg-tree-toggle fg-expanded" : node.loading ? "fg-tree-toggle fg-tree-loading" : "fg-tree-toggle", {
1193
1632
  type: "button",
1194
1633
  tabindex: "-1",
1195
1634
  "aria-label": node.expanded ? loc.collapseAll : loc.expandAll,
1196
1635
  "aria-expanded": String(!!node.expanded)
1197
1636
  });
1637
+ if (node.loading) b.setAttribute("aria-busy", "true");
1198
1638
  b.innerHTML = CHEVRON;
1199
1639
  lead.push(b);
1200
1640
  } else lead.push(el("span", "fg-tree-spacer", { "aria-hidden": "true" }));
@@ -1356,10 +1796,10 @@ var Renderer = class {
1356
1796
  label.append(sel);
1357
1797
  parts.push(label);
1358
1798
  }
1359
- const btn = (action, text, label, disabled) => {
1799
+ const btn = (action, text2, label, disabled) => {
1360
1800
  const b = el("button", "fg-pager-btn", { type: "button", "aria-label": label, title: label });
1361
1801
  b.dataset.action = action;
1362
- b.textContent = text;
1802
+ b.textContent = text2;
1363
1803
  b.disabled = disabled;
1364
1804
  return b;
1365
1805
  };
@@ -1379,11 +1819,98 @@ var Renderer = class {
1379
1819
  if (this.root.querySelector(".fg-free-badge")) return;
1380
1820
  const a = el("a", "fg-free-badge", { href: "https://freegrid.app/pricing/", target: "_blank", rel: "noopener" });
1381
1821
  a.innerHTML = '<svg viewBox="0 0 32 32" width="14" height="14" aria-hidden="true"><rect width="32" height="32" rx="7" fill="#0E1726"/><path d="M7 11.5h18M7 16.5h18M7 21.5h18M13 7v18" stroke="#fff" stroke-width="2" stroke-linecap="round"/><rect x="14" y="12.5" width="11" height="3" rx="1" fill="#F2A900"/></svg>';
1382
- const text = el("span");
1383
- text.textContent = `FreeGrid \xB7 ${this.grid.store.state.locale.freeBadge}`;
1384
- a.append(text);
1822
+ const text2 = el("span");
1823
+ text2.textContent = `FreeGrid \xB7 ${this.grid.store.state.locale.freeBadge}`;
1824
+ a.append(text2);
1385
1825
  this.root.append(a);
1386
1826
  }
1827
+ /** Card view applies when enabled and the grid is narrower than the breakpoint. */
1828
+ useCards() {
1829
+ const opt = this.grid.options.cardView;
1830
+ const bp = opt === true ? 640 : typeof opt === "number" ? opt : -1;
1831
+ const on = bp > 0 && this.main.clientWidth < bp;
1832
+ if (on !== this.cardMode) {
1833
+ this.cardMode = on;
1834
+ this.root.classList.toggle("fg-card-mode", on);
1835
+ if (!on && this.cards) {
1836
+ this.cards.scroller.remove();
1837
+ this.cards = null;
1838
+ }
1839
+ this.version++;
1840
+ }
1841
+ return on;
1842
+ }
1843
+ /** Rows as cards: a check box (with row selection) and one "header: value" line per data column. */
1844
+ renderCards(count) {
1845
+ const { grid } = this;
1846
+ if (!this.cards) {
1847
+ const scroller2 = el("div", "fg-cards", { role: "list" });
1848
+ const canvas2 = el("div", "fg-cards-canvas");
1849
+ scroller2.append(canvas2);
1850
+ this.main.insertBefore(scroller2, this.overlay);
1851
+ scroller2.addEventListener("scroll", () => {
1852
+ this.schedule();
1853
+ this.grid.events.emit("scroll", this.getScroll());
1854
+ }, { passive: true });
1855
+ scroller2.addEventListener("click", this.onCardClick);
1856
+ this.cards = { scroller: scroller2, canvas: canvas2 };
1857
+ }
1858
+ const { scroller, canvas } = this.cards;
1859
+ const cols = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
1860
+ const line = Math.round(this.opts.rowHeight * 0.75);
1861
+ const select = !!grid.options.rowSelection;
1862
+ const cardH = 20 + cols.length * line + (select ? line : 0);
1863
+ const gap = 10;
1864
+ canvas.style.height = `${count * (cardH + gap)}px`;
1865
+ const first = Math.max(0, Math.floor(scroller.scrollTop / (cardH + gap)) - 2);
1866
+ const last = Math.min(count - 1, Math.ceil((scroller.scrollTop + (scroller.clientHeight || 800)) / (cardH + gap)) + 2);
1867
+ const { selected, locale } = grid.store.state;
1868
+ const out = [];
1869
+ for (let i = first; i <= last; i++) {
1870
+ const node = grid.rowModel.getRow(i);
1871
+ if (!node) continue;
1872
+ const card = el("div", node.kind ? "fg-card fg-card-other" : selected.has(node.id) ? "fg-card fg-card-selected" : "fg-card", { role: "listitem" });
1873
+ card.dataset.row = String(i);
1874
+ card.style.transform = `translateY(${i * (cardH + gap)}px)`;
1875
+ card.style.height = `${cardH}px`;
1876
+ card.style.setProperty("--fg-card-line", `${line}px`);
1877
+ if (node.kind === "group" && node.group) {
1878
+ const def = grid.columns.getNode(node.group.colId)?.def;
1879
+ card.textContent = `${def?.header ?? node.group.colId}: ${String(node.group.value ?? locale.blanks)} (${node.group.rows.length})`;
1880
+ } else if (!node.kind) {
1881
+ if (select) {
1882
+ const head = el("label", "fg-card-head");
1883
+ const box = el("input", "fg-card-check fg-checkbox", { type: "checkbox", "aria-label": locale.selectRow });
1884
+ box.checked = selected.has(node.id);
1885
+ box.disabled = !grid.rowSelectable(node.data);
1886
+ head.append(box, document.createTextNode(`#${i + 1}`));
1887
+ card.append(head);
1888
+ }
1889
+ for (const col of cols) {
1890
+ const lineEl = el("div", "fg-card-line");
1891
+ const label = el("span", "fg-card-label");
1892
+ label.textContent = col.def.header ?? col.def.field ?? col.id;
1893
+ const value = el("span", "fg-card-value");
1894
+ value.dataset.col = col.id;
1895
+ const v = getCellValue(col.def, node.data);
1896
+ const formatted = getFormatter(col.def, locale.code)(v);
1897
+ if (col.def.renderer) {
1898
+ try {
1899
+ const r = col.def.renderer({ value: v, formatted, row: node.data, rowIndex: i, column: col.def });
1900
+ if (typeof r === "string") value.innerHTML = r;
1901
+ else value.replaceChildren(r);
1902
+ } catch {
1903
+ value.textContent = formatted;
1904
+ }
1905
+ } else value.textContent = formatted;
1906
+ lineEl.append(label, value);
1907
+ card.append(lineEl);
1908
+ }
1909
+ }
1910
+ out.push(card);
1911
+ }
1912
+ canvas.replaceChildren(...out);
1913
+ }
1387
1914
  /** Forget cached detail panels (one master, or all). */
1388
1915
  dropDetail(id) {
1389
1916
  if (id === void 0) this.details.clear();
@@ -1393,14 +1920,14 @@ var Renderer = class {
1393
1920
  return this.lastRange;
1394
1921
  }
1395
1922
  /** Text width in px using the grid font; falls back to 0.6em per char without canvas. */
1396
- measureText(text, bold) {
1923
+ measureText(text2, bold) {
1397
1924
  const cs = getComputedStyle(this.main);
1398
1925
  const size = parseFloat(getComputedStyle(this.root).getPropertyValue("--fg-font-size")) || 13;
1399
1926
  if (this.measureCtx === void 0) this.measureCtx = document.createElement("canvas").getContext("2d");
1400
1927
  const ctx = this.measureCtx;
1401
- if (!ctx) return text.length * size * 0.6;
1928
+ if (!ctx) return text2.length * size * 0.6;
1402
1929
  ctx.font = `${bold ? 600 : 400} ${size}px ${cs.fontFamily || "sans-serif"}`;
1403
- return ctx.measureText(text).width;
1930
+ return ctx.measureText(text2).width;
1404
1931
  }
1405
1932
  scrollToRow(index) {
1406
1933
  this.scroller.scrollTop = this.rowTop(Math.max(0, Math.min(index, this.grid.rowModel.getRowCount())));
@@ -1486,7 +2013,7 @@ var GroupPanel = class {
1486
2013
  window.addEventListener("pointerup", up);
1487
2014
  };
1488
2015
  this.el = el("div", "fg-group-panel", { role: "toolbar" });
1489
- grid.renderer.main.insertBefore(this.el, grid.renderer.headerEl);
2016
+ grid.renderer.main.insertBefore(this.el, grid.renderer.gridEl);
1490
2017
  this.el.addEventListener("click", this.onClick);
1491
2018
  this.el.addEventListener("pointerdown", this.onPointerDown);
1492
2019
  this.refresh();
@@ -1597,6 +2124,7 @@ var en = {
1597
2124
  pasteHint: "Clipboard access was blocked. Press Ctrl+V to paste.",
1598
2125
  required: "This field is required",
1599
2126
  invalidValue: "Invalid value",
2127
+ duplicate: "This value is already used",
1600
2128
  undo: "Undo",
1601
2129
  redo: "Redo",
1602
2130
  deleteRows: "Delete selected rows",
@@ -1702,6 +2230,25 @@ function resolveLocale(input, navLangs = navigatorLanguages()) {
1702
2230
  return en_default;
1703
2231
  }
1704
2232
 
2233
+ // src/dom.ts
2234
+ function popupParent(root) {
2235
+ const r = root.getRootNode();
2236
+ return typeof ShadowRoot !== "undefined" && r instanceof ShadowRoot ? r : document.body;
2237
+ }
2238
+ function elementAt(root, x, y) {
2239
+ const r = root.getRootNode();
2240
+ return (typeof r.elementFromPoint === "function" ? r.elementFromPoint(x, y) : null) ?? document.elementFromPoint(x, y);
2241
+ }
2242
+ function activeElement(root) {
2243
+ const r = root.getRootNode();
2244
+ return r.activeElement ?? document.activeElement;
2245
+ }
2246
+ function eventInside(e, ...els) {
2247
+ const path = typeof e.composedPath === "function" ? e.composedPath() : [];
2248
+ if (path.length) return els.some((el2) => !!el2 && path.includes(el2));
2249
+ return els.some((el2) => !!el2 && el2.contains(e.target));
2250
+ }
2251
+
1705
2252
  // src/keyboard.ts
1706
2253
  var NAV_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab", "Home", "End", "PageUp", "PageDown"]);
1707
2254
  function nextFocus(key, mods, p, s) {
@@ -1734,6 +2281,10 @@ function nextFocus(key, mods, p, s) {
1734
2281
  }
1735
2282
  }
1736
2283
 
2284
+ // src/registry.ts
2285
+ var liveGrids = /* @__PURE__ */ new Map();
2286
+ var gridOfRoot = (root) => root ? liveGrids.get(root) : void 0;
2287
+
1737
2288
  // src/interactions/body.ts
1738
2289
  var DEFAULT_KEYMAP = {
1739
2290
  Enter: "edit",
@@ -1755,10 +2306,17 @@ function comboOf(e) {
1755
2306
  return parts.join("+");
1756
2307
  }
1757
2308
  var DRAG_THRESHOLD2 = 3;
2309
+ function mirrored(grid, e) {
2310
+ if (!grid.renderer.rtl) return e.key;
2311
+ return e.key === "ArrowLeft" ? "ArrowRight" : e.key === "ArrowRight" ? "ArrowLeft" : e.key;
2312
+ }
1758
2313
  function attachBody(grid) {
1759
2314
  const { renderer } = grid;
1760
2315
  const body = renderer.bodyEl;
1761
2316
  const root = renderer.root;
2317
+ const keyOf = (e) => mirrored(grid, e);
2318
+ let lastPointer = "mouse";
2319
+ let pressTimer = 0;
1762
2320
  let suppressClick = false;
1763
2321
  const toEvent = (e) => {
1764
2322
  const hit = renderer.cellAt(e.target);
@@ -1773,7 +2331,7 @@ function attachBody(grid) {
1773
2331
  const btn = e.target.closest?.(".fg-group-toggle, .fg-tree-toggle, .fg-detail-toggle");
1774
2332
  const hit = btn && renderer.cellAt(e.target);
1775
2333
  if (!hit) return false;
1776
- root.focus({ preventScroll: true });
2334
+ renderer.gridEl.focus({ preventScroll: true });
1777
2335
  grid.setFocus(hit.rowIndex, hit.colId);
1778
2336
  grid.setRange(null);
1779
2337
  const node = grid.rowModel.getRow(hit.rowIndex);
@@ -1783,23 +2341,28 @@ function attachBody(grid) {
1783
2341
  };
1784
2342
  const onClick = (e) => {
1785
2343
  if (onToggle(e)) return;
2344
+ const before = grid.store.state.focus;
1786
2345
  const hit = renderer.cellAt(e.target);
1787
2346
  if (hit && grid.rowModel.getRow(hit.rowIndex)?.kind) {
1788
- root.focus({ preventScroll: true });
2347
+ renderer.gridEl.focus({ preventScroll: true });
1789
2348
  grid.setFocus(hit.rowIndex, hit.colId);
1790
2349
  grid.setRange(null);
1791
2350
  return;
1792
2351
  }
1793
2352
  const ev = toEvent(e);
1794
2353
  if (!ev) return;
1795
- root.focus({ preventScroll: true });
2354
+ renderer.gridEl.focus({ preventScroll: true });
1796
2355
  const dragged = suppressClick;
1797
2356
  suppressClick = false;
1798
2357
  const prev = grid.store.state.focus;
1799
2358
  const onBox = ev.colId === SELECT_COL_ID;
1800
2359
  if (dragged) {
1801
2360
  } else if (e.shiftKey && prev && !onBox && !isSystemCol(prev.colId) && grid.options.rangeSelection !== false) {
1802
- grid.setRange({ start: prev, end: { rowIndex: ev.rowIndex, colId: ev.colId } });
2361
+ grid.setRange({ start: prev, end: { rowIndex: ev.rowIndex, colId: ev.colId } }, true);
2362
+ } else if ((e.ctrlKey || e.metaKey) && !onBox && !isSystemCol(ev.colId) && grid.options.rangeSelection !== false) {
2363
+ const at = { rowIndex: ev.rowIndex, colId: ev.colId };
2364
+ grid.addRange({ start: at, end: at });
2365
+ grid.setFocus(ev.rowIndex, ev.colId);
1803
2366
  } else {
1804
2367
  grid.setFocus(ev.rowIndex, ev.colId);
1805
2368
  grid.setRange(null);
@@ -1808,6 +2371,11 @@ function attachBody(grid) {
1808
2371
  grid.selectFromClick(ev.rowIndex, onBox || e.ctrlKey || e.metaKey, e.shiftKey);
1809
2372
  }
1810
2373
  grid.events.emit("cellClick", ev);
2374
+ if (lastPointer === "touch" && !dragged && before && before.rowIndex === ev.rowIndex && before.colId === ev.colId && grid.editor.canEdit(ev.rowIndex, ev.colId)) {
2375
+ const def = grid.columns.getColumn(ev.colId).def;
2376
+ if (def.type === "boolean") grid.applyEdit(ev.rowIndex, ev.colId, ev.value !== true);
2377
+ else grid.editor.start(ev.rowIndex, ev.colId);
2378
+ }
1811
2379
  };
1812
2380
  const onDblClick = (e) => {
1813
2381
  if (e.target.closest?.("button")) return;
@@ -1885,8 +2453,8 @@ function attachBody(grid) {
1885
2453
  const open = !!(node.group?.expanded ?? node.expanded);
1886
2454
  const key = grid.expandKey(node);
1887
2455
  if (group && (e.key === "Enter" || e.key === " ")) grid.setGroupExpanded(key, !open);
1888
- else if (e.key === "ArrowRight" && !open) grid.setGroupExpanded(key, true);
1889
- else if (e.key === "ArrowLeft" && open) grid.setGroupExpanded(key, false);
2456
+ else if (keyOf(e) === "ArrowRight" && !open) grid.setGroupExpanded(key, true);
2457
+ else if (keyOf(e) === "ArrowLeft" && open) grid.setGroupExpanded(key, false);
1890
2458
  else return false;
1891
2459
  return true;
1892
2460
  };
@@ -1898,16 +2466,42 @@ function attachBody(grid) {
1898
2466
  if (!node || node.kind || !grid.canMoveRows()) return true;
1899
2467
  const row = node.data;
1900
2468
  const marker = el("div", "fg-row-drop-marker");
1901
- let target = null;
2469
+ let drop = null;
1902
2470
  root.classList.add("fg-row-dragging");
1903
2471
  const move = (ev) => {
1904
- const under = document.elementFromPoint(ev.clientX, ev.clientY)?.closest(".fg-row[data-row]");
1905
- if (!under || under.closest(".fg-root") !== root) return;
1906
- const box = under.getBoundingClientRect();
1907
- const after = ev.clientY > box.top + box.height / 2;
1908
- target = { index: Number(under.dataset.row), after };
1909
- if (!marker.isConnected) root.append(marker);
1910
- marker.style.top = `${(after ? box.bottom : box.top) - root.getBoundingClientRect().top - 1}px`;
2472
+ const point = elementAt(root, ev.clientX, ev.clientY);
2473
+ const intoRoot = point?.closest(".fg-root");
2474
+ const into = intoRoot === root ? grid : gridOfRoot(intoRoot);
2475
+ const group = grid.dragOptions.group;
2476
+ drop = null;
2477
+ marker.remove();
2478
+ if (!into || !intoRoot || into !== grid && (!group || into.dragOptions.group !== group || !into.options.rowDrag)) return;
2479
+ const under = point.closest(".fg-row[data-row]");
2480
+ const box = under?.getBoundingClientRect();
2481
+ let target = null;
2482
+ let position = "after";
2483
+ let line;
2484
+ if (under && box) {
2485
+ const over = into.rowModel.getRow(Number(under.dataset.row));
2486
+ if (!over || over.kind === "group") return;
2487
+ target = over.detailOf?.data ?? over.data;
2488
+ const y = (ev.clientY - box.top) / box.height;
2489
+ if (over.kind === "detail") position = "after";
2490
+ else if (into.options.treeData) position = y < 0.25 ? "before" : y > 0.75 ? "after" : "inside";
2491
+ else position = y < 0.5 ? "before" : "after";
2492
+ line = position === "before" ? box.top : box.bottom;
2493
+ } else {
2494
+ if (!point.closest(".fg-body")) return;
2495
+ const rows = intoRoot.querySelectorAll(".fg-body-pane-center .fg-row[data-row]");
2496
+ line = rows.length ? Math.max(...[...rows].map((r) => r.getBoundingClientRect().bottom)) : intoRoot.querySelector(".fg-body").getBoundingClientRect().top;
2497
+ }
2498
+ if (!into.canDropRow(row, target, position, grid)) return;
2499
+ drop = { into, target, position };
2500
+ const top = intoRoot.getBoundingClientRect().top;
2501
+ marker.classList.toggle("fg-row-drop-inside", position === "inside");
2502
+ marker.style.top = `${position === "inside" ? box.top - top : line - top - 1}px`;
2503
+ marker.style.height = position === "inside" ? `${box.height}px` : "";
2504
+ intoRoot.append(marker);
1911
2505
  };
1912
2506
  const up = () => {
1913
2507
  window.removeEventListener("pointermove", move);
@@ -1915,39 +2509,66 @@ function attachBody(grid) {
1915
2509
  window.removeEventListener("pointercancel", up);
1916
2510
  root.classList.remove("fg-row-dragging");
1917
2511
  marker.remove();
1918
- if (!target) return;
1919
- const over = grid.rowModel.getRow(target.index);
1920
- const overRow = over?.detailOf?.data ?? over?.data;
1921
- const data = grid.store.state.data;
1922
- const from = data.indexOf(row);
1923
- let to = data.indexOf(overRow) + (target.after || over?.kind === "detail" ? 1 : 0);
1924
- if (from < 0 || to < 0) return;
1925
- if (to > from) to--;
1926
- grid.moveRow(row, to);
2512
+ if (drop) drop.into.dropRow(row, drop.target, drop.position, grid);
1927
2513
  };
1928
2514
  window.addEventListener("pointermove", move);
1929
2515
  window.addEventListener("pointerup", up);
1930
2516
  window.addEventListener("pointercancel", up);
1931
2517
  return true;
1932
2518
  };
2519
+ const onTouchDown = (e) => {
2520
+ const hit = renderer.cellAt(e.target);
2521
+ if (!hit) return;
2522
+ const x0 = e.clientX, y0 = e.clientY;
2523
+ const cancel = () => {
2524
+ clearTimeout(pressTimer);
2525
+ window.removeEventListener("pointermove", move);
2526
+ window.removeEventListener("pointerup", cancel);
2527
+ window.removeEventListener("pointercancel", cancel);
2528
+ };
2529
+ const move = (ev) => {
2530
+ if (Math.hypot(ev.clientX - x0, ev.clientY - y0) > 8) cancel();
2531
+ };
2532
+ clearTimeout(pressTimer);
2533
+ pressTimer = window.setTimeout(() => {
2534
+ cancel();
2535
+ if (grid.options.contextMenu === false) return;
2536
+ suppressClick = true;
2537
+ if (!isSystemCol(hit.colId)) {
2538
+ grid.setFocus(hit.rowIndex, hit.colId);
2539
+ if (!grid.rangeContains(hit.rowIndex, hit.colId)) grid.setRange({ start: hit, end: hit });
2540
+ }
2541
+ grid.contextMenu.open(x0, y0, hit.rowIndex, hit.colId);
2542
+ }, 550);
2543
+ window.addEventListener("pointermove", move);
2544
+ window.addEventListener("pointerup", cancel);
2545
+ window.addEventListener("pointercancel", cancel);
2546
+ };
1933
2547
  const onPointerDown = (e) => {
2548
+ lastPointer = e.pointerType || "mouse";
2549
+ if (lastPointer === "touch") {
2550
+ onTouchDown(e);
2551
+ return;
2552
+ }
1934
2553
  if (e.button === 0 && onHandleDown(e)) return;
1935
2554
  if (grid.options.rangeSelection === false) return;
1936
2555
  if (e.button !== 0 || e.shiftKey || e.target.closest("input, a, button")) return;
1937
2556
  const start = renderer.cellAt(e.target);
1938
2557
  if (!start || isSystemCol(start.colId)) return;
1939
2558
  const x0 = e.clientX, y0 = e.clientY;
2559
+ const adding = e.ctrlKey || e.metaKey;
1940
2560
  let dragging = false;
1941
2561
  const move = (ev) => {
1942
2562
  if (!dragging && Math.hypot(ev.clientX - x0, ev.clientY - y0) < DRAG_THRESHOLD2) return;
1943
- const hit = renderer.cellAt(document.elementFromPoint(ev.clientX, ev.clientY));
2563
+ const hit = renderer.cellAt(elementAt(root, ev.clientX, ev.clientY));
1944
2564
  if (!hit || isSystemCol(hit.colId)) return;
1945
2565
  if (!dragging) {
1946
2566
  dragging = true;
1947
2567
  root.classList.add("fg-range-dragging");
2568
+ if (adding) grid.addRange({ start, end: start });
1948
2569
  grid.setFocus(start.rowIndex, start.colId);
1949
2570
  }
1950
- grid.setRange({ start, end: hit });
2571
+ grid.setRange({ start, end: hit }, adding);
1951
2572
  };
1952
2573
  const up = () => {
1953
2574
  window.removeEventListener("pointermove", move);
@@ -1959,7 +2580,7 @@ function attachBody(grid) {
1959
2580
  window.addEventListener("pointerup", up);
1960
2581
  };
1961
2582
  const onKeyDown = (e) => {
1962
- if (e.target !== root) return;
2583
+ if (e.target !== root && e.target !== renderer.gridEl) return;
1963
2584
  const cols = grid.columns.displayed();
1964
2585
  const count = grid.rowModel.getRowCount();
1965
2586
  if (!cols.length || !count) return;
@@ -2001,7 +2622,7 @@ function attachBody(grid) {
2001
2622
  const range = grid.store.state.range;
2002
2623
  const end = range?.end ?? anchor;
2003
2624
  const pos = { rowIndex: Math.min(end.rowIndex, count - 1), colIndex: Math.max(0, cols.findIndex((c) => c.id === end.colId)) };
2004
- const next2 = nextFocus(e.key, { ctrl, shift: false }, pos, size);
2625
+ const next2 = nextFocus(keyOf(e), { ctrl, shift: false }, pos, size);
2005
2626
  if (!next2) return;
2006
2627
  e.preventDefault();
2007
2628
  const colId2 = cols[Math.max(firstData, next2.colIndex)].id;
@@ -2014,7 +2635,7 @@ function attachBody(grid) {
2014
2635
  if (!f) next = NAV_KEYS.has(e.key) && e.key !== "Tab" ? { rowIndex: 0, colIndex: 0 } : null;
2015
2636
  else {
2016
2637
  const colIndex2 = Math.max(0, cols.findIndex((c) => c.id === f.colId));
2017
- next = nextFocus(e.key, { ctrl, shift: e.shiftKey }, { rowIndex: Math.min(f.rowIndex, count - 1), colIndex: colIndex2 }, size);
2638
+ next = nextFocus(keyOf(e), { ctrl, shift: e.shiftKey }, { rowIndex: Math.min(f.rowIndex, count - 1), colIndex: colIndex2 }, size);
2018
2639
  }
2019
2640
  if (!next) return;
2020
2641
  e.preventDefault();
@@ -2027,17 +2648,17 @@ function attachBody(grid) {
2027
2648
  const nested = (e) => e.target?.closest?.(".fg-root") !== root;
2028
2649
  const onCopy = (e) => {
2029
2650
  if (fromField(e) || nested(e)) return;
2030
- const text = grid.selectionTSV(false);
2031
- if (!text || !e.clipboardData) return;
2032
- e.clipboardData.setData("text/plain", text);
2651
+ const text2 = grid.selectionTSV(false);
2652
+ if (!text2 || !e.clipboardData) return;
2653
+ e.clipboardData.setData("text/plain", text2);
2033
2654
  e.preventDefault();
2034
2655
  };
2035
2656
  const onPaste = (e) => {
2036
2657
  if (fromField(e) || nested(e)) return;
2037
- const text = e.clipboardData?.getData("text/plain");
2038
- if (!text) return;
2658
+ const text2 = e.clipboardData?.getData("text/plain");
2659
+ if (!text2) return;
2039
2660
  e.preventDefault();
2040
- grid.pasteText(text);
2661
+ grid.pasteText(text2);
2041
2662
  };
2042
2663
  body.addEventListener("click", onClick);
2043
2664
  body.addEventListener("dblclick", onDblClick);
@@ -2299,9 +2920,9 @@ var ColumnChooserPanel = class {
2299
2920
  box.checked = leaves.length > 0 && shown === leaves.length;
2300
2921
  box.indeterminate = shown > 0 && shown < leaves.length;
2301
2922
  box.disabled = leaves.every((l) => l.def.lockVisible);
2302
- const text = el("span", "fg-chooser-label");
2303
- text.textContent = labelOf(n);
2304
- row.append(box, text);
2923
+ const text2 = el("span", "fg-chooser-label");
2924
+ text2.textContent = labelOf(n);
2925
+ row.append(box, text2);
2305
2926
  if (!n.children && n.pinned) {
2306
2927
  const pin = el("span", "fg-chooser-pin");
2307
2928
  pin.textContent = n.pinned === "left" ? "\u21E4" : "\u21E5";
@@ -2379,14 +3000,14 @@ var ColumnChooserDialog = class {
2379
3000
  head.append(title, close);
2380
3001
  this.panel = new ColumnChooserPanel(this.grid);
2381
3002
  popup.append(head, this.panel.el);
2382
- document.body.append(popup);
3003
+ popupParent(this.grid.renderer.root).append(popup);
2383
3004
  const r = this.grid.renderer.root.getBoundingClientRect();
2384
3005
  const w = popup.offsetWidth || 280;
2385
3006
  popup.style.left = `${Math.max(4, Math.min(r.right - w - 8, window.innerWidth - w - 4))}px`;
2386
3007
  popup.style.top = `${Math.max(4, r.top + 8)}px`;
2387
3008
  this.popup = popup;
2388
3009
  const onDown = (e) => {
2389
- if (!popup.contains(e.target)) this.close();
3010
+ if (!eventInside(e, popup)) this.close();
2390
3011
  };
2391
3012
  const onKey = (e) => {
2392
3013
  if (e.key === "Escape") this.close();
@@ -2501,7 +3122,7 @@ var ContextMenu = class {
2501
3122
  const hit = grid.renderer.cellAt(target);
2502
3123
  if (!hit) return;
2503
3124
  e.preventDefault();
2504
- grid.renderer.root.focus({ preventScroll: true });
3125
+ grid.renderer.gridEl.focus({ preventScroll: true });
2505
3126
  if (!isSystemCol(hit.colId) && !grid.rangeContains(hit.rowIndex, hit.colId)) {
2506
3127
  grid.setFocus(hit.rowIndex, hit.colId);
2507
3128
  grid.setRange({ start: hit, end: hit });
@@ -2571,7 +3192,7 @@ var ContextMenu = class {
2571
3192
  const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
2572
3193
  const fail = () => this.grid.toast(this.grid.store.state.locale.pasteHint.replace("Ctrl+V", shortcut("V")));
2573
3194
  if (!clip?.readText) return fail();
2574
- clip.readText().then((text) => this.grid.pasteText(text), fail);
3195
+ clip.readText().then((text2) => this.grid.pasteText(text2), fail);
2575
3196
  }
2576
3197
  open(x, y, rowIndex, colId) {
2577
3198
  this.close();
@@ -2624,23 +3245,23 @@ var ContextMenu = class {
2624
3245
  prevSep = false;
2625
3246
  }
2626
3247
  if (popup.lastElementChild?.classList.contains("fg-cm-sep")) popup.lastElementChild.remove();
2627
- document.body.append(popup);
3248
+ popupParent(this.grid.renderer.root).append(popup);
2628
3249
  const w = popup.offsetWidth || 220, h = popup.offsetHeight || 300;
2629
3250
  popup.style.left = `${Math.max(4, Math.min(x, window.innerWidth - w - 4))}px`;
2630
3251
  popup.style.top = `${Math.max(4, Math.min(y, window.innerHeight - h - 4))}px`;
2631
3252
  this.popup = popup;
2632
3253
  const onDown = (e) => {
2633
- if (!popup.contains(e.target)) this.close();
3254
+ if (!eventInside(e, popup)) this.close();
2634
3255
  };
2635
3256
  const onKey = (e) => {
2636
3257
  if (e.key === "Escape") {
2637
3258
  this.close();
2638
- grid.renderer.root.focus();
3259
+ grid.renderer.gridEl.focus();
2639
3260
  return;
2640
3261
  }
2641
3262
  if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
2642
3263
  const buttons = [...popup.querySelectorAll("button:not(:disabled)")];
2643
- const i = buttons.indexOf(document.activeElement);
3264
+ const i = buttons.indexOf(activeElement(popup));
2644
3265
  const next = buttons[(i + (e.key === "ArrowDown" ? 1 : buttons.length - 1) + buttons.length) % buttons.length];
2645
3266
  next?.focus();
2646
3267
  e.preventDefault();
@@ -2678,7 +3299,7 @@ var FillHandle = class {
2678
3299
  let target = null;
2679
3300
  const rootEl = grid.renderer.root;
2680
3301
  const move = (ev) => {
2681
- const hit = grid.renderer.cellAt(document.elementFromPoint(ev.clientX, ev.clientY));
3302
+ const hit = grid.renderer.cellAt(elementAt(grid.renderer.root, ev.clientX, ev.clientY));
2682
3303
  const col = hit && grid.columns.getColumn(hit.colId);
2683
3304
  if (!hit || !col || isSystemCol(col.id)) return;
2684
3305
  const dr = hit.rowIndex > src.r2 ? hit.rowIndex - src.r2 : hit.rowIndex < src.r1 ? hit.rowIndex - src.r1 : 0;
@@ -2735,7 +3356,7 @@ var FillHandle = class {
2735
3356
  }
2736
3357
  const root = this.grid.renderer.root.getBoundingClientRect();
2737
3358
  const r = cell.getBoundingClientRect();
2738
- this.handle.style.left = `${r.right - root.left - 4}px`;
3359
+ this.handle.style.left = `${(this.grid.renderer.rtl ? r.left : r.right) - root.left - 4}px`;
2739
3360
  this.handle.style.top = `${r.bottom - root.top - 4}px`;
2740
3361
  if (!this.handle.isConnected) this.grid.renderer.root.append(this.handle);
2741
3362
  }
@@ -2784,8 +3405,8 @@ var Finder = class {
2784
3405
  if (i === void 0) return "";
2785
3406
  return i === this.index ? " fg-cell-match fg-cell-match-current" : " fg-cell-match";
2786
3407
  }
2787
- find(text) {
2788
- this.query = text.trim().toLowerCase();
3408
+ find(text2) {
3409
+ this.query = text2.trim().toLowerCase();
2789
3410
  this.matches = [];
2790
3411
  this.lookup.clear();
2791
3412
  this.index = -1;
@@ -2798,8 +3419,8 @@ var Finder = class {
2798
3419
  outer: for (let r = 0; r < rows.length; r++) {
2799
3420
  if (rows[r].kind) continue;
2800
3421
  for (let c = 0; c < cols.length; c++) {
2801
- const text2 = fmts[c](getCellValue(cols[c].def, rows[r].data)).toLowerCase();
2802
- if (!text2.includes(this.query)) continue;
3422
+ const text3 = fmts[c](getCellValue(cols[c].def, rows[r].data)).toLowerCase();
3423
+ if (!text3.includes(this.query)) continue;
2803
3424
  this.lookup.set(`${r}|${cols[c].id}`, this.matches.length);
2804
3425
  this.matches.push({ rowIndex: r, colId: cols[c].id });
2805
3426
  if (this.matches.length >= MAX_MATCHES) break outer;
@@ -2851,9 +3472,9 @@ var Finder = class {
2851
3472
  const bar = el("div", "fg-findbar", { role: "search" });
2852
3473
  const input = el("input", "fg-input", { type: "search", placeholder: loc.findPlaceholder, "aria-label": loc.findPlaceholder });
2853
3474
  this.countEl = el("span", "fg-findbar-count", { "aria-live": "polite" });
2854
- const btn = (label, text, run) => {
3475
+ const btn = (label, text2, run) => {
2855
3476
  const b = el("button", "fg-findbar-btn", { type: "button", "aria-label": label, title: label });
2856
- b.textContent = text;
3477
+ b.textContent = text2;
2857
3478
  b.addEventListener("click", run);
2858
3479
  return b;
2859
3480
  };
@@ -2885,40 +3506,10 @@ var Finder = class {
2885
3506
  this.bar = null;
2886
3507
  this.countEl = null;
2887
3508
  this.clear();
2888
- this.grid.renderer.root.focus({ preventScroll: true });
3509
+ this.grid.renderer.gridEl.focus({ preventScroll: true });
2889
3510
  }
2890
3511
  };
2891
3512
 
2892
- // src/merge.ts
2893
- function computeSpans(rows, def) {
2894
- const n = rows.length;
2895
- const start = new Int32Array(n);
2896
- const end = new Int32Array(n);
2897
- const same2 = typeof def.merge === "function" ? (a, b) => {
2898
- try {
2899
- return !!def.merge(getCellValue(def, rows[a]), getCellValue(def, rows[b]), rows[a], rows[b]);
2900
- } catch {
2901
- return false;
2902
- }
2903
- } : (() => {
2904
- const keys = rows.map((r) => {
2905
- const v = getCellValue(def, r);
2906
- return isBlank(v) ? null : setKey(v);
2907
- });
2908
- return (a, b) => keys[a] !== null && keys[a] === keys[b];
2909
- })();
2910
- let s = 0;
2911
- for (let i = 1; i <= n; i++) {
2912
- if (i < n && same2(i - 1, i)) continue;
2913
- for (let k = s; k < i; k++) {
2914
- start[k] = s;
2915
- end[k] = i - 1;
2916
- }
2917
- s = i;
2918
- }
2919
- return { start, end };
2920
- }
2921
-
2922
3513
  // src/fill.ts
2923
3514
  var DAY = 864e5;
2924
3515
  function fillSeries(source, count) {
@@ -2963,22 +3554,22 @@ function toTSV(rows) {
2963
3554
  const cell = (s) => /[\t\n\r"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
2964
3555
  return rows.map((r) => r.map(cell).join(" ")).join("\r\n");
2965
3556
  }
2966
- function parseTSV(text) {
2967
- return parseDelimited(text, " ");
3557
+ function parseTSV(text2) {
3558
+ return parseDelimited(text2, " ");
2968
3559
  }
2969
- function parseDelimited(text, sep) {
2970
- if (!text) return [];
3560
+ function parseDelimited(text2, sep) {
3561
+ if (!text2) return [];
2971
3562
  const rows = [];
2972
3563
  let row = [];
2973
3564
  let cell = "";
2974
3565
  let i = 0;
2975
3566
  let quoted = false;
2976
3567
  let atCellStart = true;
2977
- while (i < text.length) {
2978
- const ch = text[i];
3568
+ while (i < text2.length) {
3569
+ const ch = text2[i];
2979
3570
  if (quoted) {
2980
3571
  if (ch === '"') {
2981
- if (text[i + 1] === '"') {
3572
+ if (text2[i + 1] === '"') {
2982
3573
  cell += '"';
2983
3574
  i += 2;
2984
3575
  continue;
@@ -3010,7 +3601,7 @@ function parseDelimited(text, sep) {
3010
3601
  row = [];
3011
3602
  cell = "";
3012
3603
  atCellStart = true;
3013
- i += ch === "\r" && text[i + 1] === "\n" ? 2 : 1;
3604
+ i += ch === "\r" && text2[i + 1] === "\n" ? 2 : 1;
3014
3605
  continue;
3015
3606
  }
3016
3607
  cell += ch;
@@ -3040,8 +3631,8 @@ function copyValue(def, value, locale) {
3040
3631
  }
3041
3632
  var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "1", "y", "yes", "\uC608", "o", "on"]);
3042
3633
  var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "0", "n", "no", "\uC544\uB2C8\uC624", "x", "off"]);
3043
- function parseNumber(text) {
3044
- let s = text.trim();
3634
+ function parseNumber(text2) {
3635
+ let s = text2.trim();
3045
3636
  if (!s) return { ok: true, value: null };
3046
3637
  let negative = false;
3047
3638
  if (/^\(.*\)$/.test(s)) {
@@ -3061,8 +3652,8 @@ function validDate(y, m, d) {
3061
3652
  const date = new Date(y, m - 1, d);
3062
3653
  return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d ? date : null;
3063
3654
  }
3064
- function parseDate(text, previous) {
3065
- const s = text.trim();
3655
+ function parseDate(text2, previous) {
3656
+ const s = text2.trim();
3066
3657
  if (!s) return { ok: true, value: null };
3067
3658
  let date = null;
3068
3659
  let m = /^(\d{4})\s*[-./년]\s*(\d{1,2})\s*[-./월]\s*(\d{1,2})\s*[.일]?$/.exec(s);
@@ -3072,26 +3663,37 @@ function parseDate(text, previous) {
3072
3663
  if (!date) return { ok: false };
3073
3664
  return { ok: true, value: previous instanceof Date ? date : toDateKey(date) };
3074
3665
  }
3075
- function parseValue(def, text, previous) {
3666
+ function parseValue(def, text2, previous) {
3076
3667
  switch (def.type) {
3077
3668
  case "number":
3078
- return parseNumber(text);
3669
+ return parseNumber(text2);
3079
3670
  case "date":
3080
- return parseDate(text, previous);
3671
+ return parseDate(text2, previous);
3081
3672
  case "boolean": {
3082
- const t = text.trim().toLowerCase();
3673
+ const t = text2.trim().toLowerCase();
3083
3674
  if (!t) return { ok: true, value: null };
3084
3675
  if (TRUE_WORDS.has(t)) return { ok: true, value: true };
3085
3676
  if (FALSE_WORDS.has(t)) return { ok: true, value: false };
3086
3677
  return { ok: false };
3087
3678
  }
3088
3679
  default:
3089
- return { ok: true, value: text };
3680
+ return { ok: true, value: text2 };
3090
3681
  }
3091
3682
  }
3092
3683
 
3093
3684
  // src/editing/editor.ts
3094
- var normalizeOptions = (opts) => opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o) });
3685
+ var normalizeOptions = (opts) => opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o), disabled: false });
3686
+ var pad2 = (n) => String(n).padStart(2, "0");
3687
+ function localDateTime(v) {
3688
+ const d = v instanceof Date ? v : typeof v === "string" && v ? new Date(v) : null;
3689
+ if (!d || Number.isNaN(d.getTime())) return "";
3690
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
3691
+ }
3692
+ function timeOf(v) {
3693
+ if (v instanceof Date) return `${pad2(v.getHours())}:${pad2(v.getMinutes())}`;
3694
+ const m = typeof v === "string" ? /(\d{1,2}):(\d{2})/.exec(v) : null;
3695
+ return m ? `${pad2(Number(m[1]))}:${m[2]}` : "";
3696
+ }
3095
3697
  var CellEditor = class {
3096
3698
  constructor(grid) {
3097
3699
  this.grid = grid;
@@ -3103,12 +3705,15 @@ var CellEditor = class {
3103
3705
  canEdit(rowIndex, colId) {
3104
3706
  if (isSystemCol(colId)) return false;
3105
3707
  const col = this.grid.columns.getColumn(colId);
3106
- return !!col?.def.editable && !!col.def.field && rowIndex >= 0 && rowIndex < this.grid.rowModel.getRowCount() && !this.grid.rowModel.getRow(rowIndex).kind;
3708
+ if (!col?.def.field || rowIndex < 0 || rowIndex >= this.grid.rowModel.getRowCount()) return false;
3709
+ const node = this.grid.rowModel.getRow(rowIndex);
3710
+ return !node.kind && cellEditable(col.def, node.data, rowIndex);
3107
3711
  }
3108
3712
  start(rowIndex, colId, initialText) {
3109
3713
  if (this.cur) this.commit(null);
3110
3714
  if (!this.canEdit(rowIndex, colId)) return false;
3111
3715
  const { grid } = this;
3716
+ if (grid.veto("beforeEdit", { row: grid.rowModel.getRow(rowIndex).data, rowIndex, colId })) return false;
3112
3717
  const { renderer } = grid;
3113
3718
  const col = grid.columns.getColumn(colId);
3114
3719
  const def = col.def;
@@ -3144,12 +3749,24 @@ var CellEditor = class {
3144
3749
  const opt = el("option");
3145
3750
  opt.value = String(i);
3146
3751
  opt.textContent = o.label ?? String(o.value);
3752
+ opt.disabled = !!o.disabled;
3147
3753
  select.append(opt);
3148
3754
  });
3149
3755
  select.selectedIndex = Math.max(0, options.findIndex((o) => Object.is(o.value, value)));
3150
3756
  control = select;
3151
3757
  read = () => ({ value: options[select.selectedIndex]?.value ?? null });
3152
3758
  wrap.append(select);
3759
+ } else if (spec?.type === "time" || spec?.type === "datetime" || !spec && def.type === "date" && def.format === "datetime") {
3760
+ const isTime = spec?.type === "time";
3761
+ const input = el("input", "fg-editor-control", { type: isTime ? "time" : "datetime-local" });
3762
+ input.value = isTime ? timeOf(value) : localDateTime(value);
3763
+ control = input;
3764
+ read = () => {
3765
+ if (!input.value) return { value: null };
3766
+ if (isTime) return { value: input.value };
3767
+ return { value: value instanceof Date ? new Date(input.value) : input.value };
3768
+ };
3769
+ wrap.append(input);
3153
3770
  } else {
3154
3771
  const textarea = spec?.type === "textarea";
3155
3772
  const input = textarea ? el("textarea", "fg-editor-control") : el("input", "fg-editor-control");
@@ -3220,21 +3837,15 @@ var CellEditor = class {
3220
3837
  const old = cur.row[field];
3221
3838
  const raw = cur.read();
3222
3839
  let value;
3223
- if ("text" in raw) {
3840
+ if ("text" in raw && cur.def.formulas && isFormula(raw.text.trim())) {
3841
+ value = raw.text.trim();
3842
+ } else if ("text" in raw) {
3224
3843
  const parsed = parseValue(cur.def, raw.text, old);
3225
3844
  if (!parsed.ok) return this.fail(raw.text, loc.invalidValue);
3226
3845
  value = parsed.value;
3227
3846
  } else value = raw.value;
3228
- if (cur.def.required && isBlank(value)) return this.fail(value, loc.required);
3229
- if (cur.def.validate) {
3230
- let res;
3231
- try {
3232
- res = cur.def.validate(value, cur.row);
3233
- } catch {
3234
- res = loc.invalidValue;
3235
- }
3236
- if (res !== true) return this.fail(value, typeof res === "string" && res ? res : loc.invalidValue);
3237
- }
3847
+ const error = grid.cellError(cur.def, cur.row, value);
3848
+ if (error !== null) return this.fail(value, error);
3238
3849
  this.close(true);
3239
3850
  grid.applyEdit(cur.rowIndex, cur.colId, value);
3240
3851
  this.move(cur.rowIndex, cur.colId, move);
@@ -3255,7 +3866,7 @@ var CellEditor = class {
3255
3866
  if (!cur) return;
3256
3867
  this.cur = null;
3257
3868
  cur.dispose();
3258
- this.grid.renderer.root.focus({ preventScroll: true });
3869
+ this.grid.renderer.gridEl.focus({ preventScroll: true });
3259
3870
  this.grid.events.emit("editStop", { rowIndex: cur.rowIndex, colId: cur.colId, committed });
3260
3871
  }
3261
3872
  move(rowIndex, colId, move) {
@@ -3388,7 +3999,7 @@ function startResize(grid, e, colId) {
3388
3999
  grid.renderer.root.classList.add("fg-resizing");
3389
4000
  track(
3390
4001
  (ev) => {
3391
- grid.columns.setWidth(colId, startW + ev.clientX - startX);
4002
+ grid.columns.setWidth(colId, startW + (ev.clientX - startX) * (grid.renderer.rtl ? -1 : 1));
3392
4003
  grid.layoutChanged();
3393
4004
  },
3394
4005
  () => {
@@ -3408,7 +4019,7 @@ function beginDrag(grid, colId, cell, pane) {
3408
4019
  const ghost = el("div", `fg-drag-ghost fg-theme-${grid.store.state.theme}`);
3409
4020
  ghost.textContent = cell.querySelector(".fg-header-label")?.textContent ?? "";
3410
4021
  grid.applyFontTo(ghost);
3411
- document.body.append(ghost);
4022
+ popupParent(grid.renderer.root).append(ghost);
3412
4023
  const marker = el("div", "fg-drop-marker");
3413
4024
  marker.style.height = `${grid.renderer.headerEl.offsetHeight}px`;
3414
4025
  grid.renderer.root.append(marker);
@@ -3423,11 +4034,13 @@ function updateDrag(grid, colId, d, x, y) {
3423
4034
  let t = 0;
3424
4035
  for (const s of d.sibs) {
3425
4036
  const r = s.el.getBoundingClientRect();
3426
- if (x > r.left + r.width / 2) t++;
4037
+ if (grid.renderer.rtl ? x < r.left + r.width / 2 : x > r.left + r.width / 2) t++;
3427
4038
  }
3428
4039
  d.target = t;
3429
4040
  const rootLeft = grid.renderer.root.getBoundingClientRect().left;
3430
- const edge = t < d.sibs.length ? d.sibs[t].el.getBoundingClientRect().left : d.sibs.length ? d.sibs[d.sibs.length - 1].el.getBoundingClientRect().right : d.cell.getBoundingClientRect().left;
4041
+ const start = (el2) => grid.renderer.rtl ? el2.getBoundingClientRect().right : el2.getBoundingClientRect().left;
4042
+ const end = (el2) => grid.renderer.rtl ? el2.getBoundingClientRect().left : el2.getBoundingClientRect().right;
4043
+ const edge = t < d.sibs.length ? start(d.sibs[t].el) : d.sibs.length ? end(d.sibs[d.sibs.length - 1].el) : start(d.cell);
3431
4044
  d.marker.style.left = `${edge - rootLeft - 1}px`;
3432
4045
  }
3433
4046
  function finishDrag(grid, colId, d) {
@@ -3478,6 +4091,7 @@ function attachHeader(grid) {
3478
4091
  const cell = target.closest(".fg-header-cell");
3479
4092
  const colId = cell?.dataset.col;
3480
4093
  if (!cell || !colId || isSystemCol(colId) || target.closest(".fg-menu-btn")) return;
4094
+ if (target.closest("button, a, input, select, textarea, [data-fg-no-sort]") && target.closest(".fg-header-label")) return;
3481
4095
  e.preventDefault();
3482
4096
  if (target.closest(".fg-resize")) startResize(grid, e, colId);
3483
4097
  else startPress(grid, e, colId, cell);
@@ -3513,16 +4127,16 @@ function toCsv(rows, sep = ",") {
3513
4127
  };
3514
4128
  return rows.map((r) => r.map(cell).join(sep)).join("\r\n");
3515
4129
  }
3516
- function detectSeparator(text) {
3517
- const line = text.slice(0, text.search(/\r?\n|$/));
4130
+ function detectSeparator(text2) {
4131
+ const line = text2.slice(0, text2.search(/\r?\n|$/));
3518
4132
  const count = (ch) => line.split(ch).length - 1;
3519
4133
  const tabs = count(" "), semis = count(";"), commas = count(",");
3520
4134
  if (tabs && tabs >= commas && tabs >= semis) return " ";
3521
4135
  if (semis > commas) return ";";
3522
4136
  return ",";
3523
4137
  }
3524
- function parseCsv(text, sep = detectSeparator(text)) {
3525
- return parseDelimited(text.replace(/^\uFEFF/, ""), sep);
4138
+ function parseCsv(text2, sep = detectSeparator(text2)) {
4139
+ return parseDelimited(text2.replace(/^\uFEFF/, ""), sep);
3526
4140
  }
3527
4141
 
3528
4142
  // src/export/zip.ts
@@ -3671,9 +4285,42 @@ function writeXlsx(sheets) {
3671
4285
  const fmtIds = /* @__PURE__ */ new Map([["General", 0]]);
3672
4286
  const customFmts = [];
3673
4287
  const xfs = ['<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'];
3674
- const xfIndex = /* @__PURE__ */ new Map([["General|0", 0]]);
3675
- const styleOf = (fmt, bold) => {
3676
- const key = `${fmt}|${bold ? 1 : 0}`;
4288
+ const xfIndex = /* @__PURE__ */ new Map([["General||||", 0]]);
4289
+ const fonts = ['<font><sz val="11"/><name val="Calibri"/></font>', '<font><b/><sz val="11"/><name val="Calibri"/></font>'];
4290
+ const fontIndex = /* @__PURE__ */ new Map([["0|", 0], ["1|", 1]]);
4291
+ const fills = [
4292
+ '<fill><patternFill patternType="none"/></fill>',
4293
+ '<fill><patternFill patternType="gray125"/></fill>',
4294
+ '<fill><patternFill patternType="solid"><fgColor rgb="FFF2F4F7"/><bgColor indexed="64"/></patternFill></fill>'
4295
+ ];
4296
+ const fillIndex = /* @__PURE__ */ new Map([["FFF2F4F7", 2]]);
4297
+ const argb = (c) => c && /^[0-9a-f]{8}$/i.test(c) ? c.toUpperCase() : c && /^[0-9a-f]{6}$/i.test(c) ? `FF${c.toUpperCase()}` : "";
4298
+ const fontOf = (bold, color) => {
4299
+ const key = `${bold ? 1 : 0}|${color}`;
4300
+ let i = fontIndex.get(key);
4301
+ if (i === void 0) {
4302
+ i = fonts.length;
4303
+ fonts.push(`<font>${bold ? "<b/>" : ""}<sz val="11"/>${color ? `<color rgb="${color}"/>` : ""}<name val="Calibri"/></font>`);
4304
+ fontIndex.set(key, i);
4305
+ }
4306
+ return i;
4307
+ };
4308
+ const fillOf = (color) => {
4309
+ if (!color) return 0;
4310
+ let i = fillIndex.get(color);
4311
+ if (i === void 0) {
4312
+ i = fills.length;
4313
+ fills.push(`<fill><patternFill patternType="solid"><fgColor rgb="${color}"/><bgColor indexed="64"/></patternFill></fill>`);
4314
+ fillIndex.set(color, i);
4315
+ }
4316
+ return i;
4317
+ };
4318
+ const styleOf = (fmt, st) => {
4319
+ const color = argb(st.color);
4320
+ const fill = st.header ? "FFF2F4F7" : argb(st.fill);
4321
+ const bold = !!(st.bold || st.header);
4322
+ const align = st.align === "left" || st.align === "center" || st.align === "right" ? st.align : "";
4323
+ const key = `${fmt}|${bold ? 1 : ""}|${color}|${fill}|${align}`;
3677
4324
  let i = xfIndex.get(key);
3678
4325
  if (i !== void 0) return i;
3679
4326
  let id = fmtIds.get(fmt);
@@ -3682,8 +4329,11 @@ function writeXlsx(sheets) {
3682
4329
  customFmts.push(fmt);
3683
4330
  fmtIds.set(fmt, id);
3684
4331
  }
4332
+ const fontId = fontOf(bold, color);
4333
+ const fillId = fillOf(fill);
4334
+ const attrs = `numFmtId="${id}" fontId="${fontId}" fillId="${fillId}" borderId="0" xfId="0"` + (id ? ' applyNumberFormat="1"' : "") + (fontId ? ' applyFont="1"' : "") + (fillId ? ' applyFill="1"' : "") + (align ? ' applyAlignment="1"' : "");
3685
4335
  i = xfs.length;
3686
- xfs.push(`<xf numFmtId="${id}" fontId="${bold ? 1 : 0}" fillId="${bold ? 2 : 0}" borderId="0" xfId="0"${id ? ' applyNumberFormat="1"' : ""}${bold ? ' applyFont="1" applyFill="1"' : ""}/>`);
4336
+ xfs.push(align ? `<xf ${attrs}><alignment horizontal="${align}"/></xf>` : `<xf ${attrs}/>`);
3687
4337
  xfIndex.set(key, i);
3688
4338
  return i;
3689
4339
  };
@@ -3702,7 +4352,7 @@ function writeXlsx(sheets) {
3702
4352
  v = toSerial(v);
3703
4353
  }
3704
4354
  }
3705
- const s = styleOf(fmt, !!o.bold);
4355
+ const s = styleOf(fmt, o);
3706
4356
  const sa = s ? ` s="${s}"` : "";
3707
4357
  if (v === null || v === void 0 || v === "") return sa ? `<c r="${ref}"${sa}/>` : "";
3708
4358
  if (typeof v === "number") return Number.isFinite(v) ? `<c r="${ref}"${sa}><v>${v}</v></c>` : "";
@@ -3719,7 +4369,7 @@ function writeXlsx(sheets) {
3719
4369
  const merges = sheet.merges?.length ? `<mergeCells count="${sheet.merges.length}">${sheet.merges.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
3720
4370
  return `${XML}<worksheet xmlns="${NS}" xmlns:r="${REL_NS}">${views}<sheetFormatPr defaultRowHeight="15"/>${cols}<sheetData>${rowsXml}</sheetData>${filter}${merges}</worksheet>`;
3721
4371
  });
3722
- const styles = `${XML}<styleSheet xmlns="${NS}">` + (customFmts.length ? `<numFmts count="${customFmts.length}">${customFmts.map((f, i) => `<numFmt numFmtId="${164 + i}" formatCode="${esc(f)}"/>`).join("")}</numFmts>` : "") + `<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FFF2F4F7"/><bgColor indexed="64"/></patternFill></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${xfs.length}">${xfs.join("")}</cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
4372
+ const styles = `${XML}<styleSheet xmlns="${NS}">` + (customFmts.length ? `<numFmts count="${customFmts.length}">${customFmts.map((f, i) => `<numFmt numFmtId="${164 + i}" formatCode="${esc(f)}"/>`).join("")}</numFmts>` : "") + `<fonts count="${fonts.length}">${fonts.join("")}</fonts><fills count="${fills.length}">${fills.join("")}</fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="${xfs.length}">${xfs.join("")}</cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
3723
4373
  const files = [
3724
4374
  {
3725
4375
  name: "[Content_Types].xml",
@@ -3873,6 +4523,51 @@ function excelFormat(def) {
3873
4523
  if (o.style === "percent") return `0${decimals}%`;
3874
4524
  return `${o.useGrouping === false ? "0" : "#,##0"}${decimals}`;
3875
4525
  }
4526
+ function toArgb(css) {
4527
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(css.trim());
4528
+ if (hex) {
4529
+ const h = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join("") : hex[1];
4530
+ return `FF${h.toUpperCase()}`;
4531
+ }
4532
+ const m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?\s*\)$/i.exec(css.trim());
4533
+ if (!m) return "";
4534
+ const alpha = m[4] === void 0 ? 1 : m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
4535
+ if (!(alpha > 0)) return "";
4536
+ return `FF${[m[1], m[2], m[3]].map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
4537
+ }
4538
+ function classStyles(grid) {
4539
+ const root = grid.renderer.root;
4540
+ if (typeof getComputedStyle !== "function" || !root.isConnected) return { of: () => ({}), dispose: () => void 0 };
4541
+ const row = document.createElement("div");
4542
+ row.className = "fg-row";
4543
+ row.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;pointer-events:none";
4544
+ const cell = document.createElement("div");
4545
+ row.append(cell);
4546
+ root.append(row);
4547
+ const read = (cls) => {
4548
+ cell.className = cls ? `fg-cell ${cls}` : "fg-cell";
4549
+ const cs = getComputedStyle(cell);
4550
+ const w = cs.fontWeight;
4551
+ return { color: cs.color, fill: cs.backgroundColor, bold: w === "bold" || Number(w) >= 600 };
4552
+ };
4553
+ const base = read("");
4554
+ const cache2 = /* @__PURE__ */ new Map();
4555
+ return {
4556
+ of(classes) {
4557
+ let s = cache2.get(classes);
4558
+ if (!s) {
4559
+ const r = read(classes);
4560
+ s = {};
4561
+ if (r.color !== base.color && toArgb(r.color)) s.color = toArgb(r.color);
4562
+ if (r.fill !== base.fill && toArgb(r.fill)) s.fill = toArgb(r.fill);
4563
+ if (r.bold && !base.bold) s.bold = true;
4564
+ cache2.set(classes, s);
4565
+ }
4566
+ return s;
4567
+ },
4568
+ dispose: () => row.remove()
4569
+ };
4570
+ }
3876
4571
  function buildXlsx(grid, opts = {}) {
3877
4572
  const cols = exportColumns(grid, opts.columns);
3878
4573
  const code = grid.store.state.locale.code;
@@ -3899,43 +4594,86 @@ function buildXlsx(grid, opts = {}) {
3899
4594
  if (level < leafLevel) {
3900
4595
  const g = chain[level];
3901
4596
  const prev = ci > 0 ? chains[ci - 1][level] : null;
3902
- row.push({ v: prev === g ? null : header(g.def, g.id), bold: true });
4597
+ row.push({ v: prev === g ? null : header(g.def, g.id), header: true });
3903
4598
  if (prev !== g) {
3904
4599
  let end = ci;
3905
4600
  while (end + 1 < cols.length && chains[end + 1][level] === g) end++;
3906
4601
  if (end > ci) merges.push(`${name(ci)}${level + 1}:${name(end)}${level + 1}`);
3907
4602
  }
3908
4603
  } else if (level === leafLevel) {
3909
- row.push({ v: header(cols[ci].def, cols[ci].id), bold: true });
4604
+ row.push({ v: header(cols[ci].def, cols[ci].id), header: true });
3910
4605
  if (headRows - 1 > level) merges.push(`${name(ci)}${level + 1}:${name(ci)}${headRows}`);
3911
- } else row.push({ v: null, bold: true });
4606
+ } else row.push({ v: null, header: true });
3912
4607
  }
3913
4608
  rows.push(row);
3914
4609
  }
3915
4610
  }
3916
4611
  const fmts = cols.map((c) => excelFormat(c.def));
3917
4612
  const formatters = cols.map((c) => getFormatter(c.def, code));
3918
- for (const r of exportRows(grid, opts.rows)) {
3919
- rows.push(cols.map((c, i) => {
3920
- const v = getCellValue(c.def, r);
3921
- if (isBlank(v)) return null;
3922
- const fmt = fmts[i] ?? void 0;
3923
- switch (c.def.type) {
3924
- case "number": {
3925
- const n = typeof v === "number" ? v : Number(v);
3926
- return Number.isFinite(n) ? { v: n, fmt } : String(v);
3927
- }
3928
- case "date": {
3929
- const d = toDate(v);
3930
- return d ? { v: d, fmt } : String(v);
3931
- }
3932
- case "boolean":
3933
- return typeof v === "boolean" ? v : String(v);
3934
- default:
3935
- return formatters[i](v);
4613
+ const data = exportRows(grid, opts.rows);
4614
+ const styled = opts.styles !== false;
4615
+ const probe = styled && cols.some((c) => c.def.cellClass) ? classStyles(grid) : null;
4616
+ const valueOf = (c, i, v) => {
4617
+ if (isBlank(v)) return null;
4618
+ const fmt = fmts[i] ?? void 0;
4619
+ switch (c.def.type) {
4620
+ case "number": {
4621
+ const n = typeof v === "number" ? v : Number(v);
4622
+ return Number.isFinite(n) ? { v: n, fmt } : String(v);
3936
4623
  }
3937
- }));
4624
+ case "date": {
4625
+ const d = toDate(v);
4626
+ return d ? { v: d, fmt } : String(v);
4627
+ }
4628
+ case "boolean":
4629
+ return typeof v === "boolean" ? v : String(v);
4630
+ default:
4631
+ return formatters[i](v);
4632
+ }
4633
+ };
4634
+ try {
4635
+ data.forEach((r, ri) => {
4636
+ rows.push(cols.map((c, i) => {
4637
+ const v = getCellValue(c.def, r);
4638
+ const cell = valueOf(c, i, v);
4639
+ if (!styled) return cell;
4640
+ let style = c.def.align ? { align: c.def.align } : {};
4641
+ if (probe && c.def.cellClass) {
4642
+ let cls;
4643
+ try {
4644
+ cls = typeof c.def.cellClass === "function" ? c.def.cellClass({ value: v, formatted: formatters[i](v), row: r, rowIndex: ri, column: c.def }) : c.def.cellClass;
4645
+ } catch {
4646
+ cls = void 0;
4647
+ }
4648
+ if (cls) style = { ...style, ...probe.of(cls) };
4649
+ }
4650
+ if (!Object.keys(style).length) return cell;
4651
+ return cell !== null && typeof cell === "object" && !(cell instanceof Date) ? { ...cell, ...style } : { v: cell, ...style };
4652
+ }));
4653
+ });
4654
+ } finally {
4655
+ probe?.dispose();
3938
4656
  }
4657
+ const taken = /* @__PURE__ */ new Set();
4658
+ const letter = (i) => {
4659
+ let out = "";
4660
+ for (let n = i + 1; n > 0; n = Math.floor((n - 1) / 26)) out = String.fromCharCode(65 + (n - 1) % 26) + out;
4661
+ return out;
4662
+ };
4663
+ const mergeRange = (r1, c1, r2, c2) => {
4664
+ for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) if (taken.has(`${r},${c}`)) return;
4665
+ for (let r = r1; r <= r2; r++) for (let c = c1; c <= c2; c++) taken.add(`${r},${c}`);
4666
+ merges.push(`${letter(c1)}${headRows + r1 + 1}:${letter(c2)}${headRows + r2 + 1}`);
4667
+ };
4668
+ data.forEach((row, i) => cols.forEach((c, ci) => {
4669
+ const n = c.def.colSpan ? spanOf(c.def.colSpan, c.def, row, i, cols.length - ci) : 1;
4670
+ if (n > 1) mergeRange(i, ci, i, ci + n - 1);
4671
+ }));
4672
+ cols.forEach((c, ci) => {
4673
+ if (!c.def.merge && !c.def.rowSpan) return;
4674
+ const s = computeSpans(data, c.def);
4675
+ 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
+ });
3939
4677
  return writeXlsx([{
3940
4678
  name: opts.sheetName ?? "Sheet1",
3941
4679
  rows,
@@ -3978,8 +4716,8 @@ async function importInto(grid, file, opts = {}) {
3978
4716
  if (!s) throw new Error(`Sheet ${String(opts.sheet)} not found`);
3979
4717
  matrix = s.rows;
3980
4718
  } else {
3981
- const text = new TextDecoder().decode(bytes);
3982
- matrix = parseCsv(text, detectSeparator(text));
4719
+ const text2 = new TextDecoder().decode(bytes);
4720
+ matrix = parseCsv(text2, detectSeparator(text2));
3983
4721
  }
3984
4722
  if (!matrix.length) return { rows: [], skipped: 0 };
3985
4723
  const leaves = [];
@@ -4041,9 +4779,9 @@ function svg(tag, attrs = {}, cls = "") {
4041
4779
  for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
4042
4780
  return node;
4043
4781
  }
4044
- function tip(node, text) {
4782
+ function tip(node, text2) {
4045
4783
  const t = svg("title");
4046
- t.textContent = text;
4784
+ t.textContent = text2;
4047
4785
  node.append(t);
4048
4786
  }
4049
4787
  function niceTicks(min, max, count = 5) {
@@ -4479,7 +5217,7 @@ function buildPivot(source, config, defOf, labels, locale) {
4479
5217
  }
4480
5218
 
4481
5219
  // src/version.ts
4482
- var version = "0.7.1";
5220
+ var version = "0.8.0";
4483
5221
 
4484
5222
  // src/license.ts
4485
5223
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -4660,7 +5398,7 @@ var ColumnMenu = class {
4660
5398
  popup.append(this.section(items));
4661
5399
  const filter = this.filterSection(colId, () => this.close());
4662
5400
  if (filter) popup.append(filter);
4663
- document.body.append(popup);
5401
+ popupParent(this.grid.renderer.root).append(popup);
4664
5402
  const r = anchor.getBoundingClientRect();
4665
5403
  const w = popup.offsetWidth || 248;
4666
5404
  popup.style.left = `${Math.max(4, Math.min(r.right - w, window.innerWidth - w - 4))}px`;
@@ -4670,13 +5408,12 @@ var ColumnMenu = class {
4670
5408
  this.popup = popup;
4671
5409
  this.colId = colId;
4672
5410
  const onDown = (e) => {
4673
- const t = e.target;
4674
- if (!popup.contains(t) && !anchor.contains(t)) this.close();
5411
+ if (!eventInside(e, popup, anchor)) this.close();
4675
5412
  };
4676
5413
  const onKey = (e) => {
4677
5414
  if (e.key !== "Escape") return;
4678
5415
  this.close();
4679
- this.grid.renderer.root.focus();
5416
+ this.grid.renderer.gridEl.focus();
4680
5417
  };
4681
5418
  document.addEventListener("pointerdown", onDown, true);
4682
5419
  document.addEventListener("keydown", onKey, true);
@@ -4940,19 +5677,26 @@ var RowModel = class {
4940
5677
  if (!this.indexById) this.indexById = new Map(this.display.map((n, i) => [n.id, i]));
4941
5678
  return this.indexById.get(id) ?? -1;
4942
5679
  }
4943
- /** Prefix-sum offsets of the display rows (detail rows use `detailHeight`). */
4944
- layout(rowHeight, detailHeight) {
5680
+ /**
5681
+ * Prefix-sum offsets of the display rows: detail rows use `detailHeight`, rows with a
5682
+ * measured height (auto row height) use it, everything else `rowHeight`.
5683
+ */
5684
+ layout(rowHeight, detailHeight, heights, heightsVersion = 0) {
4945
5685
  const c = this.layoutCache;
4946
- if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight) return c;
5686
+ if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight && c.hv === heightsVersion) return c;
4947
5687
  const n = this.display.length;
4948
5688
  const offsets = new Float64Array(n + 1);
4949
5689
  let uniform = true;
5690
+ const measured = heights && heights.size ? heights : null;
4950
5691
  for (let i = 0; i < n; i++) {
4951
- const detail = this.display[i].kind === "detail";
4952
- if (detail) uniform = false;
4953
- offsets[i + 1] = offsets[i] + (detail ? detailHeight : rowHeight);
4954
- }
4955
- this.layoutCache = { rows: this.display, rh: rowHeight, dh: detailHeight, offsets, uniform };
5692
+ const node = this.display[i];
5693
+ let h = rowHeight;
5694
+ if (node.kind === "detail") h = detailHeight;
5695
+ else if (measured && !node.kind) h = measured.get(node.id) ?? rowHeight;
5696
+ if (h !== rowHeight) uniform = false;
5697
+ offsets[i + 1] = offsets[i] + h;
5698
+ }
5699
+ this.layoutCache = { rows: this.display, rh: rowHeight, dh: detailHeight, hv: heightsVersion, offsets, uniform };
4956
5700
  return this.layoutCache;
4957
5701
  }
4958
5702
  filter(input) {
@@ -4963,8 +5707,8 @@ var RowModel = class {
4963
5707
  const matches = this.nodes.filter((n) => {
4964
5708
  for (const p of preds) if (!p.pred(getCellValue(p.def, n.data))) return false;
4965
5709
  if (tokens.length) {
4966
- const text = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
4967
- if (!matchesQuick(tokens, text)) return false;
5710
+ const text2 = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
5711
+ if (!matchesQuick(tokens, text2)) return false;
4968
5712
  }
4969
5713
  return true;
4970
5714
  });
@@ -5059,8 +5803,10 @@ var RowModel = class {
5059
5803
  for (const n of nodes) {
5060
5804
  if (seen2.has(n.id)) continue;
5061
5805
  const kids = children.get(n.id) ?? [];
5806
+ const lazy = !kids.length && !!d.lazyChildren?.(n.id, n.data);
5062
5807
  const expanded = kids.length > 0 && this.isOpen(d, n.id, level);
5063
- const node = { ...n, level, hasChildren: kids.length > 0, expanded };
5808
+ const node = { ...n, level, hasChildren: kids.length > 0 || lazy, expanded };
5809
+ if (lazy && d.loading?.has(n.id)) node.loading = true;
5064
5810
  this.withDetails(d, [node], out);
5065
5811
  if (expanded) visit(kids, level + 1, /* @__PURE__ */ new Set([...seen2, n.id]));
5066
5812
  }
@@ -5328,6 +6074,17 @@ var Grid = class {
5328
6074
  /** 0-based page and rows per page (pagination only). */
5329
6075
  this.page = 0;
5330
6076
  this.pageSize = DEFAULT_PAGE_SIZE;
6077
+ /** Tree rows whose children were loaded (or are loading) through treeData.loadChildren. */
6078
+ this.treeLoaded = /* @__PURE__ */ new Set();
6079
+ this.treeLoading = /* @__PURE__ */ new Set();
6080
+ this.lazyChildren = (id, row) => {
6081
+ if (this.treeLoaded.has(id)) return false;
6082
+ try {
6083
+ return !!this.options.treeData?.hasChildren?.(row);
6084
+ } catch {
6085
+ return false;
6086
+ }
6087
+ };
5331
6088
  if (!Array.isArray(options.columns)) warn("options.columns must be an array");
5332
6089
  if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
5333
6090
  this.store = new Store({
@@ -5339,6 +6096,7 @@ var Grid = class {
5339
6096
  anchorId: null,
5340
6097
  focus: null,
5341
6098
  range: null,
6099
+ extraRanges: [],
5342
6100
  locale: resolveLocale(options.locale),
5343
6101
  theme: options.theme ?? "light"
5344
6102
  });
@@ -5415,6 +6173,7 @@ var Grid = class {
5415
6173
  });
5416
6174
  this.renderer.render();
5417
6175
  this.api = this.createApi();
6176
+ liveGrids.set(this.renderer.root, this);
5418
6177
  }
5419
6178
  collator() {
5420
6179
  const code = this.store.state.locale.code;
@@ -5425,6 +6184,7 @@ var Grid = class {
5425
6184
  }
5426
6185
  /** Column widths/order/pinning changed; rows are unaffected. */
5427
6186
  layoutChanged() {
6187
+ this.renderer.invalidateHeights();
5428
6188
  this.renderer.invalidate({ header: true, rows: true });
5429
6189
  this.refreshChoosers();
5430
6190
  }
@@ -5445,6 +6205,8 @@ var Grid = class {
5445
6205
  defaultExpanded: this.defaultExpanded,
5446
6206
  detailOpen: this.options.detailRenderer ? this.detailOpen : /* @__PURE__ */ new Set(),
5447
6207
  isMaster: this.options.isRowMaster,
6208
+ lazyChildren: this.options.treeData?.hasChildren ? this.lazyChildren : void 0,
6209
+ loading: this.treeLoading,
5448
6210
  version: this.displayVersion
5449
6211
  };
5450
6212
  }
@@ -5562,6 +6324,7 @@ var Grid = class {
5562
6324
  if (!this.options.pagination || !Number.isFinite(page)) return;
5563
6325
  const next = Math.max(0, Math.min(this.getPageCount() - 1, Math.floor(page)));
5564
6326
  if (next === this.page) return;
6327
+ if (this.veto("beforePageChange", { page: next, pageSize: this.pageSize })) return;
5565
6328
  this.page = next;
5566
6329
  this.pageMoved();
5567
6330
  }
@@ -5573,6 +6336,7 @@ var Grid = class {
5573
6336
  }
5574
6337
  if (size === this.pageSize) return;
5575
6338
  const first = this.page * this.pageSize;
6339
+ if (this.veto("beforePageChange", { page: Math.floor(first / size), pageSize: size })) return;
5576
6340
  this.pageSize = size;
5577
6341
  this.page = Math.floor(first / size);
5578
6342
  this.pageMoved();
@@ -5612,6 +6376,35 @@ var Grid = class {
5612
6376
  this.renderer.invalidate({ rows: true });
5613
6377
  this.events.emit("rowsLoadFailed", { start, end, error });
5614
6378
  }
6379
+ /**
6380
+ * Scroll `other` along with this grid and back. The follower's echo of a position we set is not
6381
+ * sent back, so a shorter grid that stops at its end does not pull the longer one back.
6382
+ */
6383
+ syncScroll(other, axis) {
6384
+ if (!other || typeof other.setScrollPosition !== "function" || other === this.api) return () => void 0;
6385
+ const written = /* @__PURE__ */ new Map();
6386
+ const key = (p) => `${p.top},${p.left}`;
6387
+ const link = (from, to) => from.on("scroll", (p) => {
6388
+ if (written.get(from) === key(p)) {
6389
+ written.delete(from);
6390
+ return;
6391
+ }
6392
+ to.setScrollPosition(axis === "vertical" ? { top: p.top } : axis === "horizontal" ? { left: p.left } : p);
6393
+ written.set(to, key(to.getScrollPosition()));
6394
+ });
6395
+ const offs = [link(this.api, other), link(other, this.api)];
6396
+ return () => {
6397
+ for (const off of offs) off();
6398
+ };
6399
+ }
6400
+ /** Emit a before-event; true when a listener called cancel(). */
6401
+ veto(type, payload) {
6402
+ let cancelled = false;
6403
+ this.events.emit(type, { ...payload, cancel: () => {
6404
+ cancelled = true;
6405
+ } });
6406
+ return cancelled;
6407
+ }
5615
6408
  /** Client-only operations log once and do nothing with a datasource. */
5616
6409
  clientOnly(what) {
5617
6410
  if (!this.rowModel.server) return false;
@@ -5661,10 +6454,45 @@ var Grid = class {
5661
6454
  expandKey(node) {
5662
6455
  return node.group ? node.group.key : node.id;
5663
6456
  }
6457
+ /** First expand of a lazy tree row: fetch its children, then show them expanded. */
6458
+ loadChildren(node) {
6459
+ const tree = this.options.treeData;
6460
+ const id = node.id;
6461
+ if (this.treeLoading.has(id)) return;
6462
+ if (typeof tree.loadChildren !== "function") {
6463
+ warn("treeData.hasChildren needs treeData.loadChildren");
6464
+ return;
6465
+ }
6466
+ this.treeLoading.add(id);
6467
+ this.displayChanged();
6468
+ const done = () => {
6469
+ this.treeLoading.delete(id);
6470
+ return !this.destroyed && this.store.state.data.includes(node.data);
6471
+ };
6472
+ Promise.resolve().then(() => tree.loadChildren(node.data)).then((rows) => {
6473
+ if (!done()) return;
6474
+ const kids = cleanRows(Array.isArray(rows) ? rows : []);
6475
+ this.treeLoaded.add(id);
6476
+ this.expanded.set(id, true);
6477
+ if (kids.length) this.replaceData([...this.store.state.data, ...kids]);
6478
+ this.displayChanged();
6479
+ this.events.emit("childrenLoaded", { row: node.data, rows: kids });
6480
+ this.events.emit("groupToggle", { key: id, expanded: true });
6481
+ }, (error) => {
6482
+ if (!done()) return;
6483
+ warn(`treeData.loadChildren failed for row "${id}"`);
6484
+ this.displayChanged();
6485
+ this.events.emit("childrenLoadFailed", { row: node.data, error });
6486
+ });
6487
+ }
5664
6488
  setGroupExpanded(key, open) {
5665
6489
  const node = this.rowModel.rows().find((n) => this.isExpandable(n) && this.expandKey(n) === key);
5666
6490
  const cur = node ? !!(node.group?.expanded ?? node.expanded) : this.expanded.get(key);
5667
6491
  if (cur === open) return;
6492
+ if (open && node && !node.group && this.options.treeData && this.lazyChildren(node.id, node.data)) {
6493
+ this.loadChildren(node);
6494
+ return;
6495
+ }
5668
6496
  this.expanded.set(key, open);
5669
6497
  this.displayChanged();
5670
6498
  this.events.emit("groupToggle", { key, expanded: open });
@@ -5723,7 +6551,113 @@ var Grid = class {
5723
6551
  /** Row order can only follow the source data when nothing reorders or hides rows. */
5724
6552
  canMoveRows() {
5725
6553
  const s = this.store.state;
5726
- return !this.rowModel.server && !s.sort.length && !Object.keys(s.filters).length && !s.quickFilter.trim() && !this.groupBy.length && !this.options.treeData;
6554
+ const tree = this.options.treeData;
6555
+ return !this.rowModel.server && !s.sort.length && !Object.keys(s.filters).length && !s.quickFilter.trim() && !this.groupBy.length && (!tree || typeof tree.setParentId === "function");
6556
+ }
6557
+ get dragOptions() {
6558
+ const o = this.options.rowDrag;
6559
+ return o && typeof o === "object" ? o : {};
6560
+ }
6561
+ parentOf(row) {
6562
+ let p = null;
6563
+ try {
6564
+ p = this.options.treeData.getParentId(row);
6565
+ } catch {
6566
+ p = null;
6567
+ }
6568
+ return p == null ? null : String(p);
6569
+ }
6570
+ rawParent(row) {
6571
+ try {
6572
+ return this.options.treeData.getParentId(row) ?? null;
6573
+ } catch {
6574
+ return null;
6575
+ }
6576
+ }
6577
+ /** The id of `row` as the data stores parent ids: a number when other rows hold numeric parent ids. */
6578
+ idValue(row) {
6579
+ const id = this.rowModel.idOf(row);
6580
+ for (const r of this.store.state.data) {
6581
+ const p = this.rawParent(r);
6582
+ if (p !== null && String(p) === id) return p;
6583
+ }
6584
+ const numeric = this.store.state.data.some((r) => typeof this.rawParent(r) === "number");
6585
+ return numeric && id.trim() !== "" && Number.isFinite(Number(id)) ? Number(id) : id;
6586
+ }
6587
+ /** Can `row` land here? Trees refuse the row's own subtree; then the user's canDrop rule decides. */
6588
+ canDropRow(row, target, position, from = this) {
6589
+ if (!this.canMoveRows() || target === row) return false;
6590
+ if (from === this && this.options.treeData && target) {
6591
+ const own = this.rowModel.idOf(row);
6592
+ const seen2 = /* @__PURE__ */ new Set();
6593
+ for (let id = this.rowModel.idOf(target); id !== null && !seen2.has(id); ) {
6594
+ if (id === own) return false;
6595
+ seen2.add(id);
6596
+ const node = this.rowModel.allNodes().find((n) => n.id === id);
6597
+ id = node ? this.parentOf(node.data) : null;
6598
+ }
6599
+ }
6600
+ const rule = this.dragOptions.canDrop;
6601
+ if (!rule) return true;
6602
+ try {
6603
+ return rule({ row, target, position, fromOtherGrid: from !== this }) !== false;
6604
+ } catch {
6605
+ warn("rowDrag.canDrop threw; the drop was refused");
6606
+ return false;
6607
+ }
6608
+ }
6609
+ /**
6610
+ * Move `row` before, after or inside `target` (null = to the end). In tree data the row takes the
6611
+ * target's parent, or the target itself for 'inside'. `from` is the grid the row is dragged out of.
6612
+ */
6613
+ dropRow(row, target, position = "after", from = this) {
6614
+ if (this.clientOnly("dropRow") || !row || typeof row !== "object") return false;
6615
+ const tree = this.options.treeData;
6616
+ if (!tree && position === "inside") position = "after";
6617
+ const data = this.store.state.data;
6618
+ const external = from !== this;
6619
+ const fromIndex = external ? -1 : data.indexOf(row);
6620
+ if (!external && fromIndex < 0) return false;
6621
+ if (target && !data.includes(target)) return false;
6622
+ if (external && (!from.canMoveRows() || data.includes(row))) return false;
6623
+ if (!this.canDropRow(row, target, position, from)) return false;
6624
+ const oldRaw = tree ? this.rawParent(row) : null;
6625
+ const parentRow = !tree || !target ? null : position === "inside" ? target : this.store.state.data.find((r) => this.rowModel.idOf(r) === this.parentOf(target)) ?? null;
6626
+ const newRaw = parentRow ? this.idValue(parentRow) : null;
6627
+ const next = data.filter((r) => r !== row);
6628
+ const at = !target || position === "inside" ? next.length : next.indexOf(target) + (position === "after" ? 1 : 0);
6629
+ next.splice(at, 0, row);
6630
+ const moved = next.some((r, i) => r !== data[i]);
6631
+ if (!external && !moved && String(oldRaw ?? "") === String(newRaw ?? "")) return false;
6632
+ const setParent = (raw, parent) => {
6633
+ try {
6634
+ tree.setParentId(row, raw, parent);
6635
+ } catch {
6636
+ warn("treeData.setParentId threw");
6637
+ }
6638
+ };
6639
+ const oldParentRow = tree ? this.store.state.data.find((r) => this.rowModel.idOf(r) === this.parentOf(row)) ?? null : null;
6640
+ if (external) {
6641
+ from.removeRows([row]);
6642
+ if (tree) setParent(newRaw, parentRow);
6643
+ this.addRows([row], at);
6644
+ } else {
6645
+ if (tree) setParent(newRaw, parentRow);
6646
+ this.replaceData(next);
6647
+ this.history.push({
6648
+ undo: () => {
6649
+ if (tree) setParent(oldRaw, oldParentRow);
6650
+ this.replaceData(data);
6651
+ },
6652
+ redo: () => {
6653
+ if (tree) setParent(newRaw, parentRow);
6654
+ this.replaceData(next);
6655
+ }
6656
+ });
6657
+ this.events.emit("rowDragEnd", { row, fromIndex, toIndex: at });
6658
+ }
6659
+ this.events.emit("rowDrop", { row, target, position, fromOtherGrid: external, fromGrid: external ? from.api : null });
6660
+ return true;
5727
6661
  }
5728
6662
  moveRow(row, toIndex, record = true) {
5729
6663
  if (this.clientOnly("moveRow")) return false;
@@ -5753,6 +6687,7 @@ var Grid = class {
5753
6687
  if (this.options.rowHeight == null) this.renderer.opts.rowHeight = autoRowHeight(effective);
5754
6688
  if (this.options.headerHeight == null) this.renderer.opts.headerHeight = autoHeaderHeight(effective);
5755
6689
  this.renderer.applyFont(size, family);
6690
+ this.renderer.invalidateHeights();
5756
6691
  this.menu.close();
5757
6692
  this.layoutChanged();
5758
6693
  }
@@ -5788,30 +6723,55 @@ var Grid = class {
5788
6723
  dataColumns() {
5789
6724
  return this.columns.displayed().filter((c) => !isSystemCol(c.id));
5790
6725
  }
5791
- setRange(range) {
5792
- let next = null;
5793
- if (range && this.options.rangeSelection !== false) {
5794
- const count = this.rowModel.getRowCount();
5795
- const ok = (p) => !isSystemCol(p.colId) && !!this.columns.getColumn(p.colId) && p.rowIndex >= 0 && p.rowIndex < count;
5796
- if (ok(range.start) && ok(range.end)) next = { start: { ...range.start }, end: { ...range.end } };
5797
- }
6726
+ validRange(range) {
6727
+ if (!range || this.options.rangeSelection === false) return null;
6728
+ const count = this.rowModel.getRowCount();
6729
+ const ok = (p) => !!p && !isSystemCol(p.colId) && !!this.columns.getColumn(p.colId) && p.rowIndex >= 0 && p.rowIndex < count;
6730
+ return ok(range.start) && ok(range.end) ? { start: { ...range.start }, end: { ...range.end } } : null;
6731
+ }
6732
+ /** Set the active range. Earlier Ctrl ranges are dropped unless `keepOthers`. */
6733
+ setRange(range, keepOthers = false) {
6734
+ const next = this.validRange(range);
6735
+ const extras = keepOthers || !this.store.state.extraRanges.length ? this.store.state.extraRanges : [];
5798
6736
  const cur = this.store.state.range;
5799
- if (JSON.stringify(cur) === JSON.stringify(next)) return;
5800
- this.store.set({ range: next });
6737
+ if (JSON.stringify(cur) === JSON.stringify(next) && extras === this.store.state.extraRanges) return;
6738
+ this.store.set({ range: next, extraRanges: extras });
5801
6739
  this.events.emit("rangeChange", { range: next && { start: { ...next.start }, end: { ...next.end } } });
5802
6740
  }
5803
- /** Range in displayed column indexes, or null. */
5804
- rangeBounds() {
5805
- const r = this.store.state.range;
6741
+ /** Ctrl+click / Ctrl+drag: keep what is selected (the focused cell when there is no range) and start a new range. */
6742
+ addRange(range) {
6743
+ const { range: active, focus } = this.store.state;
6744
+ const keep = this.validRange(active ?? (focus ? { start: focus, end: focus } : null));
6745
+ if (keep) this.store.set({ extraRanges: [...this.store.state.extraRanges, keep] });
6746
+ this.setRange(range, true);
6747
+ }
6748
+ getRanges() {
6749
+ const all2 = [...this.store.state.extraRanges];
6750
+ if (this.store.state.range) all2.push(this.store.state.range);
6751
+ return all2.map((r) => ({ start: { ...r.start }, end: { ...r.end } }));
6752
+ }
6753
+ setRanges(ranges) {
6754
+ const list = (Array.isArray(ranges) ? ranges : []).map((r) => this.validRange(r)).filter((r) => !!r);
6755
+ this.store.set({ extraRanges: list.slice(0, -1) });
6756
+ this.setRange(list[list.length - 1] ?? null, true);
6757
+ }
6758
+ boundsOf(r) {
5806
6759
  if (!r) return null;
5807
6760
  const a = this.columns.getColumn(r.start.colId), b = this.columns.getColumn(r.end.colId);
5808
6761
  if (!a || !b) return null;
5809
6762
  return normalizeRange({ start: { rowIndex: r.start.rowIndex, colIndex: a.index }, end: { rowIndex: r.end.rowIndex, colIndex: b.index } });
5810
6763
  }
6764
+ /** Active range in displayed column indexes, or null. */
6765
+ rangeBounds() {
6766
+ return this.boundsOf(this.store.state.range);
6767
+ }
6768
+ /** Every range (Ctrl ranges first, the active one last). */
6769
+ allRangeBounds() {
6770
+ return [...this.store.state.extraRanges, this.store.state.range].map((r) => this.boundsOf(r)).filter((b) => !!b);
6771
+ }
5811
6772
  rangeContains(rowIndex, colId) {
5812
- const b = this.rangeBounds();
5813
6773
  const col = this.columns.getColumn(colId);
5814
- return !!b && !!col && rangeContains(b, rowIndex, col.index);
6774
+ return !!col && this.allRangeBounds().some((b) => rangeContains(b, rowIndex, col.index));
5815
6775
  }
5816
6776
  selectAllCells() {
5817
6777
  const cols = this.dataColumns();
@@ -5829,6 +6789,18 @@ var Grid = class {
5829
6789
  const checkedRows = rows;
5830
6790
  let out;
5831
6791
  let cols;
6792
+ const all2 = rows ? [] : this.allRangeBounds();
6793
+ if (all2.length > 1) {
6794
+ const sameCols = all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2);
6795
+ const sameRows = all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2);
6796
+ if (sameCols || sameRows) {
6797
+ const parts = [...all2].sort((x, y) => sameCols ? x.r1 - y.r1 : x.c1 - y.c1);
6798
+ const blocks = parts.map((p) => this.rangeMatrix(p));
6799
+ const matrix2 = sameCols ? blocks.flat() : blocks[0].map((_, ri) => blocks.flatMap((bl) => bl[ri] ?? []));
6800
+ if (headers) matrix2.unshift((sameCols ? parts.slice(0, 1) : parts).flatMap((p) => this.rangeHeaders(p)));
6801
+ return toTSV(matrix2);
6802
+ }
6803
+ }
5832
6804
  if (b) {
5833
6805
  out = [];
5834
6806
  for (let i = b.r1; i <= b.r2; i++) {
@@ -5848,22 +6820,51 @@ var Grid = class {
5848
6820
  out = [node.data];
5849
6821
  cols = [col];
5850
6822
  }
5851
- const matrix = out.map((r) => cols.map((c) => copyValue(c.def, getCellValue(c.def, r), code)));
6823
+ const matrix = out.map((r) => cols.map((c) => this.copyCell(c.def, r, code)));
5852
6824
  if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
5853
6825
  return toTSV(matrix);
5854
6826
  }
6827
+ rangeHeaders(b) {
6828
+ return this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id)).map((c) => c.def.header ?? c.def.field ?? c.id);
6829
+ }
6830
+ /** Copy text of one range. */
6831
+ rangeMatrix(b) {
6832
+ const cols = this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
6833
+ const code = this.store.state.locale.code;
6834
+ const out = [];
6835
+ for (let i = b.r1; i <= b.r2; i++) {
6836
+ const node = this.rowModel.getRow(i);
6837
+ if (node && !node.kind) out.push(cols.map((c) => this.copyCell(c.def, node.data, code)));
6838
+ }
6839
+ return out;
6840
+ }
6841
+ copyCell(def, row, code) {
6842
+ const value = getCellValue(def, row);
6843
+ const rule = def.copyable;
6844
+ if (rule === false) return "";
6845
+ if (typeof rule === "function") {
6846
+ let ok = false;
6847
+ try {
6848
+ ok = rule({ value, row, column: def }) !== false;
6849
+ } catch {
6850
+ ok = false;
6851
+ }
6852
+ if (!ok) return "";
6853
+ }
6854
+ return copyValue(def, value, code);
6855
+ }
5855
6856
  copyToClipboard(headers, rows = false) {
5856
- const text = this.selectionTSV(headers, rows);
5857
- if (text && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
6857
+ const text2 = this.selectionTSV(headers, rows);
6858
+ if (text2 && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
5858
6859
  try {
5859
- navigator.clipboard.writeText(text)?.catch?.(() => void 0);
6860
+ navigator.clipboard.writeText(text2)?.catch?.(() => void 0);
5860
6861
  } catch {
5861
6862
  }
5862
6863
  }
5863
- return text;
6864
+ return text2;
5864
6865
  }
5865
- pasteText(text) {
5866
- const matrix = parseTSV(text);
6866
+ pasteText(text2) {
6867
+ const matrix = parseTSV(text2);
5867
6868
  const result = { changed: 0, skipped: 0 };
5868
6869
  if (!matrix.length) return result;
5869
6870
  const displayed = this.columns.displayed();
@@ -5895,7 +6896,7 @@ var Grid = class {
5895
6896
  const raw = single && b ? matrix[0][0] : matrix[dr]?.[dc];
5896
6897
  if (raw === void 0) continue;
5897
6898
  const def = col.def;
5898
- if (!def.editable || !def.field) {
6899
+ if (!def.field || !cellEditable(def, node.data, ri)) {
5899
6900
  result.skipped++;
5900
6901
  continue;
5901
6902
  }
@@ -5918,34 +6919,67 @@ var Grid = class {
5918
6919
  const spec = def.editor;
5919
6920
  if (spec && typeof spec === "object" && spec.type === "select") {
5920
6921
  const opts = (typeof spec.options === "function" ? spec.options(row) : spec.options) ?? [];
5921
- const text = raw.trim().toLowerCase();
5922
- const hit = opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o) }).find((o) => String(o.value).toLowerCase() === text || (o.label ?? "").toLowerCase() === text);
5923
- if (!hit) return { ok: false };
6922
+ const text2 = raw.trim().toLowerCase();
6923
+ const hit = opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o), disabled: false }).find((o) => String(o.value).toLowerCase() === text2 || (o.label ?? "").toLowerCase() === text2);
6924
+ if (!hit || hit.disabled) return { ok: false };
5924
6925
  value = hit.value;
6926
+ } else if (def.formulas && isFormula(raw.trim())) {
6927
+ value = raw.trim();
5925
6928
  } else {
5926
6929
  const parsed = parseValue(def, raw, oldValue);
5927
6930
  if (!parsed.ok) return parsed;
5928
6931
  value = parsed.value;
5929
6932
  }
5930
- if (def.required && isBlank(value)) return { ok: false };
6933
+ if (this.cellError(def, row, value) !== null) return { ok: false };
6934
+ return { ok: true, value };
6935
+ }
6936
+ /** Why `value` cannot go into this cell (required, unique, validate), or null when it can. */
6937
+ cellError(def, row, value) {
6938
+ const loc = this.store.state.locale;
6939
+ if (def.required && isBlank(value)) return loc.required;
6940
+ if (def.unique && def.field && !isBlank(value)) {
6941
+ const key = value instanceof Date ? value.getTime() : String(value);
6942
+ const field = def.field;
6943
+ if (this.store.state.data.some((r) => {
6944
+ if (r === row) return false;
6945
+ const v = r[field];
6946
+ return !isBlank(v) && (v instanceof Date ? v.getTime() : String(v)) === key;
6947
+ })) return loc.duplicate;
6948
+ }
5931
6949
  if (def.validate) {
6950
+ let res;
5932
6951
  try {
5933
- if (def.validate(value, row) !== true) return { ok: false };
6952
+ res = def.validate(value, row);
5934
6953
  } catch {
5935
- return { ok: false };
6954
+ res = loc.invalidValue;
5936
6955
  }
6956
+ if (res !== true) return typeof res === "string" && res ? res : loc.invalidValue;
5937
6957
  }
5938
- return { ok: true, value };
6958
+ return null;
6959
+ }
6960
+ validateAll() {
6961
+ const out = [];
6962
+ const cols = this.columns.getState().map((st) => ({ id: st.colId, def: this.columns.getNode(st.colId).def })).filter((c) => c.def.field && c.def.editable && (c.def.required || c.def.unique || c.def.validate));
6963
+ this.store.state.data.forEach((row, i) => {
6964
+ const id = this.rowModel.idOf(row);
6965
+ const rowIndex = this.rowModel.indexOfId(id);
6966
+ for (const c of cols) {
6967
+ if (!cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
6968
+ const message = this.cellError(c.def, row, row[c.def.field]);
6969
+ if (message !== null) out.push({ row, rowIndex, colId: c.id, message });
6970
+ }
6971
+ });
6972
+ return out;
5939
6973
  }
5940
6974
  // ── merge, aggregates, fill, pinned rows ────────────────────
5941
6975
  /** Merge blocks for every `merge` column over the displayed rows (memoized). */
5942
6976
  mergeSpans() {
5943
- if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge)) return NO_SPANS;
6977
+ if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan)) return NO_SPANS;
5944
6978
  const rows = this.rowModel.rows();
5945
6979
  const ver = this.editVersion * 1e6 + this.columns.displayed().length;
5946
6980
  if (this.spanCache && this.spanCache.rows === rows && this.spanCache.ver === ver) return this.spanCache.spans;
5947
6981
  const spans = /* @__PURE__ */ new Map();
5948
- const merged = this.columns.displayed().filter((c) => c.def.merge);
6982
+ const merged = this.columns.displayed().filter((c) => c.def.merge || c.def.rowSpan);
5949
6983
  if (merged.length && !rows.some((n) => n.kind)) {
5950
6984
  const data = rows.map((n) => n.data);
5951
6985
  for (const c of merged) spans.set(c.id, computeSpans(data, c.def));
@@ -5955,15 +6989,22 @@ var Grid = class {
5955
6989
  }
5956
6990
  /** Count / sum / min / max over the selected range (numbers only for the math). */
5957
6991
  rangeAggregates() {
5958
- const b = this.rangeBounds();
5959
- if (!b) return null;
5960
- const cols = this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
6992
+ const all2 = this.allRangeBounds();
6993
+ if (!all2.length) return null;
6994
+ const displayed = this.columns.displayed();
5961
6995
  const out = { cells: 0, count: 0, numbers: 0, sum: 0, min: Infinity, max: -Infinity };
5962
- for (let r = b.r1; r <= b.r2; r++) {
6996
+ const seen2 = all2.length > 1 ? /* @__PURE__ */ new Set() : null;
6997
+ for (const b of all2) for (let r = b.r1; r <= b.r2; r++) {
5963
6998
  const node = this.rowModel.getRow(r);
5964
6999
  if (!node || node.kind) continue;
5965
7000
  const row = node.data;
5966
- for (const c of cols) {
7001
+ for (const c of displayed.slice(b.c1, b.c2 + 1)) {
7002
+ if (isSystemCol(c.id)) continue;
7003
+ if (seen2) {
7004
+ const k = `${r},${c.index}`;
7005
+ if (seen2.has(k)) continue;
7006
+ seen2.add(k);
7007
+ }
5967
7008
  out.cells++;
5968
7009
  const v = getCellValue(c.def, row);
5969
7010
  if (isBlank(v)) continue;
@@ -5998,13 +7039,8 @@ var Grid = class {
5998
7039
  const col = displayed[c];
5999
7040
  const node = this.rowModel.getRow(r);
6000
7041
  const def = col?.def;
6001
- if (!node || node.kind || !def?.editable || !def.field || isSystemCol(col.id)) return;
6002
- if (def.required && isBlank(value2)) return;
6003
- try {
6004
- if (def.validate && def.validate(value2, node.data) !== true) return;
6005
- } catch {
6006
- return;
6007
- }
7042
+ if (!node || node.kind || !def?.field || isSystemCol(col.id) || !cellEditable(def, node.data, r)) return;
7043
+ if (this.cellError(def, node.data, value2) !== null) return;
6008
7044
  this.writeCell(changes, node.data, def.field, col.id, r, value2);
6009
7045
  };
6010
7046
  const value = (r, c) => getCellValue(displayed[c].def, this.rowModel.getRow(r).data);
@@ -6038,6 +7074,7 @@ var Grid = class {
6038
7074
  writeCell(changes, row, field, colId, rowIndex, value) {
6039
7075
  const oldValue = row[field];
6040
7076
  if (Object.is(oldValue, value)) return false;
7077
+ if (this.veto("beforeCellChange", { row, rowIndex, colId, oldValue, newValue: value })) return false;
6041
7078
  this.tracker.beforeChange(row);
6042
7079
  row[field] = value;
6043
7080
  changes.push({ row, field, colId, oldValue, newValue: value });
@@ -6059,6 +7096,7 @@ var Grid = class {
6059
7096
  this.afterDataEdit();
6060
7097
  }
6061
7098
  afterDataEdit() {
7099
+ this.renderer.invalidateHeights();
6062
7100
  this.editVersion++;
6063
7101
  this.refreshSummary();
6064
7102
  this.renderer.invalidate({ rows: true });
@@ -6074,17 +7112,17 @@ var Grid = class {
6074
7112
  /** Delete/Backspace: clear editable cells in the range, or the focused cell. */
6075
7113
  clearCells() {
6076
7114
  const displayed = this.columns.displayed();
6077
- const b = this.rangeBounds();
7115
+ const all2 = this.allRangeBounds();
6078
7116
  const f = this.store.state.focus;
6079
7117
  const fi = f ? this.columns.getColumn(f.colId)?.index : void 0;
6080
- const box = b ?? (f && fi !== void 0 ? { r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi } : null);
6081
- if (!box) return;
7118
+ const boxes = all2.length ? all2 : f && fi !== void 0 ? [{ r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi }] : [];
7119
+ if (!boxes.length) return;
6082
7120
  const changes = [];
6083
- for (let r = box.r1; r <= box.r2; r++) {
7121
+ for (const box of boxes) for (let r = box.r1; r <= box.r2; r++) {
6084
7122
  const node = this.rowModel.getRow(r);
6085
7123
  for (let c = box.c1; c <= box.c2; c++) {
6086
7124
  const def = displayed[c]?.def;
6087
- if (node && !node.kind && def?.editable && def.field) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
7125
+ if (node && !node.kind && def?.field && cellEditable(def, node.data, r)) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
6088
7126
  }
6089
7127
  }
6090
7128
  this.endCellTx(changes);
@@ -6264,6 +7302,7 @@ var Grid = class {
6264
7302
  }
6265
7303
  return s.dir === "asc" || s.dir === "desc";
6266
7304
  });
7305
+ if (this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) })) return;
6267
7306
  this.store.set({ sort: valid });
6268
7307
  this.events.emit("sortChange", { sort: [...valid] });
6269
7308
  }
@@ -6285,6 +7324,7 @@ var Grid = class {
6285
7324
  warn(`setFilter: unknown column "${colId}"`);
6286
7325
  return;
6287
7326
  }
7327
+ if (this.veto("beforeFilter", { colId, model })) return;
6288
7328
  const filters = { ...this.store.state.filters };
6289
7329
  if (model) filters[colId] = model;
6290
7330
  else delete filters[colId];
@@ -6305,9 +7345,21 @@ var Grid = class {
6305
7345
  this.store.set({ selected, anchorId });
6306
7346
  this.events.emit("selectionChange", { selected: this.selectedRows() });
6307
7347
  }
7348
+ /** Data rows that may be checked (isRowSelectable). */
7349
+ rowSelectable(row) {
7350
+ const f = this.options.isRowSelectable;
7351
+ if (!f) return true;
7352
+ try {
7353
+ return !!f(row);
7354
+ } catch {
7355
+ return false;
7356
+ }
7357
+ }
6308
7358
  selectFromClick(index, toggle, range) {
6309
7359
  const mode = this.options.rowSelection;
6310
- if (!mode || index < 0 || index >= this.rowModel.getRowCount() || this.rowModel.getRow(index).kind) return;
7360
+ if (!mode || index < 0 || index >= this.rowModel.getRowCount()) return;
7361
+ const target = this.rowModel.getRow(index);
7362
+ if (target.kind || !this.rowSelectable(target.data)) return;
6311
7363
  const r = nextSelection({
6312
7364
  selected: this.store.state.selected,
6313
7365
  anchorIndex: this.anchorIndex(),
@@ -6317,11 +7369,12 @@ var Grid = class {
6317
7369
  toggle,
6318
7370
  range
6319
7371
  });
6320
- if (this.rowModel.rows() !== this.rowModel.dataRows()) {
7372
+ if (this.rowModel.rows() !== this.rowModel.dataRows() || this.options.isRowSelectable) {
6321
7373
  for (const id of r.selected) {
6322
7374
  if (this.store.state.selected.has(id)) continue;
6323
7375
  const i = this.rowModel.indexOfId(id);
6324
- if (i >= 0 && this.rowModel.getRow(i).kind) r.selected.delete(id);
7376
+ const n = i >= 0 ? this.rowModel.getRow(i) : null;
7377
+ if (n && (n.kind || !this.rowSelectable(n.data))) r.selected.delete(id);
6325
7378
  }
6326
7379
  }
6327
7380
  this.setSelected(r.selected, this.rowModel.getRow(r.anchorIndex)?.id ?? null);
@@ -6333,8 +7386,13 @@ var Grid = class {
6333
7386
  const i = this.rowModel.indexOfId(id);
6334
7387
  return i < 0 ? null : i;
6335
7388
  }
6336
- toggleSelectAll() {
7389
+ /** Rows select-all acts on: displayed data rows that are selectable. */
7390
+ selectableRows() {
6337
7391
  const rows = this.rowModel.dataRows();
7392
+ return this.options.isRowSelectable ? rows.filter((n) => this.rowSelectable(n.data)) : rows;
7393
+ }
7394
+ toggleSelectAll() {
7395
+ const rows = this.selectableRows();
6338
7396
  const sel = this.store.state.selected;
6339
7397
  const all2 = rows.length > 0 && rows.every((n) => sel.has(n.id));
6340
7398
  const next = new Set(sel);
@@ -6354,7 +7412,7 @@ var Grid = class {
6354
7412
  if (this.options.rowSelection) sys.push(SELECT_COLUMN);
6355
7413
  return [...sys, ...defs];
6356
7414
  }
6357
- setData(rows) {
7415
+ setData(rows, options) {
6358
7416
  if (this.clientOnly("setData")) return;
6359
7417
  if (!Array.isArray(rows)) {
6360
7418
  warn("setData: rows must be an array");
@@ -6364,8 +7422,11 @@ var Grid = class {
6364
7422
  this.tracker.reset();
6365
7423
  this.history.clear();
6366
7424
  this.detailOpen.clear();
7425
+ this.treeLoaded.clear();
7426
+ this.treeLoading.clear();
6367
7427
  this.renderer.dropDetail();
6368
7428
  this.store.set({ data: cleanRows(rows) });
7429
+ if (options?.scrollToTop) this.renderer.setScroll({ top: 0, left: 0 });
6369
7430
  const sel = this.store.state.selected;
6370
7431
  if (sel.size) {
6371
7432
  if (this.options.getRowId) {
@@ -6439,7 +7500,7 @@ var Grid = class {
6439
7500
  return fn(...a);
6440
7501
  });
6441
7502
  return {
6442
- setData: guard((rows) => this.setData(rows)),
7503
+ setData: guard((rows, o) => this.setData(rows, o)),
6443
7504
  getData: guard(() => this.store.state.data),
6444
7505
  setColumns: guard((defs) => this.setColumns(defs)),
6445
7506
  getColumnState: guard(() => this.columns.getState()),
@@ -6453,10 +7514,14 @@ var Grid = class {
6453
7514
  getFilterModel: guard(() => ({ ...this.store.state.filters })),
6454
7515
  setQuickFilter: guard((t) => this.store.set({ quickFilter: t ?? "" })),
6455
7516
  getSelectedRows: guard(() => this.selectedRows()),
6456
- selectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set([...this.store.state.selected, ...this.rowModel.dataRows().map((n) => n.id)]))),
7517
+ selectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set([...this.store.state.selected, ...this.selectableRows().map((n) => n.id)]))),
6457
7518
  deselectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set(), null)),
6458
7519
  getDisplayedRowCount: guard(() => this.rowModel.getRowCount()),
6459
7520
  scrollToRow: guard((i) => this.renderer.scrollToRow(i)),
7521
+ validate: guard(() => this.validateAll()),
7522
+ getScrollPosition: guard(() => this.renderer.getScroll()),
7523
+ setScrollPosition: guard((p) => this.renderer.setScroll(p ?? {})),
7524
+ syncScroll: guard((other, axis = "both") => this.syncScroll(other, axis)),
6460
7525
  setTheme: guard((t) => this.store.set({ theme: t })),
6461
7526
  setLocale: guard((l) => this.store.set({ locale: resolveLocale(l) })),
6462
7527
  setFont: guard((f) => this.setFont(f)),
@@ -6473,8 +7538,10 @@ var Grid = class {
6473
7538
  return r && { start: { ...r.start }, end: { ...r.end } };
6474
7539
  }),
6475
7540
  setRange: guard((r) => this.setRange(r)),
7541
+ getRanges: guard(() => this.getRanges()),
7542
+ setRanges: guard((list) => this.setRanges(list)),
6476
7543
  copySelection: guard((o) => this.copyToClipboard(!!o?.headers, !!o?.rows)),
6477
- pasteText: guard((text) => this.pasteText(text)),
7544
+ pasteText: guard((text2) => this.pasteText(text2)),
6478
7545
  startEdit: guard((rowIndex, colId) => this.editor.start(rowIndex, colId)),
6479
7546
  stopEdit: guard((cancel) => cancel ? this.editor.cancel() : void this.editor.commit(null)),
6480
7547
  addRows: guard((rows, index) => this.addRows(rows, index)),
@@ -6491,7 +7558,7 @@ var Grid = class {
6491
7558
  canRedo: guard(() => this.history.canRedo()),
6492
7559
  fillRange: guard((t) => this.fillRange(t)),
6493
7560
  setPinnedRows: guard((r) => this.setPinnedRows(r)),
6494
- find: guard((text) => this.finder.find(text)),
7561
+ find: guard((text2) => this.finder.find(text2)),
6495
7562
  findNext: guard(() => this.finder.next()),
6496
7563
  findPrevious: guard(() => this.finder.previous()),
6497
7564
  clearFind: guard(() => this.finder.clear()),
@@ -6503,6 +7570,7 @@ var Grid = class {
6503
7570
  setDetailExpanded: guard((row, open) => this.setDetailExpanded(row, open)),
6504
7571
  isDetailExpanded: guard((row) => this.isDetailExpanded(row)),
6505
7572
  moveRow: guard((row, toIndex) => this.moveRow(row, toIndex)),
7573
+ dropRow: guard((row, target, position) => this.dropRow(row, target, position)),
6506
7574
  setPage: guard((p) => this.setPage(p)),
6507
7575
  getPage: guard(() => this.getPage()),
6508
7576
  getPageCount: guard(() => this.getPageCount()),
@@ -6524,6 +7592,7 @@ var Grid = class {
6524
7592
  destroy() {
6525
7593
  if (this.destroyed) return;
6526
7594
  this.destroyed = true;
7595
+ liveGrids.delete(this.renderer.root);
6527
7596
  for (const d of this.disposers) d();
6528
7597
  this.renderer.destroy();
6529
7598
  this.events.clear();