@freegridjs/core 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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.1";
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" };
@@ -4497,8 +5235,12 @@ function isDevHost(host) {
4497
5235
  function domainMatches(host, domains) {
4498
5236
  const h = host.toLowerCase().replace(/\.$/, "");
4499
5237
  return domains.some((d) => {
4500
- const dom = d.toLowerCase().replace(/^\*\./, "").replace(/\.$/, "");
4501
- return !!dom && (h === dom || h.endsWith(`.${dom}`));
5238
+ const raw = d.toLowerCase().replace(/\.$/, "");
5239
+ const wildcard = raw.startsWith("*.");
5240
+ const dom = wildcard ? raw.slice(2) : raw;
5241
+ if (!dom) return false;
5242
+ if (h === dom || h === `www.${dom}`) return true;
5243
+ return wildcard && h.endsWith(`.${dom}`);
4502
5244
  });
4503
5245
  }
4504
5246
  var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
@@ -4660,7 +5402,7 @@ var ColumnMenu = class {
4660
5402
  popup.append(this.section(items));
4661
5403
  const filter = this.filterSection(colId, () => this.close());
4662
5404
  if (filter) popup.append(filter);
4663
- document.body.append(popup);
5405
+ popupParent(this.grid.renderer.root).append(popup);
4664
5406
  const r = anchor.getBoundingClientRect();
4665
5407
  const w = popup.offsetWidth || 248;
4666
5408
  popup.style.left = `${Math.max(4, Math.min(r.right - w, window.innerWidth - w - 4))}px`;
@@ -4670,13 +5412,12 @@ var ColumnMenu = class {
4670
5412
  this.popup = popup;
4671
5413
  this.colId = colId;
4672
5414
  const onDown = (e) => {
4673
- const t = e.target;
4674
- if (!popup.contains(t) && !anchor.contains(t)) this.close();
5415
+ if (!eventInside(e, popup, anchor)) this.close();
4675
5416
  };
4676
5417
  const onKey = (e) => {
4677
5418
  if (e.key !== "Escape") return;
4678
5419
  this.close();
4679
- this.grid.renderer.root.focus();
5420
+ this.grid.renderer.gridEl.focus();
4680
5421
  };
4681
5422
  document.addEventListener("pointerdown", onDown, true);
4682
5423
  document.addEventListener("keydown", onKey, true);
@@ -4940,19 +5681,26 @@ var RowModel = class {
4940
5681
  if (!this.indexById) this.indexById = new Map(this.display.map((n, i) => [n.id, i]));
4941
5682
  return this.indexById.get(id) ?? -1;
4942
5683
  }
4943
- /** Prefix-sum offsets of the display rows (detail rows use `detailHeight`). */
4944
- layout(rowHeight, detailHeight) {
5684
+ /**
5685
+ * Prefix-sum offsets of the display rows: detail rows use `detailHeight`, rows with a
5686
+ * measured height (auto row height) use it, everything else `rowHeight`.
5687
+ */
5688
+ layout(rowHeight, detailHeight, heights, heightsVersion = 0) {
4945
5689
  const c = this.layoutCache;
4946
- if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight) return c;
5690
+ if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight && c.hv === heightsVersion) return c;
4947
5691
  const n = this.display.length;
4948
5692
  const offsets = new Float64Array(n + 1);
4949
5693
  let uniform = true;
5694
+ const measured = heights && heights.size ? heights : null;
4950
5695
  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 };
5696
+ const node = this.display[i];
5697
+ let h = rowHeight;
5698
+ if (node.kind === "detail") h = detailHeight;
5699
+ else if (measured && !node.kind) h = measured.get(node.id) ?? rowHeight;
5700
+ if (h !== rowHeight) uniform = false;
5701
+ offsets[i + 1] = offsets[i] + h;
5702
+ }
5703
+ this.layoutCache = { rows: this.display, rh: rowHeight, dh: detailHeight, hv: heightsVersion, offsets, uniform };
4956
5704
  return this.layoutCache;
4957
5705
  }
4958
5706
  filter(input) {
@@ -4963,8 +5711,8 @@ var RowModel = class {
4963
5711
  const matches = this.nodes.filter((n) => {
4964
5712
  for (const p of preds) if (!p.pred(getCellValue(p.def, n.data))) return false;
4965
5713
  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;
5714
+ const text2 = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
5715
+ if (!matchesQuick(tokens, text2)) return false;
4968
5716
  }
4969
5717
  return true;
4970
5718
  });
@@ -5059,8 +5807,10 @@ var RowModel = class {
5059
5807
  for (const n of nodes) {
5060
5808
  if (seen2.has(n.id)) continue;
5061
5809
  const kids = children.get(n.id) ?? [];
5810
+ const lazy = !kids.length && !!d.lazyChildren?.(n.id, n.data);
5062
5811
  const expanded = kids.length > 0 && this.isOpen(d, n.id, level);
5063
- const node = { ...n, level, hasChildren: kids.length > 0, expanded };
5812
+ const node = { ...n, level, hasChildren: kids.length > 0 || lazy, expanded };
5813
+ if (lazy && d.loading?.has(n.id)) node.loading = true;
5064
5814
  this.withDetails(d, [node], out);
5065
5815
  if (expanded) visit(kids, level + 1, /* @__PURE__ */ new Set([...seen2, n.id]));
5066
5816
  }
@@ -5328,6 +6078,17 @@ var Grid = class {
5328
6078
  /** 0-based page and rows per page (pagination only). */
5329
6079
  this.page = 0;
5330
6080
  this.pageSize = DEFAULT_PAGE_SIZE;
6081
+ /** Tree rows whose children were loaded (or are loading) through treeData.loadChildren. */
6082
+ this.treeLoaded = /* @__PURE__ */ new Set();
6083
+ this.treeLoading = /* @__PURE__ */ new Set();
6084
+ this.lazyChildren = (id, row) => {
6085
+ if (this.treeLoaded.has(id)) return false;
6086
+ try {
6087
+ return !!this.options.treeData?.hasChildren?.(row);
6088
+ } catch {
6089
+ return false;
6090
+ }
6091
+ };
5331
6092
  if (!Array.isArray(options.columns)) warn("options.columns must be an array");
5332
6093
  if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
5333
6094
  this.store = new Store({
@@ -5339,6 +6100,7 @@ var Grid = class {
5339
6100
  anchorId: null,
5340
6101
  focus: null,
5341
6102
  range: null,
6103
+ extraRanges: [],
5342
6104
  locale: resolveLocale(options.locale),
5343
6105
  theme: options.theme ?? "light"
5344
6106
  });
@@ -5415,6 +6177,7 @@ var Grid = class {
5415
6177
  });
5416
6178
  this.renderer.render();
5417
6179
  this.api = this.createApi();
6180
+ liveGrids.set(this.renderer.root, this);
5418
6181
  }
5419
6182
  collator() {
5420
6183
  const code = this.store.state.locale.code;
@@ -5425,6 +6188,7 @@ var Grid = class {
5425
6188
  }
5426
6189
  /** Column widths/order/pinning changed; rows are unaffected. */
5427
6190
  layoutChanged() {
6191
+ this.renderer.invalidateHeights();
5428
6192
  this.renderer.invalidate({ header: true, rows: true });
5429
6193
  this.refreshChoosers();
5430
6194
  }
@@ -5445,6 +6209,8 @@ var Grid = class {
5445
6209
  defaultExpanded: this.defaultExpanded,
5446
6210
  detailOpen: this.options.detailRenderer ? this.detailOpen : /* @__PURE__ */ new Set(),
5447
6211
  isMaster: this.options.isRowMaster,
6212
+ lazyChildren: this.options.treeData?.hasChildren ? this.lazyChildren : void 0,
6213
+ loading: this.treeLoading,
5448
6214
  version: this.displayVersion
5449
6215
  };
5450
6216
  }
@@ -5562,6 +6328,7 @@ var Grid = class {
5562
6328
  if (!this.options.pagination || !Number.isFinite(page)) return;
5563
6329
  const next = Math.max(0, Math.min(this.getPageCount() - 1, Math.floor(page)));
5564
6330
  if (next === this.page) return;
6331
+ if (this.veto("beforePageChange", { page: next, pageSize: this.pageSize })) return;
5565
6332
  this.page = next;
5566
6333
  this.pageMoved();
5567
6334
  }
@@ -5573,6 +6340,7 @@ var Grid = class {
5573
6340
  }
5574
6341
  if (size === this.pageSize) return;
5575
6342
  const first = this.page * this.pageSize;
6343
+ if (this.veto("beforePageChange", { page: Math.floor(first / size), pageSize: size })) return;
5576
6344
  this.pageSize = size;
5577
6345
  this.page = Math.floor(first / size);
5578
6346
  this.pageMoved();
@@ -5612,6 +6380,35 @@ var Grid = class {
5612
6380
  this.renderer.invalidate({ rows: true });
5613
6381
  this.events.emit("rowsLoadFailed", { start, end, error });
5614
6382
  }
6383
+ /**
6384
+ * Scroll `other` along with this grid and back. The follower's echo of a position we set is not
6385
+ * sent back, so a shorter grid that stops at its end does not pull the longer one back.
6386
+ */
6387
+ syncScroll(other, axis) {
6388
+ if (!other || typeof other.setScrollPosition !== "function" || other === this.api) return () => void 0;
6389
+ const written = /* @__PURE__ */ new Map();
6390
+ const key = (p) => `${p.top},${p.left}`;
6391
+ const link = (from, to) => from.on("scroll", (p) => {
6392
+ if (written.get(from) === key(p)) {
6393
+ written.delete(from);
6394
+ return;
6395
+ }
6396
+ to.setScrollPosition(axis === "vertical" ? { top: p.top } : axis === "horizontal" ? { left: p.left } : p);
6397
+ written.set(to, key(to.getScrollPosition()));
6398
+ });
6399
+ const offs = [link(this.api, other), link(other, this.api)];
6400
+ return () => {
6401
+ for (const off of offs) off();
6402
+ };
6403
+ }
6404
+ /** Emit a before-event; true when a listener called cancel(). */
6405
+ veto(type, payload) {
6406
+ let cancelled = false;
6407
+ this.events.emit(type, { ...payload, cancel: () => {
6408
+ cancelled = true;
6409
+ } });
6410
+ return cancelled;
6411
+ }
5615
6412
  /** Client-only operations log once and do nothing with a datasource. */
5616
6413
  clientOnly(what) {
5617
6414
  if (!this.rowModel.server) return false;
@@ -5661,10 +6458,45 @@ var Grid = class {
5661
6458
  expandKey(node) {
5662
6459
  return node.group ? node.group.key : node.id;
5663
6460
  }
6461
+ /** First expand of a lazy tree row: fetch its children, then show them expanded. */
6462
+ loadChildren(node) {
6463
+ const tree = this.options.treeData;
6464
+ const id = node.id;
6465
+ if (this.treeLoading.has(id)) return;
6466
+ if (typeof tree.loadChildren !== "function") {
6467
+ warn("treeData.hasChildren needs treeData.loadChildren");
6468
+ return;
6469
+ }
6470
+ this.treeLoading.add(id);
6471
+ this.displayChanged();
6472
+ const done = () => {
6473
+ this.treeLoading.delete(id);
6474
+ return !this.destroyed && this.store.state.data.includes(node.data);
6475
+ };
6476
+ Promise.resolve().then(() => tree.loadChildren(node.data)).then((rows) => {
6477
+ if (!done()) return;
6478
+ const kids = cleanRows(Array.isArray(rows) ? rows : []);
6479
+ this.treeLoaded.add(id);
6480
+ this.expanded.set(id, true);
6481
+ if (kids.length) this.replaceData([...this.store.state.data, ...kids]);
6482
+ this.displayChanged();
6483
+ this.events.emit("childrenLoaded", { row: node.data, rows: kids });
6484
+ this.events.emit("groupToggle", { key: id, expanded: true });
6485
+ }, (error) => {
6486
+ if (!done()) return;
6487
+ warn(`treeData.loadChildren failed for row "${id}"`);
6488
+ this.displayChanged();
6489
+ this.events.emit("childrenLoadFailed", { row: node.data, error });
6490
+ });
6491
+ }
5664
6492
  setGroupExpanded(key, open) {
5665
6493
  const node = this.rowModel.rows().find((n) => this.isExpandable(n) && this.expandKey(n) === key);
5666
6494
  const cur = node ? !!(node.group?.expanded ?? node.expanded) : this.expanded.get(key);
5667
6495
  if (cur === open) return;
6496
+ if (open && node && !node.group && this.options.treeData && this.lazyChildren(node.id, node.data)) {
6497
+ this.loadChildren(node);
6498
+ return;
6499
+ }
5668
6500
  this.expanded.set(key, open);
5669
6501
  this.displayChanged();
5670
6502
  this.events.emit("groupToggle", { key, expanded: open });
@@ -5723,7 +6555,113 @@ var Grid = class {
5723
6555
  /** Row order can only follow the source data when nothing reorders or hides rows. */
5724
6556
  canMoveRows() {
5725
6557
  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;
6558
+ const tree = this.options.treeData;
6559
+ return !this.rowModel.server && !s.sort.length && !Object.keys(s.filters).length && !s.quickFilter.trim() && !this.groupBy.length && (!tree || typeof tree.setParentId === "function");
6560
+ }
6561
+ get dragOptions() {
6562
+ const o = this.options.rowDrag;
6563
+ return o && typeof o === "object" ? o : {};
6564
+ }
6565
+ parentOf(row) {
6566
+ let p = null;
6567
+ try {
6568
+ p = this.options.treeData.getParentId(row);
6569
+ } catch {
6570
+ p = null;
6571
+ }
6572
+ return p == null ? null : String(p);
6573
+ }
6574
+ rawParent(row) {
6575
+ try {
6576
+ return this.options.treeData.getParentId(row) ?? null;
6577
+ } catch {
6578
+ return null;
6579
+ }
6580
+ }
6581
+ /** The id of `row` as the data stores parent ids: a number when other rows hold numeric parent ids. */
6582
+ idValue(row) {
6583
+ const id = this.rowModel.idOf(row);
6584
+ for (const r of this.store.state.data) {
6585
+ const p = this.rawParent(r);
6586
+ if (p !== null && String(p) === id) return p;
6587
+ }
6588
+ const numeric = this.store.state.data.some((r) => typeof this.rawParent(r) === "number");
6589
+ return numeric && id.trim() !== "" && Number.isFinite(Number(id)) ? Number(id) : id;
6590
+ }
6591
+ /** Can `row` land here? Trees refuse the row's own subtree; then the user's canDrop rule decides. */
6592
+ canDropRow(row, target, position, from = this) {
6593
+ if (!this.canMoveRows() || target === row) return false;
6594
+ if (from === this && this.options.treeData && target) {
6595
+ const own = this.rowModel.idOf(row);
6596
+ const seen2 = /* @__PURE__ */ new Set();
6597
+ for (let id = this.rowModel.idOf(target); id !== null && !seen2.has(id); ) {
6598
+ if (id === own) return false;
6599
+ seen2.add(id);
6600
+ const node = this.rowModel.allNodes().find((n) => n.id === id);
6601
+ id = node ? this.parentOf(node.data) : null;
6602
+ }
6603
+ }
6604
+ const rule = this.dragOptions.canDrop;
6605
+ if (!rule) return true;
6606
+ try {
6607
+ return rule({ row, target, position, fromOtherGrid: from !== this }) !== false;
6608
+ } catch {
6609
+ warn("rowDrag.canDrop threw; the drop was refused");
6610
+ return false;
6611
+ }
6612
+ }
6613
+ /**
6614
+ * Move `row` before, after or inside `target` (null = to the end). In tree data the row takes the
6615
+ * target's parent, or the target itself for 'inside'. `from` is the grid the row is dragged out of.
6616
+ */
6617
+ dropRow(row, target, position = "after", from = this) {
6618
+ if (this.clientOnly("dropRow") || !row || typeof row !== "object") return false;
6619
+ const tree = this.options.treeData;
6620
+ if (!tree && position === "inside") position = "after";
6621
+ const data = this.store.state.data;
6622
+ const external = from !== this;
6623
+ const fromIndex = external ? -1 : data.indexOf(row);
6624
+ if (!external && fromIndex < 0) return false;
6625
+ if (target && !data.includes(target)) return false;
6626
+ if (external && (!from.canMoveRows() || data.includes(row))) return false;
6627
+ if (!this.canDropRow(row, target, position, from)) return false;
6628
+ const oldRaw = tree ? this.rawParent(row) : null;
6629
+ const parentRow = !tree || !target ? null : position === "inside" ? target : this.store.state.data.find((r) => this.rowModel.idOf(r) === this.parentOf(target)) ?? null;
6630
+ const newRaw = parentRow ? this.idValue(parentRow) : null;
6631
+ const next = data.filter((r) => r !== row);
6632
+ const at = !target || position === "inside" ? next.length : next.indexOf(target) + (position === "after" ? 1 : 0);
6633
+ next.splice(at, 0, row);
6634
+ const moved = next.some((r, i) => r !== data[i]);
6635
+ if (!external && !moved && String(oldRaw ?? "") === String(newRaw ?? "")) return false;
6636
+ const setParent = (raw, parent) => {
6637
+ try {
6638
+ tree.setParentId(row, raw, parent);
6639
+ } catch {
6640
+ warn("treeData.setParentId threw");
6641
+ }
6642
+ };
6643
+ const oldParentRow = tree ? this.store.state.data.find((r) => this.rowModel.idOf(r) === this.parentOf(row)) ?? null : null;
6644
+ if (external) {
6645
+ from.removeRows([row]);
6646
+ if (tree) setParent(newRaw, parentRow);
6647
+ this.addRows([row], at);
6648
+ } else {
6649
+ if (tree) setParent(newRaw, parentRow);
6650
+ this.replaceData(next);
6651
+ this.history.push({
6652
+ undo: () => {
6653
+ if (tree) setParent(oldRaw, oldParentRow);
6654
+ this.replaceData(data);
6655
+ },
6656
+ redo: () => {
6657
+ if (tree) setParent(newRaw, parentRow);
6658
+ this.replaceData(next);
6659
+ }
6660
+ });
6661
+ this.events.emit("rowDragEnd", { row, fromIndex, toIndex: at });
6662
+ }
6663
+ this.events.emit("rowDrop", { row, target, position, fromOtherGrid: external, fromGrid: external ? from.api : null });
6664
+ return true;
5727
6665
  }
5728
6666
  moveRow(row, toIndex, record = true) {
5729
6667
  if (this.clientOnly("moveRow")) return false;
@@ -5753,6 +6691,7 @@ var Grid = class {
5753
6691
  if (this.options.rowHeight == null) this.renderer.opts.rowHeight = autoRowHeight(effective);
5754
6692
  if (this.options.headerHeight == null) this.renderer.opts.headerHeight = autoHeaderHeight(effective);
5755
6693
  this.renderer.applyFont(size, family);
6694
+ this.renderer.invalidateHeights();
5756
6695
  this.menu.close();
5757
6696
  this.layoutChanged();
5758
6697
  }
@@ -5788,30 +6727,55 @@ var Grid = class {
5788
6727
  dataColumns() {
5789
6728
  return this.columns.displayed().filter((c) => !isSystemCol(c.id));
5790
6729
  }
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
- }
6730
+ validRange(range) {
6731
+ if (!range || this.options.rangeSelection === false) return null;
6732
+ const count = this.rowModel.getRowCount();
6733
+ const ok = (p) => !!p && !isSystemCol(p.colId) && !!this.columns.getColumn(p.colId) && p.rowIndex >= 0 && p.rowIndex < count;
6734
+ return ok(range.start) && ok(range.end) ? { start: { ...range.start }, end: { ...range.end } } : null;
6735
+ }
6736
+ /** Set the active range. Earlier Ctrl ranges are dropped unless `keepOthers`. */
6737
+ setRange(range, keepOthers = false) {
6738
+ const next = this.validRange(range);
6739
+ const extras = keepOthers || !this.store.state.extraRanges.length ? this.store.state.extraRanges : [];
5798
6740
  const cur = this.store.state.range;
5799
- if (JSON.stringify(cur) === JSON.stringify(next)) return;
5800
- this.store.set({ range: next });
6741
+ if (JSON.stringify(cur) === JSON.stringify(next) && extras === this.store.state.extraRanges) return;
6742
+ this.store.set({ range: next, extraRanges: extras });
5801
6743
  this.events.emit("rangeChange", { range: next && { start: { ...next.start }, end: { ...next.end } } });
5802
6744
  }
5803
- /** Range in displayed column indexes, or null. */
5804
- rangeBounds() {
5805
- const r = this.store.state.range;
6745
+ /** Ctrl+click / Ctrl+drag: keep what is selected (the focused cell when there is no range) and start a new range. */
6746
+ addRange(range) {
6747
+ const { range: active, focus } = this.store.state;
6748
+ const keep = this.validRange(active ?? (focus ? { start: focus, end: focus } : null));
6749
+ if (keep) this.store.set({ extraRanges: [...this.store.state.extraRanges, keep] });
6750
+ this.setRange(range, true);
6751
+ }
6752
+ getRanges() {
6753
+ const all2 = [...this.store.state.extraRanges];
6754
+ if (this.store.state.range) all2.push(this.store.state.range);
6755
+ return all2.map((r) => ({ start: { ...r.start }, end: { ...r.end } }));
6756
+ }
6757
+ setRanges(ranges) {
6758
+ const list = (Array.isArray(ranges) ? ranges : []).map((r) => this.validRange(r)).filter((r) => !!r);
6759
+ this.store.set({ extraRanges: list.slice(0, -1) });
6760
+ this.setRange(list[list.length - 1] ?? null, true);
6761
+ }
6762
+ boundsOf(r) {
5806
6763
  if (!r) return null;
5807
6764
  const a = this.columns.getColumn(r.start.colId), b = this.columns.getColumn(r.end.colId);
5808
6765
  if (!a || !b) return null;
5809
6766
  return normalizeRange({ start: { rowIndex: r.start.rowIndex, colIndex: a.index }, end: { rowIndex: r.end.rowIndex, colIndex: b.index } });
5810
6767
  }
6768
+ /** Active range in displayed column indexes, or null. */
6769
+ rangeBounds() {
6770
+ return this.boundsOf(this.store.state.range);
6771
+ }
6772
+ /** Every range (Ctrl ranges first, the active one last). */
6773
+ allRangeBounds() {
6774
+ return [...this.store.state.extraRanges, this.store.state.range].map((r) => this.boundsOf(r)).filter((b) => !!b);
6775
+ }
5811
6776
  rangeContains(rowIndex, colId) {
5812
- const b = this.rangeBounds();
5813
6777
  const col = this.columns.getColumn(colId);
5814
- return !!b && !!col && rangeContains(b, rowIndex, col.index);
6778
+ return !!col && this.allRangeBounds().some((b) => rangeContains(b, rowIndex, col.index));
5815
6779
  }
5816
6780
  selectAllCells() {
5817
6781
  const cols = this.dataColumns();
@@ -5829,6 +6793,18 @@ var Grid = class {
5829
6793
  const checkedRows = rows;
5830
6794
  let out;
5831
6795
  let cols;
6796
+ const all2 = rows ? [] : this.allRangeBounds();
6797
+ if (all2.length > 1) {
6798
+ const sameCols = all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2);
6799
+ const sameRows = all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2);
6800
+ if (sameCols || sameRows) {
6801
+ const parts = [...all2].sort((x, y) => sameCols ? x.r1 - y.r1 : x.c1 - y.c1);
6802
+ const blocks = parts.map((p) => this.rangeMatrix(p));
6803
+ const matrix2 = sameCols ? blocks.flat() : blocks[0].map((_, ri) => blocks.flatMap((bl) => bl[ri] ?? []));
6804
+ if (headers) matrix2.unshift((sameCols ? parts.slice(0, 1) : parts).flatMap((p) => this.rangeHeaders(p)));
6805
+ return toTSV(matrix2);
6806
+ }
6807
+ }
5832
6808
  if (b) {
5833
6809
  out = [];
5834
6810
  for (let i = b.r1; i <= b.r2; i++) {
@@ -5848,22 +6824,51 @@ var Grid = class {
5848
6824
  out = [node.data];
5849
6825
  cols = [col];
5850
6826
  }
5851
- const matrix = out.map((r) => cols.map((c) => copyValue(c.def, getCellValue(c.def, r), code)));
6827
+ const matrix = out.map((r) => cols.map((c) => this.copyCell(c.def, r, code)));
5852
6828
  if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
5853
6829
  return toTSV(matrix);
5854
6830
  }
6831
+ rangeHeaders(b) {
6832
+ return this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id)).map((c) => c.def.header ?? c.def.field ?? c.id);
6833
+ }
6834
+ /** Copy text of one range. */
6835
+ rangeMatrix(b) {
6836
+ const cols = this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
6837
+ const code = this.store.state.locale.code;
6838
+ const out = [];
6839
+ for (let i = b.r1; i <= b.r2; i++) {
6840
+ const node = this.rowModel.getRow(i);
6841
+ if (node && !node.kind) out.push(cols.map((c) => this.copyCell(c.def, node.data, code)));
6842
+ }
6843
+ return out;
6844
+ }
6845
+ copyCell(def, row, code) {
6846
+ const value = getCellValue(def, row);
6847
+ const rule = def.copyable;
6848
+ if (rule === false) return "";
6849
+ if (typeof rule === "function") {
6850
+ let ok = false;
6851
+ try {
6852
+ ok = rule({ value, row, column: def }) !== false;
6853
+ } catch {
6854
+ ok = false;
6855
+ }
6856
+ if (!ok) return "";
6857
+ }
6858
+ return copyValue(def, value, code);
6859
+ }
5855
6860
  copyToClipboard(headers, rows = false) {
5856
- const text = this.selectionTSV(headers, rows);
5857
- if (text && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
6861
+ const text2 = this.selectionTSV(headers, rows);
6862
+ if (text2 && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
5858
6863
  try {
5859
- navigator.clipboard.writeText(text)?.catch?.(() => void 0);
6864
+ navigator.clipboard.writeText(text2)?.catch?.(() => void 0);
5860
6865
  } catch {
5861
6866
  }
5862
6867
  }
5863
- return text;
6868
+ return text2;
5864
6869
  }
5865
- pasteText(text) {
5866
- const matrix = parseTSV(text);
6870
+ pasteText(text2) {
6871
+ const matrix = parseTSV(text2);
5867
6872
  const result = { changed: 0, skipped: 0 };
5868
6873
  if (!matrix.length) return result;
5869
6874
  const displayed = this.columns.displayed();
@@ -5895,7 +6900,7 @@ var Grid = class {
5895
6900
  const raw = single && b ? matrix[0][0] : matrix[dr]?.[dc];
5896
6901
  if (raw === void 0) continue;
5897
6902
  const def = col.def;
5898
- if (!def.editable || !def.field) {
6903
+ if (!def.field || !cellEditable(def, node.data, ri)) {
5899
6904
  result.skipped++;
5900
6905
  continue;
5901
6906
  }
@@ -5918,34 +6923,67 @@ var Grid = class {
5918
6923
  const spec = def.editor;
5919
6924
  if (spec && typeof spec === "object" && spec.type === "select") {
5920
6925
  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 };
6926
+ const text2 = raw.trim().toLowerCase();
6927
+ 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);
6928
+ if (!hit || hit.disabled) return { ok: false };
5924
6929
  value = hit.value;
6930
+ } else if (def.formulas && isFormula(raw.trim())) {
6931
+ value = raw.trim();
5925
6932
  } else {
5926
6933
  const parsed = parseValue(def, raw, oldValue);
5927
6934
  if (!parsed.ok) return parsed;
5928
6935
  value = parsed.value;
5929
6936
  }
5930
- if (def.required && isBlank(value)) return { ok: false };
6937
+ if (this.cellError(def, row, value) !== null) return { ok: false };
6938
+ return { ok: true, value };
6939
+ }
6940
+ /** Why `value` cannot go into this cell (required, unique, validate), or null when it can. */
6941
+ cellError(def, row, value) {
6942
+ const loc = this.store.state.locale;
6943
+ if (def.required && isBlank(value)) return loc.required;
6944
+ if (def.unique && def.field && !isBlank(value)) {
6945
+ const key = value instanceof Date ? value.getTime() : String(value);
6946
+ const field = def.field;
6947
+ if (this.store.state.data.some((r) => {
6948
+ if (r === row) return false;
6949
+ const v = r[field];
6950
+ return !isBlank(v) && (v instanceof Date ? v.getTime() : String(v)) === key;
6951
+ })) return loc.duplicate;
6952
+ }
5931
6953
  if (def.validate) {
6954
+ let res;
5932
6955
  try {
5933
- if (def.validate(value, row) !== true) return { ok: false };
6956
+ res = def.validate(value, row);
5934
6957
  } catch {
5935
- return { ok: false };
6958
+ res = loc.invalidValue;
5936
6959
  }
6960
+ if (res !== true) return typeof res === "string" && res ? res : loc.invalidValue;
5937
6961
  }
5938
- return { ok: true, value };
6962
+ return null;
6963
+ }
6964
+ validateAll() {
6965
+ const out = [];
6966
+ 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));
6967
+ this.store.state.data.forEach((row, i) => {
6968
+ const id = this.rowModel.idOf(row);
6969
+ const rowIndex = this.rowModel.indexOfId(id);
6970
+ for (const c of cols) {
6971
+ if (!cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
6972
+ const message = this.cellError(c.def, row, row[c.def.field]);
6973
+ if (message !== null) out.push({ row, rowIndex, colId: c.id, message });
6974
+ }
6975
+ });
6976
+ return out;
5939
6977
  }
5940
6978
  // ── merge, aggregates, fill, pinned rows ────────────────────
5941
6979
  /** Merge blocks for every `merge` column over the displayed rows (memoized). */
5942
6980
  mergeSpans() {
5943
- if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge)) return NO_SPANS;
6981
+ if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan)) return NO_SPANS;
5944
6982
  const rows = this.rowModel.rows();
5945
6983
  const ver = this.editVersion * 1e6 + this.columns.displayed().length;
5946
6984
  if (this.spanCache && this.spanCache.rows === rows && this.spanCache.ver === ver) return this.spanCache.spans;
5947
6985
  const spans = /* @__PURE__ */ new Map();
5948
- const merged = this.columns.displayed().filter((c) => c.def.merge);
6986
+ const merged = this.columns.displayed().filter((c) => c.def.merge || c.def.rowSpan);
5949
6987
  if (merged.length && !rows.some((n) => n.kind)) {
5950
6988
  const data = rows.map((n) => n.data);
5951
6989
  for (const c of merged) spans.set(c.id, computeSpans(data, c.def));
@@ -5955,15 +6993,22 @@ var Grid = class {
5955
6993
  }
5956
6994
  /** Count / sum / min / max over the selected range (numbers only for the math). */
5957
6995
  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));
6996
+ const all2 = this.allRangeBounds();
6997
+ if (!all2.length) return null;
6998
+ const displayed = this.columns.displayed();
5961
6999
  const out = { cells: 0, count: 0, numbers: 0, sum: 0, min: Infinity, max: -Infinity };
5962
- for (let r = b.r1; r <= b.r2; r++) {
7000
+ const seen2 = all2.length > 1 ? /* @__PURE__ */ new Set() : null;
7001
+ for (const b of all2) for (let r = b.r1; r <= b.r2; r++) {
5963
7002
  const node = this.rowModel.getRow(r);
5964
7003
  if (!node || node.kind) continue;
5965
7004
  const row = node.data;
5966
- for (const c of cols) {
7005
+ for (const c of displayed.slice(b.c1, b.c2 + 1)) {
7006
+ if (isSystemCol(c.id)) continue;
7007
+ if (seen2) {
7008
+ const k = `${r},${c.index}`;
7009
+ if (seen2.has(k)) continue;
7010
+ seen2.add(k);
7011
+ }
5967
7012
  out.cells++;
5968
7013
  const v = getCellValue(c.def, row);
5969
7014
  if (isBlank(v)) continue;
@@ -5998,13 +7043,8 @@ var Grid = class {
5998
7043
  const col = displayed[c];
5999
7044
  const node = this.rowModel.getRow(r);
6000
7045
  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
- }
7046
+ if (!node || node.kind || !def?.field || isSystemCol(col.id) || !cellEditable(def, node.data, r)) return;
7047
+ if (this.cellError(def, node.data, value2) !== null) return;
6008
7048
  this.writeCell(changes, node.data, def.field, col.id, r, value2);
6009
7049
  };
6010
7050
  const value = (r, c) => getCellValue(displayed[c].def, this.rowModel.getRow(r).data);
@@ -6038,6 +7078,7 @@ var Grid = class {
6038
7078
  writeCell(changes, row, field, colId, rowIndex, value) {
6039
7079
  const oldValue = row[field];
6040
7080
  if (Object.is(oldValue, value)) return false;
7081
+ if (this.veto("beforeCellChange", { row, rowIndex, colId, oldValue, newValue: value })) return false;
6041
7082
  this.tracker.beforeChange(row);
6042
7083
  row[field] = value;
6043
7084
  changes.push({ row, field, colId, oldValue, newValue: value });
@@ -6059,6 +7100,7 @@ var Grid = class {
6059
7100
  this.afterDataEdit();
6060
7101
  }
6061
7102
  afterDataEdit() {
7103
+ this.renderer.invalidateHeights();
6062
7104
  this.editVersion++;
6063
7105
  this.refreshSummary();
6064
7106
  this.renderer.invalidate({ rows: true });
@@ -6074,17 +7116,17 @@ var Grid = class {
6074
7116
  /** Delete/Backspace: clear editable cells in the range, or the focused cell. */
6075
7117
  clearCells() {
6076
7118
  const displayed = this.columns.displayed();
6077
- const b = this.rangeBounds();
7119
+ const all2 = this.allRangeBounds();
6078
7120
  const f = this.store.state.focus;
6079
7121
  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;
7122
+ const boxes = all2.length ? all2 : f && fi !== void 0 ? [{ r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi }] : [];
7123
+ if (!boxes.length) return;
6082
7124
  const changes = [];
6083
- for (let r = box.r1; r <= box.r2; r++) {
7125
+ for (const box of boxes) for (let r = box.r1; r <= box.r2; r++) {
6084
7126
  const node = this.rowModel.getRow(r);
6085
7127
  for (let c = box.c1; c <= box.c2; c++) {
6086
7128
  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);
7129
+ if (node && !node.kind && def?.field && cellEditable(def, node.data, r)) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
6088
7130
  }
6089
7131
  }
6090
7132
  this.endCellTx(changes);
@@ -6264,6 +7306,7 @@ var Grid = class {
6264
7306
  }
6265
7307
  return s.dir === "asc" || s.dir === "desc";
6266
7308
  });
7309
+ if (this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) })) return;
6267
7310
  this.store.set({ sort: valid });
6268
7311
  this.events.emit("sortChange", { sort: [...valid] });
6269
7312
  }
@@ -6285,6 +7328,7 @@ var Grid = class {
6285
7328
  warn(`setFilter: unknown column "${colId}"`);
6286
7329
  return;
6287
7330
  }
7331
+ if (this.veto("beforeFilter", { colId, model })) return;
6288
7332
  const filters = { ...this.store.state.filters };
6289
7333
  if (model) filters[colId] = model;
6290
7334
  else delete filters[colId];
@@ -6305,9 +7349,21 @@ var Grid = class {
6305
7349
  this.store.set({ selected, anchorId });
6306
7350
  this.events.emit("selectionChange", { selected: this.selectedRows() });
6307
7351
  }
7352
+ /** Data rows that may be checked (isRowSelectable). */
7353
+ rowSelectable(row) {
7354
+ const f = this.options.isRowSelectable;
7355
+ if (!f) return true;
7356
+ try {
7357
+ return !!f(row);
7358
+ } catch {
7359
+ return false;
7360
+ }
7361
+ }
6308
7362
  selectFromClick(index, toggle, range) {
6309
7363
  const mode = this.options.rowSelection;
6310
- if (!mode || index < 0 || index >= this.rowModel.getRowCount() || this.rowModel.getRow(index).kind) return;
7364
+ if (!mode || index < 0 || index >= this.rowModel.getRowCount()) return;
7365
+ const target = this.rowModel.getRow(index);
7366
+ if (target.kind || !this.rowSelectable(target.data)) return;
6311
7367
  const r = nextSelection({
6312
7368
  selected: this.store.state.selected,
6313
7369
  anchorIndex: this.anchorIndex(),
@@ -6317,11 +7373,12 @@ var Grid = class {
6317
7373
  toggle,
6318
7374
  range
6319
7375
  });
6320
- if (this.rowModel.rows() !== this.rowModel.dataRows()) {
7376
+ if (this.rowModel.rows() !== this.rowModel.dataRows() || this.options.isRowSelectable) {
6321
7377
  for (const id of r.selected) {
6322
7378
  if (this.store.state.selected.has(id)) continue;
6323
7379
  const i = this.rowModel.indexOfId(id);
6324
- if (i >= 0 && this.rowModel.getRow(i).kind) r.selected.delete(id);
7380
+ const n = i >= 0 ? this.rowModel.getRow(i) : null;
7381
+ if (n && (n.kind || !this.rowSelectable(n.data))) r.selected.delete(id);
6325
7382
  }
6326
7383
  }
6327
7384
  this.setSelected(r.selected, this.rowModel.getRow(r.anchorIndex)?.id ?? null);
@@ -6333,8 +7390,13 @@ var Grid = class {
6333
7390
  const i = this.rowModel.indexOfId(id);
6334
7391
  return i < 0 ? null : i;
6335
7392
  }
6336
- toggleSelectAll() {
7393
+ /** Rows select-all acts on: displayed data rows that are selectable. */
7394
+ selectableRows() {
6337
7395
  const rows = this.rowModel.dataRows();
7396
+ return this.options.isRowSelectable ? rows.filter((n) => this.rowSelectable(n.data)) : rows;
7397
+ }
7398
+ toggleSelectAll() {
7399
+ const rows = this.selectableRows();
6338
7400
  const sel = this.store.state.selected;
6339
7401
  const all2 = rows.length > 0 && rows.every((n) => sel.has(n.id));
6340
7402
  const next = new Set(sel);
@@ -6354,7 +7416,7 @@ var Grid = class {
6354
7416
  if (this.options.rowSelection) sys.push(SELECT_COLUMN);
6355
7417
  return [...sys, ...defs];
6356
7418
  }
6357
- setData(rows) {
7419
+ setData(rows, options) {
6358
7420
  if (this.clientOnly("setData")) return;
6359
7421
  if (!Array.isArray(rows)) {
6360
7422
  warn("setData: rows must be an array");
@@ -6364,8 +7426,11 @@ var Grid = class {
6364
7426
  this.tracker.reset();
6365
7427
  this.history.clear();
6366
7428
  this.detailOpen.clear();
7429
+ this.treeLoaded.clear();
7430
+ this.treeLoading.clear();
6367
7431
  this.renderer.dropDetail();
6368
7432
  this.store.set({ data: cleanRows(rows) });
7433
+ if (options?.scrollToTop) this.renderer.setScroll({ top: 0, left: 0 });
6369
7434
  const sel = this.store.state.selected;
6370
7435
  if (sel.size) {
6371
7436
  if (this.options.getRowId) {
@@ -6439,7 +7504,7 @@ var Grid = class {
6439
7504
  return fn(...a);
6440
7505
  });
6441
7506
  return {
6442
- setData: guard((rows) => this.setData(rows)),
7507
+ setData: guard((rows, o) => this.setData(rows, o)),
6443
7508
  getData: guard(() => this.store.state.data),
6444
7509
  setColumns: guard((defs) => this.setColumns(defs)),
6445
7510
  getColumnState: guard(() => this.columns.getState()),
@@ -6453,10 +7518,14 @@ var Grid = class {
6453
7518
  getFilterModel: guard(() => ({ ...this.store.state.filters })),
6454
7519
  setQuickFilter: guard((t) => this.store.set({ quickFilter: t ?? "" })),
6455
7520
  getSelectedRows: guard(() => this.selectedRows()),
6456
- selectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set([...this.store.state.selected, ...this.rowModel.dataRows().map((n) => n.id)]))),
7521
+ selectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set([...this.store.state.selected, ...this.selectableRows().map((n) => n.id)]))),
6457
7522
  deselectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set(), null)),
6458
7523
  getDisplayedRowCount: guard(() => this.rowModel.getRowCount()),
6459
7524
  scrollToRow: guard((i) => this.renderer.scrollToRow(i)),
7525
+ validate: guard(() => this.validateAll()),
7526
+ getScrollPosition: guard(() => this.renderer.getScroll()),
7527
+ setScrollPosition: guard((p) => this.renderer.setScroll(p ?? {})),
7528
+ syncScroll: guard((other, axis = "both") => this.syncScroll(other, axis)),
6460
7529
  setTheme: guard((t) => this.store.set({ theme: t })),
6461
7530
  setLocale: guard((l) => this.store.set({ locale: resolveLocale(l) })),
6462
7531
  setFont: guard((f) => this.setFont(f)),
@@ -6473,8 +7542,10 @@ var Grid = class {
6473
7542
  return r && { start: { ...r.start }, end: { ...r.end } };
6474
7543
  }),
6475
7544
  setRange: guard((r) => this.setRange(r)),
7545
+ getRanges: guard(() => this.getRanges()),
7546
+ setRanges: guard((list) => this.setRanges(list)),
6476
7547
  copySelection: guard((o) => this.copyToClipboard(!!o?.headers, !!o?.rows)),
6477
- pasteText: guard((text) => this.pasteText(text)),
7548
+ pasteText: guard((text2) => this.pasteText(text2)),
6478
7549
  startEdit: guard((rowIndex, colId) => this.editor.start(rowIndex, colId)),
6479
7550
  stopEdit: guard((cancel) => cancel ? this.editor.cancel() : void this.editor.commit(null)),
6480
7551
  addRows: guard((rows, index) => this.addRows(rows, index)),
@@ -6491,7 +7562,7 @@ var Grid = class {
6491
7562
  canRedo: guard(() => this.history.canRedo()),
6492
7563
  fillRange: guard((t) => this.fillRange(t)),
6493
7564
  setPinnedRows: guard((r) => this.setPinnedRows(r)),
6494
- find: guard((text) => this.finder.find(text)),
7565
+ find: guard((text2) => this.finder.find(text2)),
6495
7566
  findNext: guard(() => this.finder.next()),
6496
7567
  findPrevious: guard(() => this.finder.previous()),
6497
7568
  clearFind: guard(() => this.finder.clear()),
@@ -6503,6 +7574,7 @@ var Grid = class {
6503
7574
  setDetailExpanded: guard((row, open) => this.setDetailExpanded(row, open)),
6504
7575
  isDetailExpanded: guard((row) => this.isDetailExpanded(row)),
6505
7576
  moveRow: guard((row, toIndex) => this.moveRow(row, toIndex)),
7577
+ dropRow: guard((row, target, position) => this.dropRow(row, target, position)),
6506
7578
  setPage: guard((p) => this.setPage(p)),
6507
7579
  getPage: guard(() => this.getPage()),
6508
7580
  getPageCount: guard(() => this.getPageCount()),
@@ -6524,6 +7596,7 @@ var Grid = class {
6524
7596
  destroy() {
6525
7597
  if (this.destroyed) return;
6526
7598
  this.destroyed = true;
7599
+ liveGrids.delete(this.renderer.root);
6527
7600
  for (const d of this.disposers) d();
6528
7601
  this.renderer.destroy();
6529
7602
  this.events.clear();