@freegridjs/core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -50,6 +50,15 @@ function filterTypeOf(def) {
50
50
  return "text";
51
51
  }
52
52
  }
53
+ function cellEditable(def, row, rowIndex) {
54
+ const e = def.editable;
55
+ if (typeof e !== "function") return !!e;
56
+ try {
57
+ return !!e({ value: def.valueGetter ? def.valueGetter(row) : def.field ? row[def.field] : void 0, row, rowIndex, column: def });
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
53
62
  var isSortable = (def) => def.id !== SELECT_COL_ID && def.sortable !== false;
54
63
  var isResizable = (def) => def.id !== SELECT_COL_ID && def.resizable !== false;
55
64
  function clampWidth(def, w) {
@@ -387,8 +396,8 @@ function quickTokens(q) {
387
396
  const t = q.trim().toLowerCase();
388
397
  return t ? t.split(/\s+/) : [];
389
398
  }
390
- function matchesQuick(tokens, text) {
391
- return tokens.every((t) => text.includes(t));
399
+ function matchesQuick(tokens, text2) {
400
+ return tokens.every((t) => text2.includes(t));
392
401
  }
393
402
 
394
403
  // src/i18n/format.ts
@@ -445,13 +454,13 @@ function create(type, format, locale) {
445
454
  }
446
455
  case "date": {
447
456
  if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
448
- const pad2 = (n) => String(n).padStart(2, "0");
457
+ const pad3 = (n) => String(n).padStart(2, "0");
449
458
  const withTime = format === "datetime";
450
459
  return (v) => {
451
460
  const d = toDate(v);
452
461
  if (!d) return isBlank(v) ? "" : String(v);
453
- const day = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
454
- return withTime ? `${day} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` : day;
462
+ const day = `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}`;
463
+ return withTime ? `${day} ${pad3(d.getHours())}:${pad3(d.getMinutes())}` : day;
455
464
  };
456
465
  }
457
466
  let opts = DATE_PRESETS.date;
@@ -482,6 +491,216 @@ function getFormatter(def, locale) {
482
491
  return fmt;
483
492
  }
484
493
 
494
+ // src/formula.ts
495
+ var FormulaError = class extends Error {
496
+ constructor(code) {
497
+ super(code);
498
+ this.code = code;
499
+ }
500
+ };
501
+ function tokenize(src) {
502
+ const out = [];
503
+ let i = 0;
504
+ while (i < src.length) {
505
+ const c = src[i];
506
+ if (/\s/.test(c)) {
507
+ i++;
508
+ } else if (/[0-9.]/.test(c)) {
509
+ const m = /^(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/i.exec(src.slice(i));
510
+ if (!m) throw new FormulaError("#ERROR!");
511
+ out.push({ t: "num", v: Number(m[0]) });
512
+ i += m[0].length;
513
+ } else if (c === '"') {
514
+ let s = "";
515
+ i++;
516
+ for (; ; ) {
517
+ if (i >= src.length) throw new FormulaError("#ERROR!");
518
+ if (src[i] === '"') {
519
+ if (src[i + 1] === '"') {
520
+ s += '"';
521
+ i += 2;
522
+ continue;
523
+ }
524
+ i++;
525
+ break;
526
+ }
527
+ s += src[i++];
528
+ }
529
+ out.push({ t: "str", v: s });
530
+ } else if (c === "[") {
531
+ const end = src.indexOf("]", i);
532
+ if (end < 0) throw new FormulaError("#ERROR!");
533
+ out.push({ t: "id", v: src.slice(i + 1, end) });
534
+ i = end + 1;
535
+ } else if (/[A-Za-z_À-￿]/.test(c)) {
536
+ const m = /^[A-Za-z_À-￿][\wÀ-￿]*/.exec(src.slice(i));
537
+ out.push({ t: "id", v: m[0] });
538
+ i += m[0].length;
539
+ } else {
540
+ const two = src.slice(i, i + 2);
541
+ if (two === "<>" || two === "<=" || two === ">=") {
542
+ out.push({ t: "op", v: two });
543
+ i += 2;
544
+ } else if ("+-*/%^&=<>(),".includes(c)) {
545
+ out.push({ t: "op", v: c });
546
+ i++;
547
+ } else throw new FormulaError("#ERROR!");
548
+ }
549
+ }
550
+ return out;
551
+ }
552
+ var isBlankValue = (v) => v === null || v === void 0 || v === "";
553
+ function num(v) {
554
+ if (typeof v === "number") return v;
555
+ if (typeof v === "boolean") return v ? 1 : 0;
556
+ if (isBlankValue(v)) return 0;
557
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
558
+ throw new FormulaError("#VALUE!");
559
+ }
560
+ var text = (v) => isBlankValue(v) ? "" : typeof v === "boolean" ? v ? "TRUE" : "FALSE" : String(v);
561
+ function round(x, digits) {
562
+ const d = Math.trunc(digits);
563
+ const r = Number(`${Math.round(Number(`${Math.abs(x)}e${d}`))}e${-d}`);
564
+ return x < 0 ? -r : r;
565
+ }
566
+ var FUNCS = {
567
+ ROUND: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0),
568
+ ABS: (a) => Math.abs(num(a[0])),
569
+ MIN: (a) => a.length ? Math.min(...a.map(num)) : 0,
570
+ MAX: (a) => a.length ? Math.max(...a.map(num)) : 0,
571
+ SUM: (a) => a.reduce((s, v) => s + num(v), 0),
572
+ AVG: (a) => {
573
+ if (!a.length) throw new FormulaError("#DIV/0!");
574
+ return a.reduce((s, v) => s + num(v), 0) / a.length;
575
+ }
576
+ };
577
+ function compare(a, b) {
578
+ const bothNum = (typeof a === "number" || isBlankValue(a)) && (typeof b === "number" || isBlankValue(b));
579
+ if (bothNum) return num(a) - num(b);
580
+ return text(a).localeCompare(text(b), void 0, { sensitivity: "accent" });
581
+ }
582
+ var Parser = class {
583
+ constructor(tokens, resolve) {
584
+ this.tokens = tokens;
585
+ this.resolve = resolve;
586
+ this.i = 0;
587
+ }
588
+ parse() {
589
+ const v = this.comparison();
590
+ if (this.i < this.tokens.length) throw new FormulaError("#ERROR!");
591
+ return v;
592
+ }
593
+ peek(v) {
594
+ const t = this.tokens[this.i];
595
+ return !!t && t.t === "op" && (v === void 0 || t.v === v);
596
+ }
597
+ take(v) {
598
+ if (!this.peek(v)) return false;
599
+ this.i++;
600
+ return true;
601
+ }
602
+ comparison() {
603
+ const a = this.concat();
604
+ for (const op of ["=", "<>", "<=", ">=", "<", ">"]) {
605
+ if (this.take(op)) {
606
+ const c = compare(a, this.concat());
607
+ return op === "=" ? c === 0 : op === "<>" ? c !== 0 : op === "<" ? c < 0 : op === ">" ? c > 0 : op === "<=" ? c <= 0 : c >= 0;
608
+ }
609
+ }
610
+ return a;
611
+ }
612
+ concat() {
613
+ let v = this.additive();
614
+ while (this.take("&")) v = text(v) + text(this.additive());
615
+ return v;
616
+ }
617
+ additive() {
618
+ let v = this.multiplicative();
619
+ for (; ; ) {
620
+ if (this.take("+")) v = num(v) + num(this.multiplicative());
621
+ else if (this.take("-")) v = num(v) - num(this.multiplicative());
622
+ else return v;
623
+ }
624
+ }
625
+ multiplicative() {
626
+ let v = this.unary();
627
+ for (; ; ) {
628
+ if (this.take("*")) v = num(v) * num(this.unary());
629
+ else if (this.take("/") || this.take("%")) {
630
+ const op = this.tokens[this.i - 1].v;
631
+ const d = num(this.unary());
632
+ if (d === 0) throw new FormulaError("#DIV/0!");
633
+ v = op === "/" ? num(v) / d : num(v) % d;
634
+ } else return v;
635
+ }
636
+ }
637
+ unary() {
638
+ if (this.take("-")) return -num(this.unary());
639
+ if (this.take("+")) return num(this.unary());
640
+ return this.power();
641
+ }
642
+ power() {
643
+ const base = this.primary();
644
+ if (this.take("^")) return num(base) ** num(this.unary());
645
+ return base;
646
+ }
647
+ primary() {
648
+ const t = this.tokens[this.i++];
649
+ if (!t) throw new FormulaError("#ERROR!");
650
+ if (t.t === "num" || t.t === "str") return t.v;
651
+ if (t.t === "op") {
652
+ if (t.v !== "(") throw new FormulaError("#ERROR!");
653
+ const v = this.comparison();
654
+ if (!this.take(")")) throw new FormulaError("#ERROR!");
655
+ return v;
656
+ }
657
+ const name = t.v;
658
+ if (this.peek("(")) {
659
+ this.i++;
660
+ const args = [];
661
+ if (!this.take(")")) {
662
+ do
663
+ args.push(this.comparison());
664
+ while (this.take(","));
665
+ if (!this.take(")")) throw new FormulaError("#ERROR!");
666
+ }
667
+ const upper = name.toUpperCase();
668
+ if (upper === "IF") {
669
+ if (args.length < 2) throw new FormulaError("#ERROR!");
670
+ const cond = typeof args[0] === "boolean" ? args[0] : num(args[0]) !== 0;
671
+ return cond ? args[1] : args.length > 2 ? args[2] : false;
672
+ }
673
+ const fn = Object.hasOwn(FUNCS, upper) ? FUNCS[upper] : void 0;
674
+ if (!fn) throw new FormulaError("#NAME?");
675
+ return fn(args);
676
+ }
677
+ if (/^(true|false)$/i.test(name)) return name.toLowerCase() === "true";
678
+ return this.resolve(name);
679
+ }
680
+ };
681
+ var MAX_DEPTH = 32;
682
+ function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0) {
683
+ try {
684
+ const resolve = (name) => {
685
+ if (!row || !Object.hasOwn(row, name)) throw new FormulaError("#REF!");
686
+ const v2 = row[name];
687
+ if (typeof v2 === "string" && v2.startsWith("=")) {
688
+ if (seen2.has(name) || depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
689
+ const r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1);
690
+ if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
691
+ return r;
692
+ }
693
+ return v2;
694
+ };
695
+ const v = new Parser(tokenize(formula.slice(1)), resolve).parse();
696
+ if (typeof v === "number" && !Number.isFinite(v)) return "#NUM!";
697
+ return v;
698
+ } catch (e) {
699
+ return e instanceof FormulaError ? e.code : "#ERROR!";
700
+ }
701
+ }
702
+ var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
703
+
485
704
  // src/rows/values.ts
486
705
  function getCellValue(def, row) {
487
706
  if (def.valueGetter) {
@@ -492,7 +711,9 @@ function getCellValue(def, row) {
492
711
  return void 0;
493
712
  }
494
713
  }
495
- return def.field != null ? row[def.field] : void 0;
714
+ if (def.field == null) return void 0;
715
+ const raw = row[def.field];
716
+ return def.formulas && isFormula(raw) ? evaluateFormula(raw, row, /* @__PURE__ */ new Set([def.field])) : raw;
496
717
  }
497
718
 
498
719
  // src/summary.ts
@@ -553,6 +774,57 @@ function computeSummary(defs, rows, columnDef) {
553
774
  });
554
775
  }
555
776
 
777
+ // src/merge.ts
778
+ function spanOf(fn, def, row, rowIndex, max) {
779
+ if (!fn) return 1;
780
+ let n = 1;
781
+ try {
782
+ n = Math.floor(Number(fn({ value: getCellValue(def, row), row, rowIndex })));
783
+ } catch {
784
+ n = 1;
785
+ }
786
+ return Number.isFinite(n) ? Math.max(1, Math.min(max, n)) : 1;
787
+ }
788
+ function computeSpans(rows, def) {
789
+ const n = rows.length;
790
+ const start = new Int32Array(n);
791
+ const end = new Int32Array(n);
792
+ if (def.rowSpan) {
793
+ for (let i = 0; i < n; ) {
794
+ const len = spanOf(def.rowSpan, def, rows[i], i, n - i);
795
+ for (let k = i; k < i + len; k++) {
796
+ start[k] = i;
797
+ end[k] = i + len - 1;
798
+ }
799
+ i += len;
800
+ }
801
+ return { start, end };
802
+ }
803
+ const same2 = typeof def.merge === "function" ? (a, b) => {
804
+ try {
805
+ return !!def.merge(getCellValue(def, rows[a]), getCellValue(def, rows[b]), rows[a], rows[b]);
806
+ } catch {
807
+ return false;
808
+ }
809
+ } : (() => {
810
+ const keys = rows.map((r) => {
811
+ const v = getCellValue(def, r);
812
+ return isBlank(v) ? null : setKey(v);
813
+ });
814
+ return (a, b) => keys[a] !== null && keys[a] === keys[b];
815
+ })();
816
+ let s = 0;
817
+ for (let i = 1; i <= n; i++) {
818
+ if (i < n && same2(i - 1, i)) continue;
819
+ for (let k = s; k < i; k++) {
820
+ start[k] = s;
821
+ end[k] = i - 1;
822
+ }
823
+ s = i;
824
+ }
825
+ return { start, end };
826
+ }
827
+
556
828
  // src/viewport.ts
557
829
  function rowRange(scrollTop, height, rowHeight, count, overscan = 4) {
558
830
  if (count <= 0 || rowHeight <= 0) return { first: 0, last: -1 };
@@ -601,6 +873,7 @@ function rowRangeByOffsets(offsets, scrollTop, height, count, overscan = 4) {
601
873
  // src/render/renderer.ts
602
874
  var REGIONS2 = ["left", "center", "right"];
603
875
  var ROW_OVERSCAN = 6;
876
+ var MAX_COLSPAN_REACH = 8;
604
877
  var COL_OVERSCAN = 2;
605
878
  var MENU_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M3 4h10M3 8h10M3 12h10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
606
879
  var CHEVRON = '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M6 3.5 10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
@@ -615,6 +888,8 @@ var Renderer = class {
615
888
  constructor(host, grid, opts) {
616
889
  this.grid = grid;
617
890
  this.opts = opts;
891
+ this.activeCellId = "";
892
+ this.idBase = `fg${Math.random().toString(36).slice(2, 8)}`;
618
893
  this.painted = { left: /* @__PURE__ */ new Map(), center: /* @__PURE__ */ new Map(), right: /* @__PURE__ */ new Map() };
619
894
  this.pool = { left: [], center: [], right: [] };
620
895
  this.frame = 0;
@@ -634,6 +909,9 @@ var Renderer = class {
634
909
  this.statusEl = null;
635
910
  this.statusKey = "";
636
911
  this.pagerEl = null;
912
+ /** Card view (narrow screens): scroller and canvas of absolutely placed cards. */
913
+ this.cards = null;
914
+ this.cardMode = false;
637
915
  this.pagerKey = "";
638
916
  /** Prefix-sum row tops when rows differ in height (detail rows); null when every row is rowHeight. */
639
917
  this.offsets = null;
@@ -641,19 +919,42 @@ var Renderer = class {
641
919
  this.details = /* @__PURE__ */ new Map();
642
920
  /** First non-system displayed column: holds group labels, tree chevrons and detail toggles. */
643
921
  this.firstDataId = "";
922
+ /** Measured heights per row id (autoRowHeight); cleared when content or widths change. */
923
+ this.rowHeights = /* @__PURE__ */ new Map();
924
+ this.heightsVersion = 0;
644
925
  /** Column holding group labels: the first scrolling data column, so long labels have room. */
645
926
  this.groupLabelId = "";
646
927
  this.onScroll = () => {
647
928
  if (this.frame) cancelAnimationFrame(this.frame);
648
929
  this.frame = 0;
649
930
  this.render();
931
+ this.grid.events.emit("scroll", this.getScroll());
650
932
  };
651
933
  this.onSideWheel = (e) => {
652
934
  if (!e.deltaY) return;
653
935
  e.preventDefault();
654
936
  this.scroller.scrollTop += e.deltaY;
655
937
  };
656
- this.root = el("div", "", { role: "grid", tabindex: "0" });
938
+ this.onCardClick = (e) => {
939
+ const target = e.target;
940
+ const card = target.closest(".fg-card");
941
+ if (!card) return;
942
+ const i = Number(card.dataset.row);
943
+ const node = this.grid.rowModel.getRow(i);
944
+ if (!node || node.kind) return;
945
+ if (target.closest(".fg-card-check")) {
946
+ this.grid.selectFromClick(i, true, false);
947
+ this.invalidate({ rows: true });
948
+ return;
949
+ }
950
+ const colId = target.closest(".fg-card-value")?.dataset.col;
951
+ const col = colId ? this.grid.columns.getColumn(colId) : void 0;
952
+ if (!col) return;
953
+ this.grid.events.emit("cellClick", { row: node.data, rowIndex: i, colId: col.id, value: getCellValue(col.def, node.data), event: e });
954
+ };
955
+ this.root = el("div", "", { tabindex: "-1" });
956
+ this.root.addEventListener("focus", () => this.gridEl.focus({ preventScroll: true }));
957
+ this.gridEl = el("div", "fg-grid", { role: "grid", tabindex: "0" });
657
958
  this.headerEl = el("div", "fg-header", { role: "rowgroup" });
658
959
  this.bodyEl = el("div", "fg-body", { role: "rowgroup" });
659
960
  const panes = (cls) => ({
@@ -663,6 +964,7 @@ var Renderer = class {
663
964
  });
664
965
  this.headerPanes = panes("fg-header-pane");
665
966
  this.headerInners = panes("fg-header-inner");
967
+ for (const r of REGIONS2) this.headerInners[r].setAttribute("role", "row");
666
968
  this.bodyPanes = panes("fg-body-pane");
667
969
  this.canvases = panes("fg-canvas");
668
970
  for (const r of REGIONS2) {
@@ -674,9 +976,10 @@ var Renderer = class {
674
976
  this.bodyEl.append(this.bodyPanes[r]);
675
977
  }
676
978
  this.scroller = this.bodyPanes.center;
677
- this.overlay = el("div", "fg-overlay", { role: "status" });
979
+ this.overlay = el("div", "fg-overlay", { "aria-live": "polite" });
678
980
  this.main = el("div", "fg-main");
679
- this.main.append(this.headerEl, this.bodyEl, this.overlay);
981
+ this.gridEl.append(this.headerEl, this.bodyEl);
982
+ this.main.append(this.gridEl, this.overlay);
680
983
  this.root.append(this.main);
681
984
  host.append(this.root);
682
985
  this.applyTheme();
@@ -684,6 +987,19 @@ var Renderer = class {
684
987
  this.bodyPanes.left.addEventListener("wheel", this.onSideWheel, { passive: false });
685
988
  this.bodyPanes.right.addEventListener("wheel", this.onSideWheel, { passive: false });
686
989
  }
990
+ /** The element that scrolls the rows: the card list when cards are shown. */
991
+ get activeScroller() {
992
+ return this.cards?.scroller ?? this.scroller;
993
+ }
994
+ getScroll() {
995
+ const s = this.activeScroller;
996
+ return { top: s.scrollTop, left: s.scrollLeft };
997
+ }
998
+ setScroll(p) {
999
+ const s = this.activeScroller;
1000
+ if (typeof p?.top === "number" && Number.isFinite(p.top)) s.scrollTop = Math.max(0, p.top);
1001
+ if (typeof p?.left === "number" && Number.isFinite(p.left)) s.scrollLeft = Math.max(0, p.left);
1002
+ }
687
1003
  applyFont(size, family) {
688
1004
  if (size) this.root.style.setProperty("--fg-font-size", `${size}px`);
689
1005
  else this.root.style.removeProperty("--fg-font-size");
@@ -691,7 +1007,13 @@ var Renderer = class {
691
1007
  else this.root.style.removeProperty("--fg-font");
692
1008
  }
693
1009
  applyTheme() {
694
- this.root.className = `fg-root fg-theme-${this.grid.store.state.theme}`;
1010
+ const rtl = this.grid.options.direction === "rtl";
1011
+ this.root.className = `fg-root fg-theme-${this.grid.store.state.theme}${rtl ? " fg-rtl" : ""}${this.cardMode ? " fg-card-mode" : ""}`;
1012
+ this.root.setAttribute("dir", rtl ? "rtl" : "ltr");
1013
+ }
1014
+ /** Right-to-left layout: the main area is mirrored, so screen deltas along x flip sign. */
1015
+ get rtl() {
1016
+ return this.grid.options.direction === "rtl";
695
1017
  }
696
1018
  invalidate(what = {}) {
697
1019
  if (what.header) this.headerDirty = true;
@@ -714,10 +1036,20 @@ var Renderer = class {
714
1036
  this.version++;
715
1037
  }
716
1038
  const count = rowModel.getRowCount();
717
- const lay = rowModel.layout(rh, this.detailHeight());
1039
+ const auto = !!this.grid.options.autoRowHeight && !rowModel.server;
1040
+ const lay = rowModel.layout(rh, this.detailHeight(), auto ? this.rowHeights : void 0, this.heightsVersion);
718
1041
  this.offsets = lay.uniform ? null : lay.offsets;
719
1042
  this.firstDataId = columns.displayed().find((c) => !isSystemCol(c.id))?.id ?? "";
720
1043
  this.groupLabelId = columns.region("center").find((c) => !isSystemCol(c.id))?.id ?? this.firstDataId;
1044
+ if (this.useCards()) {
1045
+ this.renderCards(count);
1046
+ this.renderStatusBar(rowModel.dataCount());
1047
+ this.renderPager();
1048
+ this.orderSections(null);
1049
+ this.overlay.hidden = count > 0;
1050
+ this.overlay.textContent = count === 0 && rowModel.loading() ? store.state.locale.loading : store.state.locale.noRows;
1051
+ return;
1052
+ }
721
1053
  const summary = this.grid.summary();
722
1054
  const sumRows = summary ? summary.opts.rows.length : 0;
723
1055
  const sumPos = summary?.opts.position ?? "bottom";
@@ -738,14 +1070,18 @@ var Renderer = class {
738
1070
  }
739
1071
  const hScrollbar = this.scroller.offsetHeight - this.scroller.clientHeight;
740
1072
  this.bodyPanes.left.style.marginBottom = this.bodyPanes.right.style.marginBottom = `${hScrollbar}px`;
741
- this.root.setAttribute("aria-rowcount", String(count + columns.depth));
742
- this.root.setAttribute("aria-colcount", String(columns.displayed().length));
743
- if (options.rowSelection === "multiple") this.root.setAttribute("aria-multiselectable", "true");
1073
+ const g = this.gridEl;
1074
+ g.setAttribute("role", options.treeData || this.grid.getGroupBy().length ? "treegrid" : "grid");
1075
+ g.setAttribute("aria-rowcount", String(count + columns.depth));
1076
+ g.setAttribute("aria-colcount", String(columns.displayed().length));
1077
+ if (options.rowSelection === "multiple") g.setAttribute("aria-multiselectable", "true");
744
1078
  if (this.headerDirty) {
745
1079
  this.headerDirty = false;
746
1080
  this.renderHeader();
747
1081
  }
748
1082
  this.renderRows(count);
1083
+ this.updateActiveCell();
1084
+ if (auto) this.measureRows();
749
1085
  this.renderSummary(count, summary && summary.opts.rows, summary?.values ?? [], sumPinned, sumPos);
750
1086
  this.pinnedTop = this.renderPinned(this.pinnedTop, "top", this.grid.pinnedRows.top);
751
1087
  this.pinnedBottom = this.renderPinned(this.pinnedBottom, "bottom", this.grid.pinnedRows.bottom);
@@ -765,11 +1101,12 @@ var Renderer = class {
765
1101
  /** Place optional sections around the body without ever moving the body (which would reset its scroll). */
766
1102
  orderSections(summaryPos) {
767
1103
  const tops = [summaryPos === "top" ? this.summaryEl : null, this.pinnedTop?.el ?? null].filter((x) => !!x);
768
- const bottoms = [this.pinnedBottom?.el ?? null, summaryPos === "bottom" ? this.summaryEl : null, this.statusEl, this.pagerEl].filter((x) => !!x);
769
- for (const t of tops) if (t.nextElementSibling !== this.bodyEl && !tops.includes(t.nextElementSibling)) this.main.insertBefore(t, this.bodyEl);
1104
+ const bottoms = [this.pinnedBottom?.el ?? null, summaryPos === "bottom" ? this.summaryEl : null].filter((x) => !!x);
1105
+ const after = [this.statusEl, this.pagerEl].filter((x) => !!x);
1106
+ for (const t of tops) if (t.nextElementSibling !== this.bodyEl && !tops.includes(t.nextElementSibling)) this.gridEl.insertBefore(t, this.bodyEl);
770
1107
  let ref = this.bodyEl;
771
1108
  for (let i = tops.length - 1; i >= 0; i--) {
772
- if (ref.previousElementSibling !== tops[i]) this.main.insertBefore(tops[i], ref);
1109
+ if (ref.previousElementSibling !== tops[i]) this.gridEl.insertBefore(tops[i], ref);
773
1110
  ref = tops[i];
774
1111
  }
775
1112
  ref = this.bodyEl;
@@ -777,6 +1114,25 @@ var Renderer = class {
777
1114
  if (ref.nextElementSibling !== b) ref.after(b);
778
1115
  ref = b;
779
1116
  }
1117
+ ref = this.gridEl;
1118
+ for (const b of after) {
1119
+ if (ref.nextElementSibling !== b) ref.after(b);
1120
+ ref = b;
1121
+ }
1122
+ }
1123
+ /** Point aria-activedescendant at the focused cell so screen readers announce it. */
1124
+ updateActiveCell() {
1125
+ const prev = this.activeCellId ? this.root.getRootNode().getElementById?.(this.activeCellId) ?? null : null;
1126
+ const cell = this.gridEl.querySelector(".fg-body .fg-cell-focused, .fg-body .fg-row-group .fg-cell-focused");
1127
+ if (prev && prev !== cell) prev.removeAttribute("id");
1128
+ if (!cell) {
1129
+ this.gridEl.removeAttribute("aria-activedescendant");
1130
+ this.activeCellId = "";
1131
+ return;
1132
+ }
1133
+ if (!cell.id) cell.id = `${this.idBase}-cell`;
1134
+ this.activeCellId = cell.id;
1135
+ this.gridEl.setAttribute("aria-activedescendant", cell.id);
780
1136
  }
781
1137
  renderPinned(section, position, rows) {
782
1138
  if (!rows.length) {
@@ -862,7 +1218,26 @@ var Renderer = class {
862
1218
  return cell;
863
1219
  }
864
1220
  const label = el("span", "fg-header-label");
1221
+ const hdef = c.node.def;
865
1222
  label.textContent = c.label;
1223
+ if (hdef.headerRenderer) {
1224
+ try {
1225
+ const out = hdef.headerRenderer({ header: c.label, column: hdef });
1226
+ if (typeof out === "string") label.innerHTML = out;
1227
+ else label.replaceChildren(out);
1228
+ } catch {
1229
+ warn(`headerRenderer of column "${c.id}" threw; showing the plain header`);
1230
+ label.textContent = c.label;
1231
+ }
1232
+ }
1233
+ if (hdef.headerClass) {
1234
+ try {
1235
+ const extra = typeof hdef.headerClass === "function" ? hdef.headerClass(hdef) : hdef.headerClass;
1236
+ if (extra) cell.classList.add(...extra.split(/\s+/).filter(Boolean));
1237
+ } catch {
1238
+ warn(`headerClass of column "${c.id}" threw`);
1239
+ }
1240
+ }
866
1241
  cell.append(label);
867
1242
  if (c.isGroup || !c.column) return cell;
868
1243
  const def = c.node.def;
@@ -892,26 +1267,27 @@ var Renderer = class {
892
1267
  return cell;
893
1268
  }
894
1269
  selectAllBox(locale) {
895
- const rows = this.grid.rowModel.dataRows();
1270
+ const rows = this.grid.selectableRows();
896
1271
  const sel = this.grid.store.state.selected;
897
1272
  let n = 0;
898
1273
  for (const r of rows) if (sel.has(r.id)) n++;
899
1274
  const box = el("input", "fg-checkbox fg-select-all", { type: "checkbox", tabindex: "-1", "aria-label": locale.selectAllRows });
900
1275
  box.checked = rows.length > 0 && n === rows.length;
901
1276
  box.indeterminate = n > 0 && n < rows.length;
902
- box.hidden = this.grid.options.rowSelection !== "multiple";
1277
+ box.hidden = this.grid.options.rowSelection !== "multiple" || this.grid.options.headerCheckbox === false;
903
1278
  return box;
904
1279
  }
905
1280
  renderRows(count) {
906
1281
  const { columns, rowModel, store } = this.grid;
907
1282
  const { selected, focus } = store.state;
908
- const bounds = this.grid.rangeBounds();
1283
+ const allBounds = this.grid.allRangeBounds();
909
1284
  const spans = this.grid.mergeSpans();
910
1285
  const top = Math.max(0, this.scroller.scrollTop - this.topOffset);
911
1286
  const range = this.offsets ? rowRangeByOffsets(this.offsets, top, this.scroller.clientHeight, count, ROW_OVERSCAN) : rowRange(top, this.scroller.clientHeight, this.opts.rowHeight, count, ROW_OVERSCAN);
912
1287
  this.lastRange = range;
913
1288
  const center = columns.region("center");
914
1289
  const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
1290
+ if (center.some((c) => c.def.colSpan)) cr.first = Math.max(0, cr.first - MAX_COLSPAN_REACH);
915
1291
  for (const r of REGIONS2) {
916
1292
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
917
1293
  const map = this.painted[r];
@@ -927,7 +1303,8 @@ var Renderer = class {
927
1303
  const node = rowModel.getRow(i);
928
1304
  const isSel = selected.has(node.id);
929
1305
  const focusCol = focus && focus.rowIndex === i ? focus.colId : "";
930
- const inRange = bounds && i >= bounds.r1 && i <= bounds.r2 ? `${bounds.c1}-${bounds.c2}` : "";
1306
+ const hits = allBounds.filter((b) => i >= b.r1 && i <= b.r2);
1307
+ const inRange = hits.map((b) => `${b.c1}-${b.c2}`).join(",");
931
1308
  const merge2 = this.mergeState(spans, cols, i, range.first);
932
1309
  const pos = node.kind === "detail" ? `${this.scroller.scrollLeft}:${this.scroller.clientWidth}` : "";
933
1310
  const key = `${node.id}|${this.version}|${colKey}|${isSel ? 1 : 0}|${focusCol}|${inRange}|${merge2}|${pos}`;
@@ -947,7 +1324,7 @@ var Renderer = class {
947
1324
  if (node.kind === "group") this.paintGroupRow(row.el, node, i, cols, focusCol);
948
1325
  else if (node.kind === "detail") this.paintDetailRow(row.el, node, i, r);
949
1326
  else if (node.kind === "loading") this.paintLoadingRow(row.el, i, cols);
950
- else this.paintRow(row.el, node, i, cols, isSel, focusCol, inRange ? bounds : null, spans, range.first);
1327
+ else this.paintRow(row.el, node, i, cols, isSel, focusCol, hits, spans, range.first);
951
1328
  }
952
1329
  }
953
1330
  }
@@ -975,22 +1352,40 @@ var Renderer = class {
975
1352
  while (rowEl.childElementCount > cols.length) rowEl.lastElementChild.remove();
976
1353
  while (rowEl.childElementCount < cols.length) rowEl.append(el("div", "fg-cell", { role: "gridcell" }));
977
1354
  let drawsMerge = false;
1355
+ const area = cols.length ? this.grid.columns.region(cols[0].region) : [];
1356
+ const spanning = area.some((c) => c.def.colSpan);
1357
+ let coveredUntil = -1;
1358
+ const posOf = spanning ? new Map(area.map((c, p) => [c.id, p])) : null;
978
1359
  cols.forEach((col, k) => {
979
1360
  const cellEl = rowEl.children[k];
980
1361
  this.paintCell(cellEl, node, i, col, selected, focusCol === col.id);
1362
+ if (posOf) {
1363
+ const p = posOf.get(col.id);
1364
+ if (p <= coveredUntil) cellEl.classList.add("fg-cell-span-hidden");
1365
+ else if (col.def.colSpan && !isSystemCol(col.id)) {
1366
+ const n = spanOf(col.def.colSpan, col.def, node.data, i, area.length - p);
1367
+ if (n > 1) {
1368
+ coveredUntil = p + n - 1;
1369
+ let w = 0;
1370
+ for (let q = p; q <= coveredUntil; q++) w += area[q].width;
1371
+ cellEl.style.width = `${w}px`;
1372
+ cellEl.classList.add("fg-cell-colspan");
1373
+ }
1374
+ }
1375
+ }
981
1376
  if (col.id === this.firstDataId) this.decorate(cellEl, node);
982
1377
  cellEl.style.top = cellEl.style.height = cellEl.style.bottom = "";
983
1378
  const s = spans.get(col.id);
984
1379
  if (s && s.start[i] !== s.end[i]) {
985
1380
  if (i === Math.max(s.start[i], first)) {
986
1381
  cellEl.classList.add("fg-cell-merged");
987
- cellEl.style.top = `${-(i - s.start[i]) * rh}px`;
988
- cellEl.style.height = `${(s.end[i] - s.start[i] + 1) * rh}px`;
1382
+ cellEl.style.top = `${-(this.rowTop(i) - this.rowTop(s.start[i]))}px`;
1383
+ cellEl.style.height = `${this.rowTop(s.end[i]) + this.rowH(s.end[i]) - this.rowTop(s.start[i])}px`;
989
1384
  cellEl.style.bottom = "auto";
990
1385
  drawsMerge = true;
991
1386
  } else cellEl.classList.add("fg-cell-merge-hidden");
992
1387
  }
993
- if (bounds && col.index >= bounds.c1 && col.index <= bounds.c2 && !isSystemCol(col.id)) cellEl.classList.add("fg-cell-range");
1388
+ if (!isSystemCol(col.id) && bounds.some((b) => col.index >= b.c1 && col.index <= b.c2)) cellEl.classList.add("fg-cell-range");
994
1389
  });
995
1390
  rowEl.style.zIndex = drawsMerge ? "1" : "";
996
1391
  }
@@ -1012,6 +1407,10 @@ var Renderer = class {
1012
1407
  cell.className = `${cls} fg-cell-select`;
1013
1408
  const box = el("input", "fg-checkbox", { type: "checkbox", tabindex: "-1", "aria-label": locale.selectRow });
1014
1409
  box.checked = selected;
1410
+ if (i >= 0 && !node.kind && !this.grid.rowSelectable(node.data)) {
1411
+ box.disabled = true;
1412
+ cell.classList.add("fg-cell-select-locked");
1413
+ }
1015
1414
  cell.replaceChildren(box);
1016
1415
  return;
1017
1416
  }
@@ -1029,6 +1428,8 @@ var Renderer = class {
1029
1428
  }
1030
1429
  }
1031
1430
  if (this.grid.tracker.isDirty(node.data, def.field)) cls += " fg-cell-dirty";
1431
+ if (def.wrapText) cls += " fg-cell-wrap";
1432
+ if (typeof def.editable === "function" && i >= 0 && !cellEditable(def, node.data, i)) cls += " fg-cell-readonly";
1032
1433
  if (i >= 0) cls += this.grid.finder.classFor(i, col.id);
1033
1434
  cell.className = cls;
1034
1435
  if (!def.renderer) {
@@ -1145,17 +1546,55 @@ var Renderer = class {
1145
1546
  const value = values[col.id];
1146
1547
  const s = cellSpec(spec);
1147
1548
  const code = this.grid.store.state.locale.code;
1148
- let text;
1149
- if (value == null || value === "") text = "";
1150
- else if (typeof value === "string") text = value;
1151
- else if (s.agg === "count") text = new Intl.NumberFormat(code).format(value);
1549
+ let text2;
1550
+ if (value == null || value === "") text2 = "";
1551
+ else if (typeof value === "string") text2 = value;
1552
+ else if (s.agg === "count") text2 = new Intl.NumberFormat(code).format(value);
1152
1553
  else {
1153
1554
  const type = cd.type ?? (typeof value === "number" ? "number" : value instanceof Date ? "date" : void 0);
1154
- text = getFormatter({ ...cd, type, format: s.format ?? cd.format }, code)(value);
1555
+ text2 = getFormatter({ ...cd, type, format: s.format ?? cd.format }, code)(value);
1155
1556
  }
1156
- cell.textContent = s.prefix && text ? `${s.prefix} ${text}` : text;
1557
+ cell.textContent = s.prefix && text2 ? `${s.prefix} ${text2}` : text2;
1157
1558
  return cell;
1158
1559
  }
1560
+ /** Forget measured row heights (data, widths or fonts changed); rows are measured again on screen. */
1561
+ invalidateHeights() {
1562
+ if (!this.rowHeights.size) return;
1563
+ this.rowHeights.clear();
1564
+ this.heightsVersion++;
1565
+ }
1566
+ /** Measure wrapped cells of the painted rows; when a height changes, lay out again next frame. */
1567
+ measureRows() {
1568
+ const base = this.opts.rowHeight;
1569
+ const need = /* @__PURE__ */ new Map();
1570
+ for (const r of REGIONS2) {
1571
+ for (const [i, row] of this.painted[r]) {
1572
+ const node = this.grid.rowModel.getRow(i);
1573
+ if (!node || node.kind) continue;
1574
+ let h = need.get(i) ?? base;
1575
+ for (const cell of row.el.querySelectorAll(".fg-cell-wrap")) {
1576
+ if (cell.classList.contains("fg-cell-merged") || cell.classList.contains("fg-cell-merge-hidden")) continue;
1577
+ h = Math.max(h, Math.ceil(cell.scrollHeight));
1578
+ }
1579
+ need.set(i, h);
1580
+ }
1581
+ }
1582
+ let changed = false;
1583
+ for (const [i, h] of need) {
1584
+ const id = this.grid.rowModel.getRow(i).id;
1585
+ const cur = this.rowHeights.get(id) ?? base;
1586
+ if (Math.abs(cur - h) > 1) {
1587
+ if (h === base) this.rowHeights.delete(id);
1588
+ else this.rowHeights.set(id, h);
1589
+ changed = true;
1590
+ }
1591
+ }
1592
+ if (changed) {
1593
+ this.heightsVersion++;
1594
+ this.version++;
1595
+ this.schedule();
1596
+ }
1597
+ }
1159
1598
  detailHeight() {
1160
1599
  const h = this.grid.options.detailHeight;
1161
1600
  return typeof h === "number" && h > 0 ? h : 240;
@@ -1189,12 +1628,13 @@ var Renderer = class {
1189
1628
  const lead = [];
1190
1629
  if (tree) {
1191
1630
  if (node.hasChildren) {
1192
- const b = el("button", node.expanded ? "fg-tree-toggle fg-expanded" : "fg-tree-toggle", {
1631
+ const b = el("button", node.expanded ? "fg-tree-toggle fg-expanded" : node.loading ? "fg-tree-toggle fg-tree-loading" : "fg-tree-toggle", {
1193
1632
  type: "button",
1194
1633
  tabindex: "-1",
1195
1634
  "aria-label": node.expanded ? loc.collapseAll : loc.expandAll,
1196
1635
  "aria-expanded": String(!!node.expanded)
1197
1636
  });
1637
+ if (node.loading) b.setAttribute("aria-busy", "true");
1198
1638
  b.innerHTML = CHEVRON;
1199
1639
  lead.push(b);
1200
1640
  } else lead.push(el("span", "fg-tree-spacer", { "aria-hidden": "true" }));
@@ -1356,10 +1796,10 @@ var Renderer = class {
1356
1796
  label.append(sel);
1357
1797
  parts.push(label);
1358
1798
  }
1359
- const btn = (action, text, label, disabled) => {
1799
+ const btn = (action, text2, label, disabled) => {
1360
1800
  const b = el("button", "fg-pager-btn", { type: "button", "aria-label": label, title: label });
1361
1801
  b.dataset.action = action;
1362
- b.textContent = text;
1802
+ b.textContent = text2;
1363
1803
  b.disabled = disabled;
1364
1804
  return b;
1365
1805
  };
@@ -1374,6 +1814,103 @@ var Renderer = class {
1374
1814
  );
1375
1815
  this.pagerEl.replaceChildren(...parts);
1376
1816
  }
1817
+ /** Small "free version" mark in the bottom-right corner (unlicensed company domains flagged on freegrid.app). */
1818
+ showFreeBadge() {
1819
+ if (this.root.querySelector(".fg-free-badge")) return;
1820
+ const a = el("a", "fg-free-badge", { href: "https://freegrid.app/pricing/", target: "_blank", rel: "noopener" });
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>';
1822
+ const text2 = el("span");
1823
+ text2.textContent = `FreeGrid \xB7 ${this.grid.store.state.locale.freeBadge}`;
1824
+ a.append(text2);
1825
+ this.root.append(a);
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
+ }
1377
1914
  /** Forget cached detail panels (one master, or all). */
1378
1915
  dropDetail(id) {
1379
1916
  if (id === void 0) this.details.clear();
@@ -1383,14 +1920,14 @@ var Renderer = class {
1383
1920
  return this.lastRange;
1384
1921
  }
1385
1922
  /** Text width in px using the grid font; falls back to 0.6em per char without canvas. */
1386
- measureText(text, bold) {
1923
+ measureText(text2, bold) {
1387
1924
  const cs = getComputedStyle(this.main);
1388
1925
  const size = parseFloat(getComputedStyle(this.root).getPropertyValue("--fg-font-size")) || 13;
1389
1926
  if (this.measureCtx === void 0) this.measureCtx = document.createElement("canvas").getContext("2d");
1390
1927
  const ctx = this.measureCtx;
1391
- if (!ctx) return text.length * size * 0.6;
1928
+ if (!ctx) return text2.length * size * 0.6;
1392
1929
  ctx.font = `${bold ? 600 : 400} ${size}px ${cs.fontFamily || "sans-serif"}`;
1393
- return ctx.measureText(text).width;
1930
+ return ctx.measureText(text2).width;
1394
1931
  }
1395
1932
  scrollToRow(index) {
1396
1933
  this.scroller.scrollTop = this.rowTop(Math.max(0, Math.min(index, this.grid.rowModel.getRowCount())));
@@ -1476,7 +2013,7 @@ var GroupPanel = class {
1476
2013
  window.addEventListener("pointerup", up);
1477
2014
  };
1478
2015
  this.el = el("div", "fg-group-panel", { role: "toolbar" });
1479
- grid.renderer.main.insertBefore(this.el, grid.renderer.headerEl);
2016
+ grid.renderer.main.insertBefore(this.el, grid.renderer.gridEl);
1480
2017
  this.el.addEventListener("click", this.onClick);
1481
2018
  this.el.addEventListener("pointerdown", this.onPointerDown);
1482
2019
  this.refresh();
@@ -1587,6 +2124,7 @@ var en = {
1587
2124
  pasteHint: "Clipboard access was blocked. Press Ctrl+V to paste.",
1588
2125
  required: "This field is required",
1589
2126
  invalidValue: "Invalid value",
2127
+ duplicate: "This value is already used",
1590
2128
  undo: "Undo",
1591
2129
  redo: "Redo",
1592
2130
  deleteRows: "Delete selected rows",
@@ -1633,6 +2171,7 @@ var en = {
1633
2171
  chartLine: "Line chart",
1634
2172
  chartArea: "Area chart",
1635
2173
  chartPie: "Pie chart",
2174
+ freeBadge: "Free version",
1636
2175
  ops: {
1637
2176
  contains: "Contains",
1638
2177
  equals: "Equals",
@@ -1691,6 +2230,25 @@ function resolveLocale(input, navLangs = navigatorLanguages()) {
1691
2230
  return en_default;
1692
2231
  }
1693
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
+
1694
2252
  // src/keyboard.ts
1695
2253
  var NAV_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Tab", "Home", "End", "PageUp", "PageDown"]);
1696
2254
  function nextFocus(key, mods, p, s) {
@@ -1723,6 +2281,10 @@ function nextFocus(key, mods, p, s) {
1723
2281
  }
1724
2282
  }
1725
2283
 
2284
+ // src/registry.ts
2285
+ var liveGrids = /* @__PURE__ */ new Map();
2286
+ var gridOfRoot = (root) => root ? liveGrids.get(root) : void 0;
2287
+
1726
2288
  // src/interactions/body.ts
1727
2289
  var DEFAULT_KEYMAP = {
1728
2290
  Enter: "edit",
@@ -1744,10 +2306,17 @@ function comboOf(e) {
1744
2306
  return parts.join("+");
1745
2307
  }
1746
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
+ }
1747
2313
  function attachBody(grid) {
1748
2314
  const { renderer } = grid;
1749
2315
  const body = renderer.bodyEl;
1750
2316
  const root = renderer.root;
2317
+ const keyOf = (e) => mirrored(grid, e);
2318
+ let lastPointer = "mouse";
2319
+ let pressTimer = 0;
1751
2320
  let suppressClick = false;
1752
2321
  const toEvent = (e) => {
1753
2322
  const hit = renderer.cellAt(e.target);
@@ -1762,7 +2331,7 @@ function attachBody(grid) {
1762
2331
  const btn = e.target.closest?.(".fg-group-toggle, .fg-tree-toggle, .fg-detail-toggle");
1763
2332
  const hit = btn && renderer.cellAt(e.target);
1764
2333
  if (!hit) return false;
1765
- root.focus({ preventScroll: true });
2334
+ renderer.gridEl.focus({ preventScroll: true });
1766
2335
  grid.setFocus(hit.rowIndex, hit.colId);
1767
2336
  grid.setRange(null);
1768
2337
  const node = grid.rowModel.getRow(hit.rowIndex);
@@ -1772,23 +2341,28 @@ function attachBody(grid) {
1772
2341
  };
1773
2342
  const onClick = (e) => {
1774
2343
  if (onToggle(e)) return;
2344
+ const before = grid.store.state.focus;
1775
2345
  const hit = renderer.cellAt(e.target);
1776
2346
  if (hit && grid.rowModel.getRow(hit.rowIndex)?.kind) {
1777
- root.focus({ preventScroll: true });
2347
+ renderer.gridEl.focus({ preventScroll: true });
1778
2348
  grid.setFocus(hit.rowIndex, hit.colId);
1779
2349
  grid.setRange(null);
1780
2350
  return;
1781
2351
  }
1782
2352
  const ev = toEvent(e);
1783
2353
  if (!ev) return;
1784
- root.focus({ preventScroll: true });
2354
+ renderer.gridEl.focus({ preventScroll: true });
1785
2355
  const dragged = suppressClick;
1786
2356
  suppressClick = false;
1787
2357
  const prev = grid.store.state.focus;
1788
2358
  const onBox = ev.colId === SELECT_COL_ID;
1789
2359
  if (dragged) {
1790
2360
  } else if (e.shiftKey && prev && !onBox && !isSystemCol(prev.colId) && grid.options.rangeSelection !== false) {
1791
- 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);
1792
2366
  } else {
1793
2367
  grid.setFocus(ev.rowIndex, ev.colId);
1794
2368
  grid.setRange(null);
@@ -1797,6 +2371,11 @@ function attachBody(grid) {
1797
2371
  grid.selectFromClick(ev.rowIndex, onBox || e.ctrlKey || e.metaKey, e.shiftKey);
1798
2372
  }
1799
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
+ }
1800
2379
  };
1801
2380
  const onDblClick = (e) => {
1802
2381
  if (e.target.closest?.("button")) return;
@@ -1874,8 +2453,8 @@ function attachBody(grid) {
1874
2453
  const open = !!(node.group?.expanded ?? node.expanded);
1875
2454
  const key = grid.expandKey(node);
1876
2455
  if (group && (e.key === "Enter" || e.key === " ")) grid.setGroupExpanded(key, !open);
1877
- else if (e.key === "ArrowRight" && !open) grid.setGroupExpanded(key, true);
1878
- 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);
1879
2458
  else return false;
1880
2459
  return true;
1881
2460
  };
@@ -1887,16 +2466,42 @@ function attachBody(grid) {
1887
2466
  if (!node || node.kind || !grid.canMoveRows()) return true;
1888
2467
  const row = node.data;
1889
2468
  const marker = el("div", "fg-row-drop-marker");
1890
- let target = null;
2469
+ let drop = null;
1891
2470
  root.classList.add("fg-row-dragging");
1892
2471
  const move = (ev) => {
1893
- const under = document.elementFromPoint(ev.clientX, ev.clientY)?.closest(".fg-row[data-row]");
1894
- if (!under || under.closest(".fg-root") !== root) return;
1895
- const box = under.getBoundingClientRect();
1896
- const after = ev.clientY > box.top + box.height / 2;
1897
- target = { index: Number(under.dataset.row), after };
1898
- if (!marker.isConnected) root.append(marker);
1899
- 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);
1900
2505
  };
1901
2506
  const up = () => {
1902
2507
  window.removeEventListener("pointermove", move);
@@ -1904,39 +2509,66 @@ function attachBody(grid) {
1904
2509
  window.removeEventListener("pointercancel", up);
1905
2510
  root.classList.remove("fg-row-dragging");
1906
2511
  marker.remove();
1907
- if (!target) return;
1908
- const over = grid.rowModel.getRow(target.index);
1909
- const overRow = over?.detailOf?.data ?? over?.data;
1910
- const data = grid.store.state.data;
1911
- const from = data.indexOf(row);
1912
- let to = data.indexOf(overRow) + (target.after || over?.kind === "detail" ? 1 : 0);
1913
- if (from < 0 || to < 0) return;
1914
- if (to > from) to--;
1915
- grid.moveRow(row, to);
2512
+ if (drop) drop.into.dropRow(row, drop.target, drop.position, grid);
1916
2513
  };
1917
2514
  window.addEventListener("pointermove", move);
1918
2515
  window.addEventListener("pointerup", up);
1919
2516
  window.addEventListener("pointercancel", up);
1920
2517
  return true;
1921
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
+ };
1922
2547
  const onPointerDown = (e) => {
2548
+ lastPointer = e.pointerType || "mouse";
2549
+ if (lastPointer === "touch") {
2550
+ onTouchDown(e);
2551
+ return;
2552
+ }
1923
2553
  if (e.button === 0 && onHandleDown(e)) return;
1924
2554
  if (grid.options.rangeSelection === false) return;
1925
2555
  if (e.button !== 0 || e.shiftKey || e.target.closest("input, a, button")) return;
1926
2556
  const start = renderer.cellAt(e.target);
1927
2557
  if (!start || isSystemCol(start.colId)) return;
1928
2558
  const x0 = e.clientX, y0 = e.clientY;
2559
+ const adding = e.ctrlKey || e.metaKey;
1929
2560
  let dragging = false;
1930
2561
  const move = (ev) => {
1931
2562
  if (!dragging && Math.hypot(ev.clientX - x0, ev.clientY - y0) < DRAG_THRESHOLD2) return;
1932
- const hit = renderer.cellAt(document.elementFromPoint(ev.clientX, ev.clientY));
2563
+ const hit = renderer.cellAt(elementAt(root, ev.clientX, ev.clientY));
1933
2564
  if (!hit || isSystemCol(hit.colId)) return;
1934
2565
  if (!dragging) {
1935
2566
  dragging = true;
1936
2567
  root.classList.add("fg-range-dragging");
2568
+ if (adding) grid.addRange({ start, end: start });
1937
2569
  grid.setFocus(start.rowIndex, start.colId);
1938
2570
  }
1939
- grid.setRange({ start, end: hit });
2571
+ grid.setRange({ start, end: hit }, adding);
1940
2572
  };
1941
2573
  const up = () => {
1942
2574
  window.removeEventListener("pointermove", move);
@@ -1948,7 +2580,7 @@ function attachBody(grid) {
1948
2580
  window.addEventListener("pointerup", up);
1949
2581
  };
1950
2582
  const onKeyDown = (e) => {
1951
- if (e.target !== root) return;
2583
+ if (e.target !== root && e.target !== renderer.gridEl) return;
1952
2584
  const cols = grid.columns.displayed();
1953
2585
  const count = grid.rowModel.getRowCount();
1954
2586
  if (!cols.length || !count) return;
@@ -1990,7 +2622,7 @@ function attachBody(grid) {
1990
2622
  const range = grid.store.state.range;
1991
2623
  const end = range?.end ?? anchor;
1992
2624
  const pos = { rowIndex: Math.min(end.rowIndex, count - 1), colIndex: Math.max(0, cols.findIndex((c) => c.id === end.colId)) };
1993
- const next2 = nextFocus(e.key, { ctrl, shift: false }, pos, size);
2625
+ const next2 = nextFocus(keyOf(e), { ctrl, shift: false }, pos, size);
1994
2626
  if (!next2) return;
1995
2627
  e.preventDefault();
1996
2628
  const colId2 = cols[Math.max(firstData, next2.colIndex)].id;
@@ -2003,7 +2635,7 @@ function attachBody(grid) {
2003
2635
  if (!f) next = NAV_KEYS.has(e.key) && e.key !== "Tab" ? { rowIndex: 0, colIndex: 0 } : null;
2004
2636
  else {
2005
2637
  const colIndex2 = Math.max(0, cols.findIndex((c) => c.id === f.colId));
2006
- 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);
2007
2639
  }
2008
2640
  if (!next) return;
2009
2641
  e.preventDefault();
@@ -2016,17 +2648,17 @@ function attachBody(grid) {
2016
2648
  const nested = (e) => e.target?.closest?.(".fg-root") !== root;
2017
2649
  const onCopy = (e) => {
2018
2650
  if (fromField(e) || nested(e)) return;
2019
- const text = grid.selectionTSV(false);
2020
- if (!text || !e.clipboardData) return;
2021
- 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);
2022
2654
  e.preventDefault();
2023
2655
  };
2024
2656
  const onPaste = (e) => {
2025
2657
  if (fromField(e) || nested(e)) return;
2026
- const text = e.clipboardData?.getData("text/plain");
2027
- if (!text) return;
2658
+ const text2 = e.clipboardData?.getData("text/plain");
2659
+ if (!text2) return;
2028
2660
  e.preventDefault();
2029
- grid.pasteText(text);
2661
+ grid.pasteText(text2);
2030
2662
  };
2031
2663
  body.addEventListener("click", onClick);
2032
2664
  body.addEventListener("dblclick", onDblClick);
@@ -2288,9 +2920,9 @@ var ColumnChooserPanel = class {
2288
2920
  box.checked = leaves.length > 0 && shown === leaves.length;
2289
2921
  box.indeterminate = shown > 0 && shown < leaves.length;
2290
2922
  box.disabled = leaves.every((l) => l.def.lockVisible);
2291
- const text = el("span", "fg-chooser-label");
2292
- text.textContent = labelOf(n);
2293
- row.append(box, text);
2923
+ const text2 = el("span", "fg-chooser-label");
2924
+ text2.textContent = labelOf(n);
2925
+ row.append(box, text2);
2294
2926
  if (!n.children && n.pinned) {
2295
2927
  const pin = el("span", "fg-chooser-pin");
2296
2928
  pin.textContent = n.pinned === "left" ? "\u21E4" : "\u21E5";
@@ -2368,14 +3000,14 @@ var ColumnChooserDialog = class {
2368
3000
  head.append(title, close);
2369
3001
  this.panel = new ColumnChooserPanel(this.grid);
2370
3002
  popup.append(head, this.panel.el);
2371
- document.body.append(popup);
3003
+ popupParent(this.grid.renderer.root).append(popup);
2372
3004
  const r = this.grid.renderer.root.getBoundingClientRect();
2373
3005
  const w = popup.offsetWidth || 280;
2374
3006
  popup.style.left = `${Math.max(4, Math.min(r.right - w - 8, window.innerWidth - w - 4))}px`;
2375
3007
  popup.style.top = `${Math.max(4, r.top + 8)}px`;
2376
3008
  this.popup = popup;
2377
3009
  const onDown = (e) => {
2378
- if (!popup.contains(e.target)) this.close();
3010
+ if (!eventInside(e, popup)) this.close();
2379
3011
  };
2380
3012
  const onKey = (e) => {
2381
3013
  if (e.key === "Escape") this.close();
@@ -2490,7 +3122,7 @@ var ContextMenu = class {
2490
3122
  const hit = grid.renderer.cellAt(target);
2491
3123
  if (!hit) return;
2492
3124
  e.preventDefault();
2493
- grid.renderer.root.focus({ preventScroll: true });
3125
+ grid.renderer.gridEl.focus({ preventScroll: true });
2494
3126
  if (!isSystemCol(hit.colId) && !grid.rangeContains(hit.rowIndex, hit.colId)) {
2495
3127
  grid.setFocus(hit.rowIndex, hit.colId);
2496
3128
  grid.setRange({ start: hit, end: hit });
@@ -2560,7 +3192,7 @@ var ContextMenu = class {
2560
3192
  const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
2561
3193
  const fail = () => this.grid.toast(this.grid.store.state.locale.pasteHint.replace("Ctrl+V", shortcut("V")));
2562
3194
  if (!clip?.readText) return fail();
2563
- clip.readText().then((text) => this.grid.pasteText(text), fail);
3195
+ clip.readText().then((text2) => this.grid.pasteText(text2), fail);
2564
3196
  }
2565
3197
  open(x, y, rowIndex, colId) {
2566
3198
  this.close();
@@ -2613,23 +3245,23 @@ var ContextMenu = class {
2613
3245
  prevSep = false;
2614
3246
  }
2615
3247
  if (popup.lastElementChild?.classList.contains("fg-cm-sep")) popup.lastElementChild.remove();
2616
- document.body.append(popup);
3248
+ popupParent(this.grid.renderer.root).append(popup);
2617
3249
  const w = popup.offsetWidth || 220, h = popup.offsetHeight || 300;
2618
3250
  popup.style.left = `${Math.max(4, Math.min(x, window.innerWidth - w - 4))}px`;
2619
3251
  popup.style.top = `${Math.max(4, Math.min(y, window.innerHeight - h - 4))}px`;
2620
3252
  this.popup = popup;
2621
3253
  const onDown = (e) => {
2622
- if (!popup.contains(e.target)) this.close();
3254
+ if (!eventInside(e, popup)) this.close();
2623
3255
  };
2624
3256
  const onKey = (e) => {
2625
3257
  if (e.key === "Escape") {
2626
3258
  this.close();
2627
- grid.renderer.root.focus();
3259
+ grid.renderer.gridEl.focus();
2628
3260
  return;
2629
3261
  }
2630
3262
  if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
2631
3263
  const buttons = [...popup.querySelectorAll("button:not(:disabled)")];
2632
- const i = buttons.indexOf(document.activeElement);
3264
+ const i = buttons.indexOf(activeElement(popup));
2633
3265
  const next = buttons[(i + (e.key === "ArrowDown" ? 1 : buttons.length - 1) + buttons.length) % buttons.length];
2634
3266
  next?.focus();
2635
3267
  e.preventDefault();
@@ -2667,7 +3299,7 @@ var FillHandle = class {
2667
3299
  let target = null;
2668
3300
  const rootEl = grid.renderer.root;
2669
3301
  const move = (ev) => {
2670
- 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));
2671
3303
  const col = hit && grid.columns.getColumn(hit.colId);
2672
3304
  if (!hit || !col || isSystemCol(col.id)) return;
2673
3305
  const dr = hit.rowIndex > src.r2 ? hit.rowIndex - src.r2 : hit.rowIndex < src.r1 ? hit.rowIndex - src.r1 : 0;
@@ -2724,7 +3356,7 @@ var FillHandle = class {
2724
3356
  }
2725
3357
  const root = this.grid.renderer.root.getBoundingClientRect();
2726
3358
  const r = cell.getBoundingClientRect();
2727
- 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`;
2728
3360
  this.handle.style.top = `${r.bottom - root.top - 4}px`;
2729
3361
  if (!this.handle.isConnected) this.grid.renderer.root.append(this.handle);
2730
3362
  }
@@ -2773,8 +3405,8 @@ var Finder = class {
2773
3405
  if (i === void 0) return "";
2774
3406
  return i === this.index ? " fg-cell-match fg-cell-match-current" : " fg-cell-match";
2775
3407
  }
2776
- find(text) {
2777
- this.query = text.trim().toLowerCase();
3408
+ find(text2) {
3409
+ this.query = text2.trim().toLowerCase();
2778
3410
  this.matches = [];
2779
3411
  this.lookup.clear();
2780
3412
  this.index = -1;
@@ -2787,8 +3419,8 @@ var Finder = class {
2787
3419
  outer: for (let r = 0; r < rows.length; r++) {
2788
3420
  if (rows[r].kind) continue;
2789
3421
  for (let c = 0; c < cols.length; c++) {
2790
- const text2 = fmts[c](getCellValue(cols[c].def, rows[r].data)).toLowerCase();
2791
- 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;
2792
3424
  this.lookup.set(`${r}|${cols[c].id}`, this.matches.length);
2793
3425
  this.matches.push({ rowIndex: r, colId: cols[c].id });
2794
3426
  if (this.matches.length >= MAX_MATCHES) break outer;
@@ -2840,9 +3472,9 @@ var Finder = class {
2840
3472
  const bar = el("div", "fg-findbar", { role: "search" });
2841
3473
  const input = el("input", "fg-input", { type: "search", placeholder: loc.findPlaceholder, "aria-label": loc.findPlaceholder });
2842
3474
  this.countEl = el("span", "fg-findbar-count", { "aria-live": "polite" });
2843
- const btn = (label, text, run) => {
3475
+ const btn = (label, text2, run) => {
2844
3476
  const b = el("button", "fg-findbar-btn", { type: "button", "aria-label": label, title: label });
2845
- b.textContent = text;
3477
+ b.textContent = text2;
2846
3478
  b.addEventListener("click", run);
2847
3479
  return b;
2848
3480
  };
@@ -2874,40 +3506,10 @@ var Finder = class {
2874
3506
  this.bar = null;
2875
3507
  this.countEl = null;
2876
3508
  this.clear();
2877
- this.grid.renderer.root.focus({ preventScroll: true });
3509
+ this.grid.renderer.gridEl.focus({ preventScroll: true });
2878
3510
  }
2879
3511
  };
2880
3512
 
2881
- // src/merge.ts
2882
- function computeSpans(rows, def) {
2883
- const n = rows.length;
2884
- const start = new Int32Array(n);
2885
- const end = new Int32Array(n);
2886
- const same2 = typeof def.merge === "function" ? (a, b) => {
2887
- try {
2888
- return !!def.merge(getCellValue(def, rows[a]), getCellValue(def, rows[b]), rows[a], rows[b]);
2889
- } catch {
2890
- return false;
2891
- }
2892
- } : (() => {
2893
- const keys = rows.map((r) => {
2894
- const v = getCellValue(def, r);
2895
- return isBlank(v) ? null : setKey(v);
2896
- });
2897
- return (a, b) => keys[a] !== null && keys[a] === keys[b];
2898
- })();
2899
- let s = 0;
2900
- for (let i = 1; i <= n; i++) {
2901
- if (i < n && same2(i - 1, i)) continue;
2902
- for (let k = s; k < i; k++) {
2903
- start[k] = s;
2904
- end[k] = i - 1;
2905
- }
2906
- s = i;
2907
- }
2908
- return { start, end };
2909
- }
2910
-
2911
3513
  // src/fill.ts
2912
3514
  var DAY = 864e5;
2913
3515
  function fillSeries(source, count) {
@@ -2952,22 +3554,22 @@ function toTSV(rows) {
2952
3554
  const cell = (s) => /[\t\n\r"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
2953
3555
  return rows.map((r) => r.map(cell).join(" ")).join("\r\n");
2954
3556
  }
2955
- function parseTSV(text) {
2956
- return parseDelimited(text, " ");
3557
+ function parseTSV(text2) {
3558
+ return parseDelimited(text2, " ");
2957
3559
  }
2958
- function parseDelimited(text, sep) {
2959
- if (!text) return [];
3560
+ function parseDelimited(text2, sep) {
3561
+ if (!text2) return [];
2960
3562
  const rows = [];
2961
3563
  let row = [];
2962
3564
  let cell = "";
2963
3565
  let i = 0;
2964
3566
  let quoted = false;
2965
3567
  let atCellStart = true;
2966
- while (i < text.length) {
2967
- const ch = text[i];
3568
+ while (i < text2.length) {
3569
+ const ch = text2[i];
2968
3570
  if (quoted) {
2969
3571
  if (ch === '"') {
2970
- if (text[i + 1] === '"') {
3572
+ if (text2[i + 1] === '"') {
2971
3573
  cell += '"';
2972
3574
  i += 2;
2973
3575
  continue;
@@ -2999,7 +3601,7 @@ function parseDelimited(text, sep) {
2999
3601
  row = [];
3000
3602
  cell = "";
3001
3603
  atCellStart = true;
3002
- i += ch === "\r" && text[i + 1] === "\n" ? 2 : 1;
3604
+ i += ch === "\r" && text2[i + 1] === "\n" ? 2 : 1;
3003
3605
  continue;
3004
3606
  }
3005
3607
  cell += ch;
@@ -3029,8 +3631,8 @@ function copyValue(def, value, locale) {
3029
3631
  }
3030
3632
  var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "1", "y", "yes", "\uC608", "o", "on"]);
3031
3633
  var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "0", "n", "no", "\uC544\uB2C8\uC624", "x", "off"]);
3032
- function parseNumber(text) {
3033
- let s = text.trim();
3634
+ function parseNumber(text2) {
3635
+ let s = text2.trim();
3034
3636
  if (!s) return { ok: true, value: null };
3035
3637
  let negative = false;
3036
3638
  if (/^\(.*\)$/.test(s)) {
@@ -3050,8 +3652,8 @@ function validDate(y, m, d) {
3050
3652
  const date = new Date(y, m - 1, d);
3051
3653
  return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d ? date : null;
3052
3654
  }
3053
- function parseDate(text, previous) {
3054
- const s = text.trim();
3655
+ function parseDate(text2, previous) {
3656
+ const s = text2.trim();
3055
3657
  if (!s) return { ok: true, value: null };
3056
3658
  let date = null;
3057
3659
  let m = /^(\d{4})\s*[-./년]\s*(\d{1,2})\s*[-./월]\s*(\d{1,2})\s*[.일]?$/.exec(s);
@@ -3061,26 +3663,37 @@ function parseDate(text, previous) {
3061
3663
  if (!date) return { ok: false };
3062
3664
  return { ok: true, value: previous instanceof Date ? date : toDateKey(date) };
3063
3665
  }
3064
- function parseValue(def, text, previous) {
3666
+ function parseValue(def, text2, previous) {
3065
3667
  switch (def.type) {
3066
3668
  case "number":
3067
- return parseNumber(text);
3669
+ return parseNumber(text2);
3068
3670
  case "date":
3069
- return parseDate(text, previous);
3671
+ return parseDate(text2, previous);
3070
3672
  case "boolean": {
3071
- const t = text.trim().toLowerCase();
3673
+ const t = text2.trim().toLowerCase();
3072
3674
  if (!t) return { ok: true, value: null };
3073
3675
  if (TRUE_WORDS.has(t)) return { ok: true, value: true };
3074
3676
  if (FALSE_WORDS.has(t)) return { ok: true, value: false };
3075
3677
  return { ok: false };
3076
3678
  }
3077
3679
  default:
3078
- return { ok: true, value: text };
3680
+ return { ok: true, value: text2 };
3079
3681
  }
3080
3682
  }
3081
3683
 
3082
3684
  // src/editing/editor.ts
3083
- 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
+ }
3084
3697
  var CellEditor = class {
3085
3698
  constructor(grid) {
3086
3699
  this.grid = grid;
@@ -3092,12 +3705,15 @@ var CellEditor = class {
3092
3705
  canEdit(rowIndex, colId) {
3093
3706
  if (isSystemCol(colId)) return false;
3094
3707
  const col = this.grid.columns.getColumn(colId);
3095
- 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);
3096
3711
  }
3097
3712
  start(rowIndex, colId, initialText) {
3098
3713
  if (this.cur) this.commit(null);
3099
3714
  if (!this.canEdit(rowIndex, colId)) return false;
3100
3715
  const { grid } = this;
3716
+ if (grid.veto("beforeEdit", { row: grid.rowModel.getRow(rowIndex).data, rowIndex, colId })) return false;
3101
3717
  const { renderer } = grid;
3102
3718
  const col = grid.columns.getColumn(colId);
3103
3719
  const def = col.def;
@@ -3133,12 +3749,24 @@ var CellEditor = class {
3133
3749
  const opt = el("option");
3134
3750
  opt.value = String(i);
3135
3751
  opt.textContent = o.label ?? String(o.value);
3752
+ opt.disabled = !!o.disabled;
3136
3753
  select.append(opt);
3137
3754
  });
3138
3755
  select.selectedIndex = Math.max(0, options.findIndex((o) => Object.is(o.value, value)));
3139
3756
  control = select;
3140
3757
  read = () => ({ value: options[select.selectedIndex]?.value ?? null });
3141
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);
3142
3770
  } else {
3143
3771
  const textarea = spec?.type === "textarea";
3144
3772
  const input = textarea ? el("textarea", "fg-editor-control") : el("input", "fg-editor-control");
@@ -3209,21 +3837,15 @@ var CellEditor = class {
3209
3837
  const old = cur.row[field];
3210
3838
  const raw = cur.read();
3211
3839
  let value;
3212
- 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) {
3213
3843
  const parsed = parseValue(cur.def, raw.text, old);
3214
3844
  if (!parsed.ok) return this.fail(raw.text, loc.invalidValue);
3215
3845
  value = parsed.value;
3216
3846
  } else value = raw.value;
3217
- if (cur.def.required && isBlank(value)) return this.fail(value, loc.required);
3218
- if (cur.def.validate) {
3219
- let res;
3220
- try {
3221
- res = cur.def.validate(value, cur.row);
3222
- } catch {
3223
- res = loc.invalidValue;
3224
- }
3225
- if (res !== true) return this.fail(value, typeof res === "string" && res ? res : loc.invalidValue);
3226
- }
3847
+ const error = grid.cellError(cur.def, cur.row, value);
3848
+ if (error !== null) return this.fail(value, error);
3227
3849
  this.close(true);
3228
3850
  grid.applyEdit(cur.rowIndex, cur.colId, value);
3229
3851
  this.move(cur.rowIndex, cur.colId, move);
@@ -3244,7 +3866,7 @@ var CellEditor = class {
3244
3866
  if (!cur) return;
3245
3867
  this.cur = null;
3246
3868
  cur.dispose();
3247
- this.grid.renderer.root.focus({ preventScroll: true });
3869
+ this.grid.renderer.gridEl.focus({ preventScroll: true });
3248
3870
  this.grid.events.emit("editStop", { rowIndex: cur.rowIndex, colId: cur.colId, committed });
3249
3871
  }
3250
3872
  move(rowIndex, colId, move) {
@@ -3377,7 +3999,7 @@ function startResize(grid, e, colId) {
3377
3999
  grid.renderer.root.classList.add("fg-resizing");
3378
4000
  track(
3379
4001
  (ev) => {
3380
- grid.columns.setWidth(colId, startW + ev.clientX - startX);
4002
+ grid.columns.setWidth(colId, startW + (ev.clientX - startX) * (grid.renderer.rtl ? -1 : 1));
3381
4003
  grid.layoutChanged();
3382
4004
  },
3383
4005
  () => {
@@ -3397,7 +4019,7 @@ function beginDrag(grid, colId, cell, pane) {
3397
4019
  const ghost = el("div", `fg-drag-ghost fg-theme-${grid.store.state.theme}`);
3398
4020
  ghost.textContent = cell.querySelector(".fg-header-label")?.textContent ?? "";
3399
4021
  grid.applyFontTo(ghost);
3400
- document.body.append(ghost);
4022
+ popupParent(grid.renderer.root).append(ghost);
3401
4023
  const marker = el("div", "fg-drop-marker");
3402
4024
  marker.style.height = `${grid.renderer.headerEl.offsetHeight}px`;
3403
4025
  grid.renderer.root.append(marker);
@@ -3412,11 +4034,13 @@ function updateDrag(grid, colId, d, x, y) {
3412
4034
  let t = 0;
3413
4035
  for (const s of d.sibs) {
3414
4036
  const r = s.el.getBoundingClientRect();
3415
- 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++;
3416
4038
  }
3417
4039
  d.target = t;
3418
4040
  const rootLeft = grid.renderer.root.getBoundingClientRect().left;
3419
- 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);
3420
4044
  d.marker.style.left = `${edge - rootLeft - 1}px`;
3421
4045
  }
3422
4046
  function finishDrag(grid, colId, d) {
@@ -3467,6 +4091,7 @@ function attachHeader(grid) {
3467
4091
  const cell = target.closest(".fg-header-cell");
3468
4092
  const colId = cell?.dataset.col;
3469
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;
3470
4095
  e.preventDefault();
3471
4096
  if (target.closest(".fg-resize")) startResize(grid, e, colId);
3472
4097
  else startPress(grid, e, colId, cell);
@@ -3502,16 +4127,16 @@ function toCsv(rows, sep = ",") {
3502
4127
  };
3503
4128
  return rows.map((r) => r.map(cell).join(sep)).join("\r\n");
3504
4129
  }
3505
- function detectSeparator(text) {
3506
- const line = text.slice(0, text.search(/\r?\n|$/));
4130
+ function detectSeparator(text2) {
4131
+ const line = text2.slice(0, text2.search(/\r?\n|$/));
3507
4132
  const count = (ch) => line.split(ch).length - 1;
3508
4133
  const tabs = count(" "), semis = count(";"), commas = count(",");
3509
4134
  if (tabs && tabs >= commas && tabs >= semis) return " ";
3510
4135
  if (semis > commas) return ";";
3511
4136
  return ",";
3512
4137
  }
3513
- function parseCsv(text, sep = detectSeparator(text)) {
3514
- return parseDelimited(text.replace(/^\uFEFF/, ""), sep);
4138
+ function parseCsv(text2, sep = detectSeparator(text2)) {
4139
+ return parseDelimited(text2.replace(/^\uFEFF/, ""), sep);
3515
4140
  }
3516
4141
 
3517
4142
  // src/export/zip.ts
@@ -3660,9 +4285,42 @@ function writeXlsx(sheets) {
3660
4285
  const fmtIds = /* @__PURE__ */ new Map([["General", 0]]);
3661
4286
  const customFmts = [];
3662
4287
  const xfs = ['<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>'];
3663
- const xfIndex = /* @__PURE__ */ new Map([["General|0", 0]]);
3664
- const styleOf = (fmt, bold) => {
3665
- 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}`;
3666
4324
  let i = xfIndex.get(key);
3667
4325
  if (i !== void 0) return i;
3668
4326
  let id = fmtIds.get(fmt);
@@ -3671,8 +4329,11 @@ function writeXlsx(sheets) {
3671
4329
  customFmts.push(fmt);
3672
4330
  fmtIds.set(fmt, id);
3673
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"' : "");
3674
4335
  i = xfs.length;
3675
- 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}/>`);
3676
4337
  xfIndex.set(key, i);
3677
4338
  return i;
3678
4339
  };
@@ -3691,7 +4352,7 @@ function writeXlsx(sheets) {
3691
4352
  v = toSerial(v);
3692
4353
  }
3693
4354
  }
3694
- const s = styleOf(fmt, !!o.bold);
4355
+ const s = styleOf(fmt, o);
3695
4356
  const sa = s ? ` s="${s}"` : "";
3696
4357
  if (v === null || v === void 0 || v === "") return sa ? `<c r="${ref}"${sa}/>` : "";
3697
4358
  if (typeof v === "number") return Number.isFinite(v) ? `<c r="${ref}"${sa}><v>${v}</v></c>` : "";
@@ -3708,7 +4369,7 @@ function writeXlsx(sheets) {
3708
4369
  const merges = sheet.merges?.length ? `<mergeCells count="${sheet.merges.length}">${sheet.merges.map((m) => `<mergeCell ref="${m}"/>`).join("")}</mergeCells>` : "";
3709
4370
  return `${XML}<worksheet xmlns="${NS}" xmlns:r="${REL_NS}">${views}<sheetFormatPr defaultRowHeight="15"/>${cols}<sheetData>${rowsXml}</sheetData>${filter}${merges}</worksheet>`;
3710
4371
  });
3711
- 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>`;
3712
4373
  const files = [
3713
4374
  {
3714
4375
  name: "[Content_Types].xml",
@@ -3862,6 +4523,51 @@ function excelFormat(def) {
3862
4523
  if (o.style === "percent") return `0${decimals}%`;
3863
4524
  return `${o.useGrouping === false ? "0" : "#,##0"}${decimals}`;
3864
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
+ }
3865
4571
  function buildXlsx(grid, opts = {}) {
3866
4572
  const cols = exportColumns(grid, opts.columns);
3867
4573
  const code = grid.store.state.locale.code;
@@ -3888,43 +4594,86 @@ function buildXlsx(grid, opts = {}) {
3888
4594
  if (level < leafLevel) {
3889
4595
  const g = chain[level];
3890
4596
  const prev = ci > 0 ? chains[ci - 1][level] : null;
3891
- 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 });
3892
4598
  if (prev !== g) {
3893
4599
  let end = ci;
3894
4600
  while (end + 1 < cols.length && chains[end + 1][level] === g) end++;
3895
4601
  if (end > ci) merges.push(`${name(ci)}${level + 1}:${name(end)}${level + 1}`);
3896
4602
  }
3897
4603
  } else if (level === leafLevel) {
3898
- 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 });
3899
4605
  if (headRows - 1 > level) merges.push(`${name(ci)}${level + 1}:${name(ci)}${headRows}`);
3900
- } else row.push({ v: null, bold: true });
4606
+ } else row.push({ v: null, header: true });
3901
4607
  }
3902
4608
  rows.push(row);
3903
4609
  }
3904
4610
  }
3905
4611
  const fmts = cols.map((c) => excelFormat(c.def));
3906
4612
  const formatters = cols.map((c) => getFormatter(c.def, code));
3907
- for (const r of exportRows(grid, opts.rows)) {
3908
- rows.push(cols.map((c, i) => {
3909
- const v = getCellValue(c.def, r);
3910
- if (isBlank(v)) return null;
3911
- const fmt = fmts[i] ?? void 0;
3912
- switch (c.def.type) {
3913
- case "number": {
3914
- const n = typeof v === "number" ? v : Number(v);
3915
- return Number.isFinite(n) ? { v: n, fmt } : String(v);
3916
- }
3917
- case "date": {
3918
- const d = toDate(v);
3919
- return d ? { v: d, fmt } : String(v);
3920
- }
3921
- case "boolean":
3922
- return typeof v === "boolean" ? v : String(v);
3923
- default:
3924
- 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);
3925
4623
  }
3926
- }));
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();
3927
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
+ });
3928
4677
  return writeXlsx([{
3929
4678
  name: opts.sheetName ?? "Sheet1",
3930
4679
  rows,
@@ -3967,8 +4716,8 @@ async function importInto(grid, file, opts = {}) {
3967
4716
  if (!s) throw new Error(`Sheet ${String(opts.sheet)} not found`);
3968
4717
  matrix = s.rows;
3969
4718
  } else {
3970
- const text = new TextDecoder().decode(bytes);
3971
- matrix = parseCsv(text, detectSeparator(text));
4719
+ const text2 = new TextDecoder().decode(bytes);
4720
+ matrix = parseCsv(text2, detectSeparator(text2));
3972
4721
  }
3973
4722
  if (!matrix.length) return { rows: [], skipped: 0 };
3974
4723
  const leaves = [];
@@ -4030,9 +4779,9 @@ function svg(tag, attrs = {}, cls = "") {
4030
4779
  for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
4031
4780
  return node;
4032
4781
  }
4033
- function tip(node, text) {
4782
+ function tip(node, text2) {
4034
4783
  const t = svg("title");
4035
- t.textContent = text;
4784
+ t.textContent = text2;
4036
4785
  node.append(t);
4037
4786
  }
4038
4787
  function niceTicks(min, max, count = 5) {
@@ -4468,12 +5217,13 @@ function buildPivot(source, config, defOf, labels, locale) {
4468
5217
  }
4469
5218
 
4470
5219
  // src/version.ts
4471
- var version = "0.7.0";
5220
+ var version = "0.8.0";
4472
5221
 
4473
5222
  // src/license.ts
4474
5223
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
4475
5224
  var PRICING_URL = "https://freegrid.app/pricing/";
4476
5225
  var USAGE_URL = "https://freegrid.app/api/usage";
5226
+ var STATUS_URL = "https://freegrid.app/api/domain-status";
4477
5227
  var b64urlDecode = (s) => {
4478
5228
  const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
4479
5229
  return Uint8Array.from(bin, (c) => c.charCodeAt(0));
@@ -4515,7 +5265,8 @@ async function verifyLicense(key, host, opts = {}) {
4515
5265
  return { ok: true, info };
4516
5266
  }
4517
5267
  var globalKey;
4518
- var reported = /* @__PURE__ */ new Set();
5268
+ var statuses = /* @__PURE__ */ new Map();
5269
+ var badgeCheck = null;
4519
5270
  function setLicenseKey(key) {
4520
5271
  globalKey = key;
4521
5272
  }
@@ -4542,22 +5293,52 @@ function reportUsage(info, host = typeof location !== "undefined" ? location.hos
4542
5293
  }
4543
5294
  }
4544
5295
  function checkLicense(key = globalKey, host = typeof location !== "undefined" ? location.hostname : "") {
5296
+ if (isDevHost(host)) return Promise.resolve(true);
4545
5297
  const k = key ?? globalKey;
4546
5298
  const id = `${k ?? ""}@${host}`;
4547
- if (reported.has(id) || isDevHost(host)) return Promise.resolve();
4548
- reported.add(id);
5299
+ let status = statuses.get(id);
5300
+ if (status) return status;
4549
5301
  if (!k) {
4550
5302
  console.info(NOTICE.missing);
4551
5303
  reportUsage({ version, licensed: false }, host);
4552
- return Promise.resolve();
4553
- }
4554
- return verifyLicense(k, host).then((res) => {
4555
- reportUsage({ version, licensed: res.ok, company: res.ok ? res.info.company : void 0 }, host);
4556
- if (res.ok || res.reason === "unsupported") return;
4557
- const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.expires})` : "";
4558
- console.warn(`${NOTICE[res.reason]}${extra}
5304
+ status = Promise.resolve(false);
5305
+ } else {
5306
+ status = verifyLicense(k, host).then((res) => {
5307
+ reportUsage({ version, licensed: res.ok, company: res.ok ? res.info.company : void 0 }, host);
5308
+ if (res.ok || res.reason === "unsupported") return res.ok || res.reason === "unsupported";
5309
+ const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.expires})` : "";
5310
+ console.warn(`${NOTICE[res.reason]}${extra}
4559
5311
  ${PRICING_URL}`);
4560
- }, () => void 0);
5312
+ return false;
5313
+ }, () => false);
5314
+ }
5315
+ statuses.set(id, status);
5316
+ return status;
5317
+ }
5318
+ function badgeRequested(host = typeof location !== "undefined" ? location.hostname : "") {
5319
+ if (isDevHost(host) || typeof fetch !== "function") return Promise.resolve(false);
5320
+ if (badgeCheck) return badgeCheck;
5321
+ const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5322
+ let store = null;
5323
+ try {
5324
+ store = typeof localStorage !== "undefined" ? localStorage : null;
5325
+ const cached = JSON.parse(store?.getItem("freegrid:badge") ?? "null");
5326
+ if (cached && cached.host === host && cached.day === day) return badgeCheck = Promise.resolve(cached.show);
5327
+ } catch {
5328
+ }
5329
+ badgeCheck = fetch(STATUS_URL, { mode: "cors", credentials: "omit" }).then((r) => r.ok ? r.json() : { badge: false }).then((d) => {
5330
+ const show = d.badge === true;
5331
+ try {
5332
+ store?.setItem("freegrid:badge", JSON.stringify({ host, day, show }));
5333
+ } catch {
5334
+ }
5335
+ return show;
5336
+ }, () => false);
5337
+ return badgeCheck;
5338
+ }
5339
+ async function needsFreeBadge(key, host = typeof location !== "undefined" ? location.hostname : "") {
5340
+ if (await checkLicense(key, host)) return false;
5341
+ return badgeRequested(host);
4561
5342
  }
4562
5343
 
4563
5344
  // src/interactions/menu.ts
@@ -4617,7 +5398,7 @@ var ColumnMenu = class {
4617
5398
  popup.append(this.section(items));
4618
5399
  const filter = this.filterSection(colId, () => this.close());
4619
5400
  if (filter) popup.append(filter);
4620
- document.body.append(popup);
5401
+ popupParent(this.grid.renderer.root).append(popup);
4621
5402
  const r = anchor.getBoundingClientRect();
4622
5403
  const w = popup.offsetWidth || 248;
4623
5404
  popup.style.left = `${Math.max(4, Math.min(r.right - w, window.innerWidth - w - 4))}px`;
@@ -4627,13 +5408,12 @@ var ColumnMenu = class {
4627
5408
  this.popup = popup;
4628
5409
  this.colId = colId;
4629
5410
  const onDown = (e) => {
4630
- const t = e.target;
4631
- if (!popup.contains(t) && !anchor.contains(t)) this.close();
5411
+ if (!eventInside(e, popup, anchor)) this.close();
4632
5412
  };
4633
5413
  const onKey = (e) => {
4634
5414
  if (e.key !== "Escape") return;
4635
5415
  this.close();
4636
- this.grid.renderer.root.focus();
5416
+ this.grid.renderer.gridEl.focus();
4637
5417
  };
4638
5418
  document.addEventListener("pointerdown", onDown, true);
4639
5419
  document.addEventListener("keydown", onKey, true);
@@ -4897,19 +5677,26 @@ var RowModel = class {
4897
5677
  if (!this.indexById) this.indexById = new Map(this.display.map((n, i) => [n.id, i]));
4898
5678
  return this.indexById.get(id) ?? -1;
4899
5679
  }
4900
- /** Prefix-sum offsets of the display rows (detail rows use `detailHeight`). */
4901
- layout(rowHeight, detailHeight) {
5680
+ /**
5681
+ * Prefix-sum offsets of the display rows: detail rows use `detailHeight`, rows with a
5682
+ * measured height (auto row height) use it, everything else `rowHeight`.
5683
+ */
5684
+ layout(rowHeight, detailHeight, heights, heightsVersion = 0) {
4902
5685
  const c = this.layoutCache;
4903
- if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight) return c;
5686
+ if (c && c.rows === this.display && c.rh === rowHeight && c.dh === detailHeight && c.hv === heightsVersion) return c;
4904
5687
  const n = this.display.length;
4905
5688
  const offsets = new Float64Array(n + 1);
4906
5689
  let uniform = true;
5690
+ const measured = heights && heights.size ? heights : null;
4907
5691
  for (let i = 0; i < n; i++) {
4908
- const detail = this.display[i].kind === "detail";
4909
- if (detail) uniform = false;
4910
- offsets[i + 1] = offsets[i] + (detail ? detailHeight : rowHeight);
4911
- }
4912
- this.layoutCache = { rows: this.display, rh: rowHeight, dh: detailHeight, offsets, uniform };
5692
+ const node = this.display[i];
5693
+ let h = rowHeight;
5694
+ if (node.kind === "detail") h = detailHeight;
5695
+ else if (measured && !node.kind) h = measured.get(node.id) ?? rowHeight;
5696
+ if (h !== rowHeight) uniform = false;
5697
+ offsets[i + 1] = offsets[i] + h;
5698
+ }
5699
+ this.layoutCache = { rows: this.display, rh: rowHeight, dh: detailHeight, hv: heightsVersion, offsets, uniform };
4913
5700
  return this.layoutCache;
4914
5701
  }
4915
5702
  filter(input) {
@@ -4920,8 +5707,8 @@ var RowModel = class {
4920
5707
  const matches = this.nodes.filter((n) => {
4921
5708
  for (const p of preds) if (!p.pred(getCellValue(p.def, n.data))) return false;
4922
5709
  if (tokens.length) {
4923
- const text = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
4924
- if (!matchesQuick(tokens, text)) return false;
5710
+ const text2 = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
5711
+ if (!matchesQuick(tokens, text2)) return false;
4925
5712
  }
4926
5713
  return true;
4927
5714
  });
@@ -5016,8 +5803,10 @@ var RowModel = class {
5016
5803
  for (const n of nodes) {
5017
5804
  if (seen2.has(n.id)) continue;
5018
5805
  const kids = children.get(n.id) ?? [];
5806
+ const lazy = !kids.length && !!d.lazyChildren?.(n.id, n.data);
5019
5807
  const expanded = kids.length > 0 && this.isOpen(d, n.id, level);
5020
- const node = { ...n, level, hasChildren: kids.length > 0, expanded };
5808
+ const node = { ...n, level, hasChildren: kids.length > 0 || lazy, expanded };
5809
+ if (lazy && d.loading?.has(n.id)) node.loading = true;
5021
5810
  this.withDetails(d, [node], out);
5022
5811
  if (expanded) visit(kids, level + 1, /* @__PURE__ */ new Set([...seen2, n.id]));
5023
5812
  }
@@ -5285,6 +6074,17 @@ var Grid = class {
5285
6074
  /** 0-based page and rows per page (pagination only). */
5286
6075
  this.page = 0;
5287
6076
  this.pageSize = DEFAULT_PAGE_SIZE;
6077
+ /** Tree rows whose children were loaded (or are loading) through treeData.loadChildren. */
6078
+ this.treeLoaded = /* @__PURE__ */ new Set();
6079
+ this.treeLoading = /* @__PURE__ */ new Set();
6080
+ this.lazyChildren = (id, row) => {
6081
+ if (this.treeLoaded.has(id)) return false;
6082
+ try {
6083
+ return !!this.options.treeData?.hasChildren?.(row);
6084
+ } catch {
6085
+ return false;
6086
+ }
6087
+ };
5288
6088
  if (!Array.isArray(options.columns)) warn("options.columns must be an array");
5289
6089
  if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
5290
6090
  this.store = new Store({
@@ -5296,6 +6096,7 @@ var Grid = class {
5296
6096
  anchorId: null,
5297
6097
  focus: null,
5298
6098
  range: null,
6099
+ extraRanges: [],
5299
6100
  locale: resolveLocale(options.locale),
5300
6101
  theme: options.theme ?? "light"
5301
6102
  });
@@ -5367,9 +6168,12 @@ var Grid = class {
5367
6168
  this.disposers.push(() => ro.disconnect());
5368
6169
  }
5369
6170
  if (options.pivot) this.setPivot(options.pivot);
5370
- void checkLicense(options.licenseKey);
6171
+ void needsFreeBadge(options.licenseKey).then((show) => {
6172
+ if (show && !this.destroyed) this.renderer.showFreeBadge();
6173
+ });
5371
6174
  this.renderer.render();
5372
6175
  this.api = this.createApi();
6176
+ liveGrids.set(this.renderer.root, this);
5373
6177
  }
5374
6178
  collator() {
5375
6179
  const code = this.store.state.locale.code;
@@ -5380,6 +6184,7 @@ var Grid = class {
5380
6184
  }
5381
6185
  /** Column widths/order/pinning changed; rows are unaffected. */
5382
6186
  layoutChanged() {
6187
+ this.renderer.invalidateHeights();
5383
6188
  this.renderer.invalidate({ header: true, rows: true });
5384
6189
  this.refreshChoosers();
5385
6190
  }
@@ -5400,6 +6205,8 @@ var Grid = class {
5400
6205
  defaultExpanded: this.defaultExpanded,
5401
6206
  detailOpen: this.options.detailRenderer ? this.detailOpen : /* @__PURE__ */ new Set(),
5402
6207
  isMaster: this.options.isRowMaster,
6208
+ lazyChildren: this.options.treeData?.hasChildren ? this.lazyChildren : void 0,
6209
+ loading: this.treeLoading,
5403
6210
  version: this.displayVersion
5404
6211
  };
5405
6212
  }
@@ -5517,6 +6324,7 @@ var Grid = class {
5517
6324
  if (!this.options.pagination || !Number.isFinite(page)) return;
5518
6325
  const next = Math.max(0, Math.min(this.getPageCount() - 1, Math.floor(page)));
5519
6326
  if (next === this.page) return;
6327
+ if (this.veto("beforePageChange", { page: next, pageSize: this.pageSize })) return;
5520
6328
  this.page = next;
5521
6329
  this.pageMoved();
5522
6330
  }
@@ -5528,6 +6336,7 @@ var Grid = class {
5528
6336
  }
5529
6337
  if (size === this.pageSize) return;
5530
6338
  const first = this.page * this.pageSize;
6339
+ if (this.veto("beforePageChange", { page: Math.floor(first / size), pageSize: size })) return;
5531
6340
  this.pageSize = size;
5532
6341
  this.page = Math.floor(first / size);
5533
6342
  this.pageMoved();
@@ -5567,6 +6376,35 @@ var Grid = class {
5567
6376
  this.renderer.invalidate({ rows: true });
5568
6377
  this.events.emit("rowsLoadFailed", { start, end, error });
5569
6378
  }
6379
+ /**
6380
+ * Scroll `other` along with this grid and back. The follower's echo of a position we set is not
6381
+ * sent back, so a shorter grid that stops at its end does not pull the longer one back.
6382
+ */
6383
+ syncScroll(other, axis) {
6384
+ if (!other || typeof other.setScrollPosition !== "function" || other === this.api) return () => void 0;
6385
+ const written = /* @__PURE__ */ new Map();
6386
+ const key = (p) => `${p.top},${p.left}`;
6387
+ const link = (from, to) => from.on("scroll", (p) => {
6388
+ if (written.get(from) === key(p)) {
6389
+ written.delete(from);
6390
+ return;
6391
+ }
6392
+ to.setScrollPosition(axis === "vertical" ? { top: p.top } : axis === "horizontal" ? { left: p.left } : p);
6393
+ written.set(to, key(to.getScrollPosition()));
6394
+ });
6395
+ const offs = [link(this.api, other), link(other, this.api)];
6396
+ return () => {
6397
+ for (const off of offs) off();
6398
+ };
6399
+ }
6400
+ /** Emit a before-event; true when a listener called cancel(). */
6401
+ veto(type, payload) {
6402
+ let cancelled = false;
6403
+ this.events.emit(type, { ...payload, cancel: () => {
6404
+ cancelled = true;
6405
+ } });
6406
+ return cancelled;
6407
+ }
5570
6408
  /** Client-only operations log once and do nothing with a datasource. */
5571
6409
  clientOnly(what) {
5572
6410
  if (!this.rowModel.server) return false;
@@ -5616,10 +6454,45 @@ var Grid = class {
5616
6454
  expandKey(node) {
5617
6455
  return node.group ? node.group.key : node.id;
5618
6456
  }
6457
+ /** First expand of a lazy tree row: fetch its children, then show them expanded. */
6458
+ loadChildren(node) {
6459
+ const tree = this.options.treeData;
6460
+ const id = node.id;
6461
+ if (this.treeLoading.has(id)) return;
6462
+ if (typeof tree.loadChildren !== "function") {
6463
+ warn("treeData.hasChildren needs treeData.loadChildren");
6464
+ return;
6465
+ }
6466
+ this.treeLoading.add(id);
6467
+ this.displayChanged();
6468
+ const done = () => {
6469
+ this.treeLoading.delete(id);
6470
+ return !this.destroyed && this.store.state.data.includes(node.data);
6471
+ };
6472
+ Promise.resolve().then(() => tree.loadChildren(node.data)).then((rows) => {
6473
+ if (!done()) return;
6474
+ const kids = cleanRows(Array.isArray(rows) ? rows : []);
6475
+ this.treeLoaded.add(id);
6476
+ this.expanded.set(id, true);
6477
+ if (kids.length) this.replaceData([...this.store.state.data, ...kids]);
6478
+ this.displayChanged();
6479
+ this.events.emit("childrenLoaded", { row: node.data, rows: kids });
6480
+ this.events.emit("groupToggle", { key: id, expanded: true });
6481
+ }, (error) => {
6482
+ if (!done()) return;
6483
+ warn(`treeData.loadChildren failed for row "${id}"`);
6484
+ this.displayChanged();
6485
+ this.events.emit("childrenLoadFailed", { row: node.data, error });
6486
+ });
6487
+ }
5619
6488
  setGroupExpanded(key, open) {
5620
6489
  const node = this.rowModel.rows().find((n) => this.isExpandable(n) && this.expandKey(n) === key);
5621
6490
  const cur = node ? !!(node.group?.expanded ?? node.expanded) : this.expanded.get(key);
5622
6491
  if (cur === open) return;
6492
+ if (open && node && !node.group && this.options.treeData && this.lazyChildren(node.id, node.data)) {
6493
+ this.loadChildren(node);
6494
+ return;
6495
+ }
5623
6496
  this.expanded.set(key, open);
5624
6497
  this.displayChanged();
5625
6498
  this.events.emit("groupToggle", { key, expanded: open });
@@ -5678,7 +6551,113 @@ var Grid = class {
5678
6551
  /** Row order can only follow the source data when nothing reorders or hides rows. */
5679
6552
  canMoveRows() {
5680
6553
  const s = this.store.state;
5681
- return !this.rowModel.server && !s.sort.length && !Object.keys(s.filters).length && !s.quickFilter.trim() && !this.groupBy.length && !this.options.treeData;
6554
+ const tree = this.options.treeData;
6555
+ return !this.rowModel.server && !s.sort.length && !Object.keys(s.filters).length && !s.quickFilter.trim() && !this.groupBy.length && (!tree || typeof tree.setParentId === "function");
6556
+ }
6557
+ get dragOptions() {
6558
+ const o = this.options.rowDrag;
6559
+ return o && typeof o === "object" ? o : {};
6560
+ }
6561
+ parentOf(row) {
6562
+ let p = null;
6563
+ try {
6564
+ p = this.options.treeData.getParentId(row);
6565
+ } catch {
6566
+ p = null;
6567
+ }
6568
+ return p == null ? null : String(p);
6569
+ }
6570
+ rawParent(row) {
6571
+ try {
6572
+ return this.options.treeData.getParentId(row) ?? null;
6573
+ } catch {
6574
+ return null;
6575
+ }
6576
+ }
6577
+ /** The id of `row` as the data stores parent ids: a number when other rows hold numeric parent ids. */
6578
+ idValue(row) {
6579
+ const id = this.rowModel.idOf(row);
6580
+ for (const r of this.store.state.data) {
6581
+ const p = this.rawParent(r);
6582
+ if (p !== null && String(p) === id) return p;
6583
+ }
6584
+ const numeric = this.store.state.data.some((r) => typeof this.rawParent(r) === "number");
6585
+ return numeric && id.trim() !== "" && Number.isFinite(Number(id)) ? Number(id) : id;
6586
+ }
6587
+ /** Can `row` land here? Trees refuse the row's own subtree; then the user's canDrop rule decides. */
6588
+ canDropRow(row, target, position, from = this) {
6589
+ if (!this.canMoveRows() || target === row) return false;
6590
+ if (from === this && this.options.treeData && target) {
6591
+ const own = this.rowModel.idOf(row);
6592
+ const seen2 = /* @__PURE__ */ new Set();
6593
+ for (let id = this.rowModel.idOf(target); id !== null && !seen2.has(id); ) {
6594
+ if (id === own) return false;
6595
+ seen2.add(id);
6596
+ const node = this.rowModel.allNodes().find((n) => n.id === id);
6597
+ id = node ? this.parentOf(node.data) : null;
6598
+ }
6599
+ }
6600
+ const rule = this.dragOptions.canDrop;
6601
+ if (!rule) return true;
6602
+ try {
6603
+ return rule({ row, target, position, fromOtherGrid: from !== this }) !== false;
6604
+ } catch {
6605
+ warn("rowDrag.canDrop threw; the drop was refused");
6606
+ return false;
6607
+ }
6608
+ }
6609
+ /**
6610
+ * Move `row` before, after or inside `target` (null = to the end). In tree data the row takes the
6611
+ * target's parent, or the target itself for 'inside'. `from` is the grid the row is dragged out of.
6612
+ */
6613
+ dropRow(row, target, position = "after", from = this) {
6614
+ if (this.clientOnly("dropRow") || !row || typeof row !== "object") return false;
6615
+ const tree = this.options.treeData;
6616
+ if (!tree && position === "inside") position = "after";
6617
+ const data = this.store.state.data;
6618
+ const external = from !== this;
6619
+ const fromIndex = external ? -1 : data.indexOf(row);
6620
+ if (!external && fromIndex < 0) return false;
6621
+ if (target && !data.includes(target)) return false;
6622
+ if (external && (!from.canMoveRows() || data.includes(row))) return false;
6623
+ if (!this.canDropRow(row, target, position, from)) return false;
6624
+ const oldRaw = tree ? this.rawParent(row) : null;
6625
+ const parentRow = !tree || !target ? null : position === "inside" ? target : this.store.state.data.find((r) => this.rowModel.idOf(r) === this.parentOf(target)) ?? null;
6626
+ const newRaw = parentRow ? this.idValue(parentRow) : null;
6627
+ const next = data.filter((r) => r !== row);
6628
+ const at = !target || position === "inside" ? next.length : next.indexOf(target) + (position === "after" ? 1 : 0);
6629
+ next.splice(at, 0, row);
6630
+ const moved = next.some((r, i) => r !== data[i]);
6631
+ if (!external && !moved && String(oldRaw ?? "") === String(newRaw ?? "")) return false;
6632
+ const setParent = (raw, parent) => {
6633
+ try {
6634
+ tree.setParentId(row, raw, parent);
6635
+ } catch {
6636
+ warn("treeData.setParentId threw");
6637
+ }
6638
+ };
6639
+ const oldParentRow = tree ? this.store.state.data.find((r) => this.rowModel.idOf(r) === this.parentOf(row)) ?? null : null;
6640
+ if (external) {
6641
+ from.removeRows([row]);
6642
+ if (tree) setParent(newRaw, parentRow);
6643
+ this.addRows([row], at);
6644
+ } else {
6645
+ if (tree) setParent(newRaw, parentRow);
6646
+ this.replaceData(next);
6647
+ this.history.push({
6648
+ undo: () => {
6649
+ if (tree) setParent(oldRaw, oldParentRow);
6650
+ this.replaceData(data);
6651
+ },
6652
+ redo: () => {
6653
+ if (tree) setParent(newRaw, parentRow);
6654
+ this.replaceData(next);
6655
+ }
6656
+ });
6657
+ this.events.emit("rowDragEnd", { row, fromIndex, toIndex: at });
6658
+ }
6659
+ this.events.emit("rowDrop", { row, target, position, fromOtherGrid: external, fromGrid: external ? from.api : null });
6660
+ return true;
5682
6661
  }
5683
6662
  moveRow(row, toIndex, record = true) {
5684
6663
  if (this.clientOnly("moveRow")) return false;
@@ -5708,6 +6687,7 @@ var Grid = class {
5708
6687
  if (this.options.rowHeight == null) this.renderer.opts.rowHeight = autoRowHeight(effective);
5709
6688
  if (this.options.headerHeight == null) this.renderer.opts.headerHeight = autoHeaderHeight(effective);
5710
6689
  this.renderer.applyFont(size, family);
6690
+ this.renderer.invalidateHeights();
5711
6691
  this.menu.close();
5712
6692
  this.layoutChanged();
5713
6693
  }
@@ -5743,30 +6723,55 @@ var Grid = class {
5743
6723
  dataColumns() {
5744
6724
  return this.columns.displayed().filter((c) => !isSystemCol(c.id));
5745
6725
  }
5746
- setRange(range) {
5747
- let next = null;
5748
- if (range && this.options.rangeSelection !== false) {
5749
- const count = this.rowModel.getRowCount();
5750
- const ok = (p) => !isSystemCol(p.colId) && !!this.columns.getColumn(p.colId) && p.rowIndex >= 0 && p.rowIndex < count;
5751
- if (ok(range.start) && ok(range.end)) next = { start: { ...range.start }, end: { ...range.end } };
5752
- }
6726
+ validRange(range) {
6727
+ if (!range || this.options.rangeSelection === false) return null;
6728
+ const count = this.rowModel.getRowCount();
6729
+ const ok = (p) => !!p && !isSystemCol(p.colId) && !!this.columns.getColumn(p.colId) && p.rowIndex >= 0 && p.rowIndex < count;
6730
+ return ok(range.start) && ok(range.end) ? { start: { ...range.start }, end: { ...range.end } } : null;
6731
+ }
6732
+ /** Set the active range. Earlier Ctrl ranges are dropped unless `keepOthers`. */
6733
+ setRange(range, keepOthers = false) {
6734
+ const next = this.validRange(range);
6735
+ const extras = keepOthers || !this.store.state.extraRanges.length ? this.store.state.extraRanges : [];
5753
6736
  const cur = this.store.state.range;
5754
- if (JSON.stringify(cur) === JSON.stringify(next)) return;
5755
- this.store.set({ range: next });
6737
+ if (JSON.stringify(cur) === JSON.stringify(next) && extras === this.store.state.extraRanges) return;
6738
+ this.store.set({ range: next, extraRanges: extras });
5756
6739
  this.events.emit("rangeChange", { range: next && { start: { ...next.start }, end: { ...next.end } } });
5757
6740
  }
5758
- /** Range in displayed column indexes, or null. */
5759
- rangeBounds() {
5760
- const r = this.store.state.range;
6741
+ /** Ctrl+click / Ctrl+drag: keep what is selected (the focused cell when there is no range) and start a new range. */
6742
+ addRange(range) {
6743
+ const { range: active, focus } = this.store.state;
6744
+ const keep = this.validRange(active ?? (focus ? { start: focus, end: focus } : null));
6745
+ if (keep) this.store.set({ extraRanges: [...this.store.state.extraRanges, keep] });
6746
+ this.setRange(range, true);
6747
+ }
6748
+ getRanges() {
6749
+ const all2 = [...this.store.state.extraRanges];
6750
+ if (this.store.state.range) all2.push(this.store.state.range);
6751
+ return all2.map((r) => ({ start: { ...r.start }, end: { ...r.end } }));
6752
+ }
6753
+ setRanges(ranges) {
6754
+ const list = (Array.isArray(ranges) ? ranges : []).map((r) => this.validRange(r)).filter((r) => !!r);
6755
+ this.store.set({ extraRanges: list.slice(0, -1) });
6756
+ this.setRange(list[list.length - 1] ?? null, true);
6757
+ }
6758
+ boundsOf(r) {
5761
6759
  if (!r) return null;
5762
6760
  const a = this.columns.getColumn(r.start.colId), b = this.columns.getColumn(r.end.colId);
5763
6761
  if (!a || !b) return null;
5764
6762
  return normalizeRange({ start: { rowIndex: r.start.rowIndex, colIndex: a.index }, end: { rowIndex: r.end.rowIndex, colIndex: b.index } });
5765
6763
  }
6764
+ /** Active range in displayed column indexes, or null. */
6765
+ rangeBounds() {
6766
+ return this.boundsOf(this.store.state.range);
6767
+ }
6768
+ /** Every range (Ctrl ranges first, the active one last). */
6769
+ allRangeBounds() {
6770
+ return [...this.store.state.extraRanges, this.store.state.range].map((r) => this.boundsOf(r)).filter((b) => !!b);
6771
+ }
5766
6772
  rangeContains(rowIndex, colId) {
5767
- const b = this.rangeBounds();
5768
6773
  const col = this.columns.getColumn(colId);
5769
- return !!b && !!col && rangeContains(b, rowIndex, col.index);
6774
+ return !!col && this.allRangeBounds().some((b) => rangeContains(b, rowIndex, col.index));
5770
6775
  }
5771
6776
  selectAllCells() {
5772
6777
  const cols = this.dataColumns();
@@ -5784,6 +6789,18 @@ var Grid = class {
5784
6789
  const checkedRows = rows;
5785
6790
  let out;
5786
6791
  let cols;
6792
+ const all2 = rows ? [] : this.allRangeBounds();
6793
+ if (all2.length > 1) {
6794
+ const sameCols = all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2);
6795
+ const sameRows = all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2);
6796
+ if (sameCols || sameRows) {
6797
+ const parts = [...all2].sort((x, y) => sameCols ? x.r1 - y.r1 : x.c1 - y.c1);
6798
+ const blocks = parts.map((p) => this.rangeMatrix(p));
6799
+ const matrix2 = sameCols ? blocks.flat() : blocks[0].map((_, ri) => blocks.flatMap((bl) => bl[ri] ?? []));
6800
+ if (headers) matrix2.unshift((sameCols ? parts.slice(0, 1) : parts).flatMap((p) => this.rangeHeaders(p)));
6801
+ return toTSV(matrix2);
6802
+ }
6803
+ }
5787
6804
  if (b) {
5788
6805
  out = [];
5789
6806
  for (let i = b.r1; i <= b.r2; i++) {
@@ -5803,22 +6820,51 @@ var Grid = class {
5803
6820
  out = [node.data];
5804
6821
  cols = [col];
5805
6822
  }
5806
- const matrix = out.map((r) => cols.map((c) => copyValue(c.def, getCellValue(c.def, r), code)));
6823
+ const matrix = out.map((r) => cols.map((c) => this.copyCell(c.def, r, code)));
5807
6824
  if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
5808
6825
  return toTSV(matrix);
5809
6826
  }
6827
+ rangeHeaders(b) {
6828
+ return this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id)).map((c) => c.def.header ?? c.def.field ?? c.id);
6829
+ }
6830
+ /** Copy text of one range. */
6831
+ rangeMatrix(b) {
6832
+ const cols = this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
6833
+ const code = this.store.state.locale.code;
6834
+ const out = [];
6835
+ for (let i = b.r1; i <= b.r2; i++) {
6836
+ const node = this.rowModel.getRow(i);
6837
+ if (node && !node.kind) out.push(cols.map((c) => this.copyCell(c.def, node.data, code)));
6838
+ }
6839
+ return out;
6840
+ }
6841
+ copyCell(def, row, code) {
6842
+ const value = getCellValue(def, row);
6843
+ const rule = def.copyable;
6844
+ if (rule === false) return "";
6845
+ if (typeof rule === "function") {
6846
+ let ok = false;
6847
+ try {
6848
+ ok = rule({ value, row, column: def }) !== false;
6849
+ } catch {
6850
+ ok = false;
6851
+ }
6852
+ if (!ok) return "";
6853
+ }
6854
+ return copyValue(def, value, code);
6855
+ }
5810
6856
  copyToClipboard(headers, rows = false) {
5811
- const text = this.selectionTSV(headers, rows);
5812
- if (text && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
6857
+ const text2 = this.selectionTSV(headers, rows);
6858
+ if (text2 && typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
5813
6859
  try {
5814
- navigator.clipboard.writeText(text)?.catch?.(() => void 0);
6860
+ navigator.clipboard.writeText(text2)?.catch?.(() => void 0);
5815
6861
  } catch {
5816
6862
  }
5817
6863
  }
5818
- return text;
6864
+ return text2;
5819
6865
  }
5820
- pasteText(text) {
5821
- const matrix = parseTSV(text);
6866
+ pasteText(text2) {
6867
+ const matrix = parseTSV(text2);
5822
6868
  const result = { changed: 0, skipped: 0 };
5823
6869
  if (!matrix.length) return result;
5824
6870
  const displayed = this.columns.displayed();
@@ -5850,7 +6896,7 @@ var Grid = class {
5850
6896
  const raw = single && b ? matrix[0][0] : matrix[dr]?.[dc];
5851
6897
  if (raw === void 0) continue;
5852
6898
  const def = col.def;
5853
- if (!def.editable || !def.field) {
6899
+ if (!def.field || !cellEditable(def, node.data, ri)) {
5854
6900
  result.skipped++;
5855
6901
  continue;
5856
6902
  }
@@ -5873,34 +6919,67 @@ var Grid = class {
5873
6919
  const spec = def.editor;
5874
6920
  if (spec && typeof spec === "object" && spec.type === "select") {
5875
6921
  const opts = (typeof spec.options === "function" ? spec.options(row) : spec.options) ?? [];
5876
- const text = raw.trim().toLowerCase();
5877
- 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);
5878
- if (!hit) return { ok: false };
6922
+ const text2 = raw.trim().toLowerCase();
6923
+ const hit = opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o), disabled: false }).find((o) => String(o.value).toLowerCase() === text2 || (o.label ?? "").toLowerCase() === text2);
6924
+ if (!hit || hit.disabled) return { ok: false };
5879
6925
  value = hit.value;
6926
+ } else if (def.formulas && isFormula(raw.trim())) {
6927
+ value = raw.trim();
5880
6928
  } else {
5881
6929
  const parsed = parseValue(def, raw, oldValue);
5882
6930
  if (!parsed.ok) return parsed;
5883
6931
  value = parsed.value;
5884
6932
  }
5885
- if (def.required && isBlank(value)) return { ok: false };
6933
+ if (this.cellError(def, row, value) !== null) return { ok: false };
6934
+ return { ok: true, value };
6935
+ }
6936
+ /** Why `value` cannot go into this cell (required, unique, validate), or null when it can. */
6937
+ cellError(def, row, value) {
6938
+ const loc = this.store.state.locale;
6939
+ if (def.required && isBlank(value)) return loc.required;
6940
+ if (def.unique && def.field && !isBlank(value)) {
6941
+ const key = value instanceof Date ? value.getTime() : String(value);
6942
+ const field = def.field;
6943
+ if (this.store.state.data.some((r) => {
6944
+ if (r === row) return false;
6945
+ const v = r[field];
6946
+ return !isBlank(v) && (v instanceof Date ? v.getTime() : String(v)) === key;
6947
+ })) return loc.duplicate;
6948
+ }
5886
6949
  if (def.validate) {
6950
+ let res;
5887
6951
  try {
5888
- if (def.validate(value, row) !== true) return { ok: false };
6952
+ res = def.validate(value, row);
5889
6953
  } catch {
5890
- return { ok: false };
6954
+ res = loc.invalidValue;
5891
6955
  }
6956
+ if (res !== true) return typeof res === "string" && res ? res : loc.invalidValue;
5892
6957
  }
5893
- return { ok: true, value };
6958
+ return null;
6959
+ }
6960
+ validateAll() {
6961
+ const out = [];
6962
+ const cols = this.columns.getState().map((st) => ({ id: st.colId, def: this.columns.getNode(st.colId).def })).filter((c) => c.def.field && c.def.editable && (c.def.required || c.def.unique || c.def.validate));
6963
+ this.store.state.data.forEach((row, i) => {
6964
+ const id = this.rowModel.idOf(row);
6965
+ const rowIndex = this.rowModel.indexOfId(id);
6966
+ for (const c of cols) {
6967
+ if (!cellEditable(c.def, row, rowIndex < 0 ? i : rowIndex)) continue;
6968
+ const message = this.cellError(c.def, row, row[c.def.field]);
6969
+ if (message !== null) out.push({ row, rowIndex, colId: c.id, message });
6970
+ }
6971
+ });
6972
+ return out;
5894
6973
  }
5895
6974
  // ── merge, aggregates, fill, pinned rows ────────────────────
5896
6975
  /** Merge blocks for every `merge` column over the displayed rows (memoized). */
5897
6976
  mergeSpans() {
5898
- if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge)) return NO_SPANS;
6977
+ if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan)) return NO_SPANS;
5899
6978
  const rows = this.rowModel.rows();
5900
6979
  const ver = this.editVersion * 1e6 + this.columns.displayed().length;
5901
6980
  if (this.spanCache && this.spanCache.rows === rows && this.spanCache.ver === ver) return this.spanCache.spans;
5902
6981
  const spans = /* @__PURE__ */ new Map();
5903
- const merged = this.columns.displayed().filter((c) => c.def.merge);
6982
+ const merged = this.columns.displayed().filter((c) => c.def.merge || c.def.rowSpan);
5904
6983
  if (merged.length && !rows.some((n) => n.kind)) {
5905
6984
  const data = rows.map((n) => n.data);
5906
6985
  for (const c of merged) spans.set(c.id, computeSpans(data, c.def));
@@ -5910,15 +6989,22 @@ var Grid = class {
5910
6989
  }
5911
6990
  /** Count / sum / min / max over the selected range (numbers only for the math). */
5912
6991
  rangeAggregates() {
5913
- const b = this.rangeBounds();
5914
- if (!b) return null;
5915
- const cols = this.columns.displayed().slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
6992
+ const all2 = this.allRangeBounds();
6993
+ if (!all2.length) return null;
6994
+ const displayed = this.columns.displayed();
5916
6995
  const out = { cells: 0, count: 0, numbers: 0, sum: 0, min: Infinity, max: -Infinity };
5917
- for (let r = b.r1; r <= b.r2; r++) {
6996
+ const seen2 = all2.length > 1 ? /* @__PURE__ */ new Set() : null;
6997
+ for (const b of all2) for (let r = b.r1; r <= b.r2; r++) {
5918
6998
  const node = this.rowModel.getRow(r);
5919
6999
  if (!node || node.kind) continue;
5920
7000
  const row = node.data;
5921
- for (const c of cols) {
7001
+ for (const c of displayed.slice(b.c1, b.c2 + 1)) {
7002
+ if (isSystemCol(c.id)) continue;
7003
+ if (seen2) {
7004
+ const k = `${r},${c.index}`;
7005
+ if (seen2.has(k)) continue;
7006
+ seen2.add(k);
7007
+ }
5922
7008
  out.cells++;
5923
7009
  const v = getCellValue(c.def, row);
5924
7010
  if (isBlank(v)) continue;
@@ -5953,13 +7039,8 @@ var Grid = class {
5953
7039
  const col = displayed[c];
5954
7040
  const node = this.rowModel.getRow(r);
5955
7041
  const def = col?.def;
5956
- if (!node || node.kind || !def?.editable || !def.field || isSystemCol(col.id)) return;
5957
- if (def.required && isBlank(value2)) return;
5958
- try {
5959
- if (def.validate && def.validate(value2, node.data) !== true) return;
5960
- } catch {
5961
- return;
5962
- }
7042
+ if (!node || node.kind || !def?.field || isSystemCol(col.id) || !cellEditable(def, node.data, r)) return;
7043
+ if (this.cellError(def, node.data, value2) !== null) return;
5963
7044
  this.writeCell(changes, node.data, def.field, col.id, r, value2);
5964
7045
  };
5965
7046
  const value = (r, c) => getCellValue(displayed[c].def, this.rowModel.getRow(r).data);
@@ -5993,6 +7074,7 @@ var Grid = class {
5993
7074
  writeCell(changes, row, field, colId, rowIndex, value) {
5994
7075
  const oldValue = row[field];
5995
7076
  if (Object.is(oldValue, value)) return false;
7077
+ if (this.veto("beforeCellChange", { row, rowIndex, colId, oldValue, newValue: value })) return false;
5996
7078
  this.tracker.beforeChange(row);
5997
7079
  row[field] = value;
5998
7080
  changes.push({ row, field, colId, oldValue, newValue: value });
@@ -6014,6 +7096,7 @@ var Grid = class {
6014
7096
  this.afterDataEdit();
6015
7097
  }
6016
7098
  afterDataEdit() {
7099
+ this.renderer.invalidateHeights();
6017
7100
  this.editVersion++;
6018
7101
  this.refreshSummary();
6019
7102
  this.renderer.invalidate({ rows: true });
@@ -6029,17 +7112,17 @@ var Grid = class {
6029
7112
  /** Delete/Backspace: clear editable cells in the range, or the focused cell. */
6030
7113
  clearCells() {
6031
7114
  const displayed = this.columns.displayed();
6032
- const b = this.rangeBounds();
7115
+ const all2 = this.allRangeBounds();
6033
7116
  const f = this.store.state.focus;
6034
7117
  const fi = f ? this.columns.getColumn(f.colId)?.index : void 0;
6035
- const box = b ?? (f && fi !== void 0 ? { r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi } : null);
6036
- if (!box) return;
7118
+ const boxes = all2.length ? all2 : f && fi !== void 0 ? [{ r1: f.rowIndex, r2: f.rowIndex, c1: fi, c2: fi }] : [];
7119
+ if (!boxes.length) return;
6037
7120
  const changes = [];
6038
- for (let r = box.r1; r <= box.r2; r++) {
7121
+ for (const box of boxes) for (let r = box.r1; r <= box.r2; r++) {
6039
7122
  const node = this.rowModel.getRow(r);
6040
7123
  for (let c = box.c1; c <= box.c2; c++) {
6041
7124
  const def = displayed[c]?.def;
6042
- if (node && !node.kind && def?.editable && def.field) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
7125
+ if (node && !node.kind && def?.field && cellEditable(def, node.data, r)) this.writeCell(changes, node.data, def.field, displayed[c].id, r, null);
6043
7126
  }
6044
7127
  }
6045
7128
  this.endCellTx(changes);
@@ -6219,6 +7302,7 @@ var Grid = class {
6219
7302
  }
6220
7303
  return s.dir === "asc" || s.dir === "desc";
6221
7304
  });
7305
+ if (this.veto("beforeSort", { sort: valid.map((s) => ({ ...s })) })) return;
6222
7306
  this.store.set({ sort: valid });
6223
7307
  this.events.emit("sortChange", { sort: [...valid] });
6224
7308
  }
@@ -6240,6 +7324,7 @@ var Grid = class {
6240
7324
  warn(`setFilter: unknown column "${colId}"`);
6241
7325
  return;
6242
7326
  }
7327
+ if (this.veto("beforeFilter", { colId, model })) return;
6243
7328
  const filters = { ...this.store.state.filters };
6244
7329
  if (model) filters[colId] = model;
6245
7330
  else delete filters[colId];
@@ -6260,9 +7345,21 @@ var Grid = class {
6260
7345
  this.store.set({ selected, anchorId });
6261
7346
  this.events.emit("selectionChange", { selected: this.selectedRows() });
6262
7347
  }
7348
+ /** Data rows that may be checked (isRowSelectable). */
7349
+ rowSelectable(row) {
7350
+ const f = this.options.isRowSelectable;
7351
+ if (!f) return true;
7352
+ try {
7353
+ return !!f(row);
7354
+ } catch {
7355
+ return false;
7356
+ }
7357
+ }
6263
7358
  selectFromClick(index, toggle, range) {
6264
7359
  const mode = this.options.rowSelection;
6265
- if (!mode || index < 0 || index >= this.rowModel.getRowCount() || this.rowModel.getRow(index).kind) return;
7360
+ if (!mode || index < 0 || index >= this.rowModel.getRowCount()) return;
7361
+ const target = this.rowModel.getRow(index);
7362
+ if (target.kind || !this.rowSelectable(target.data)) return;
6266
7363
  const r = nextSelection({
6267
7364
  selected: this.store.state.selected,
6268
7365
  anchorIndex: this.anchorIndex(),
@@ -6272,11 +7369,12 @@ var Grid = class {
6272
7369
  toggle,
6273
7370
  range
6274
7371
  });
6275
- if (this.rowModel.rows() !== this.rowModel.dataRows()) {
7372
+ if (this.rowModel.rows() !== this.rowModel.dataRows() || this.options.isRowSelectable) {
6276
7373
  for (const id of r.selected) {
6277
7374
  if (this.store.state.selected.has(id)) continue;
6278
7375
  const i = this.rowModel.indexOfId(id);
6279
- if (i >= 0 && this.rowModel.getRow(i).kind) r.selected.delete(id);
7376
+ const n = i >= 0 ? this.rowModel.getRow(i) : null;
7377
+ if (n && (n.kind || !this.rowSelectable(n.data))) r.selected.delete(id);
6280
7378
  }
6281
7379
  }
6282
7380
  this.setSelected(r.selected, this.rowModel.getRow(r.anchorIndex)?.id ?? null);
@@ -6288,8 +7386,13 @@ var Grid = class {
6288
7386
  const i = this.rowModel.indexOfId(id);
6289
7387
  return i < 0 ? null : i;
6290
7388
  }
6291
- toggleSelectAll() {
7389
+ /** Rows select-all acts on: displayed data rows that are selectable. */
7390
+ selectableRows() {
6292
7391
  const rows = this.rowModel.dataRows();
7392
+ return this.options.isRowSelectable ? rows.filter((n) => this.rowSelectable(n.data)) : rows;
7393
+ }
7394
+ toggleSelectAll() {
7395
+ const rows = this.selectableRows();
6293
7396
  const sel = this.store.state.selected;
6294
7397
  const all2 = rows.length > 0 && rows.every((n) => sel.has(n.id));
6295
7398
  const next = new Set(sel);
@@ -6309,7 +7412,7 @@ var Grid = class {
6309
7412
  if (this.options.rowSelection) sys.push(SELECT_COLUMN);
6310
7413
  return [...sys, ...defs];
6311
7414
  }
6312
- setData(rows) {
7415
+ setData(rows, options) {
6313
7416
  if (this.clientOnly("setData")) return;
6314
7417
  if (!Array.isArray(rows)) {
6315
7418
  warn("setData: rows must be an array");
@@ -6319,8 +7422,11 @@ var Grid = class {
6319
7422
  this.tracker.reset();
6320
7423
  this.history.clear();
6321
7424
  this.detailOpen.clear();
7425
+ this.treeLoaded.clear();
7426
+ this.treeLoading.clear();
6322
7427
  this.renderer.dropDetail();
6323
7428
  this.store.set({ data: cleanRows(rows) });
7429
+ if (options?.scrollToTop) this.renderer.setScroll({ top: 0, left: 0 });
6324
7430
  const sel = this.store.state.selected;
6325
7431
  if (sel.size) {
6326
7432
  if (this.options.getRowId) {
@@ -6394,7 +7500,7 @@ var Grid = class {
6394
7500
  return fn(...a);
6395
7501
  });
6396
7502
  return {
6397
- setData: guard((rows) => this.setData(rows)),
7503
+ setData: guard((rows, o) => this.setData(rows, o)),
6398
7504
  getData: guard(() => this.store.state.data),
6399
7505
  setColumns: guard((defs) => this.setColumns(defs)),
6400
7506
  getColumnState: guard(() => this.columns.getState()),
@@ -6408,10 +7514,14 @@ var Grid = class {
6408
7514
  getFilterModel: guard(() => ({ ...this.store.state.filters })),
6409
7515
  setQuickFilter: guard((t) => this.store.set({ quickFilter: t ?? "" })),
6410
7516
  getSelectedRows: guard(() => this.selectedRows()),
6411
- selectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set([...this.store.state.selected, ...this.rowModel.dataRows().map((n) => n.id)]))),
7517
+ selectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set([...this.store.state.selected, ...this.selectableRows().map((n) => n.id)]))),
6412
7518
  deselectAll: guard(() => this.setSelected(/* @__PURE__ */ new Set(), null)),
6413
7519
  getDisplayedRowCount: guard(() => this.rowModel.getRowCount()),
6414
7520
  scrollToRow: guard((i) => this.renderer.scrollToRow(i)),
7521
+ validate: guard(() => this.validateAll()),
7522
+ getScrollPosition: guard(() => this.renderer.getScroll()),
7523
+ setScrollPosition: guard((p) => this.renderer.setScroll(p ?? {})),
7524
+ syncScroll: guard((other, axis = "both") => this.syncScroll(other, axis)),
6415
7525
  setTheme: guard((t) => this.store.set({ theme: t })),
6416
7526
  setLocale: guard((l) => this.store.set({ locale: resolveLocale(l) })),
6417
7527
  setFont: guard((f) => this.setFont(f)),
@@ -6428,8 +7538,10 @@ var Grid = class {
6428
7538
  return r && { start: { ...r.start }, end: { ...r.end } };
6429
7539
  }),
6430
7540
  setRange: guard((r) => this.setRange(r)),
7541
+ getRanges: guard(() => this.getRanges()),
7542
+ setRanges: guard((list) => this.setRanges(list)),
6431
7543
  copySelection: guard((o) => this.copyToClipboard(!!o?.headers, !!o?.rows)),
6432
- pasteText: guard((text) => this.pasteText(text)),
7544
+ pasteText: guard((text2) => this.pasteText(text2)),
6433
7545
  startEdit: guard((rowIndex, colId) => this.editor.start(rowIndex, colId)),
6434
7546
  stopEdit: guard((cancel) => cancel ? this.editor.cancel() : void this.editor.commit(null)),
6435
7547
  addRows: guard((rows, index) => this.addRows(rows, index)),
@@ -6446,7 +7558,7 @@ var Grid = class {
6446
7558
  canRedo: guard(() => this.history.canRedo()),
6447
7559
  fillRange: guard((t) => this.fillRange(t)),
6448
7560
  setPinnedRows: guard((r) => this.setPinnedRows(r)),
6449
- find: guard((text) => this.finder.find(text)),
7561
+ find: guard((text2) => this.finder.find(text2)),
6450
7562
  findNext: guard(() => this.finder.next()),
6451
7563
  findPrevious: guard(() => this.finder.previous()),
6452
7564
  clearFind: guard(() => this.finder.clear()),
@@ -6458,6 +7570,7 @@ var Grid = class {
6458
7570
  setDetailExpanded: guard((row, open) => this.setDetailExpanded(row, open)),
6459
7571
  isDetailExpanded: guard((row) => this.isDetailExpanded(row)),
6460
7572
  moveRow: guard((row, toIndex) => this.moveRow(row, toIndex)),
7573
+ dropRow: guard((row, target, position) => this.dropRow(row, target, position)),
6461
7574
  setPage: guard((p) => this.setPage(p)),
6462
7575
  getPage: guard(() => this.getPage()),
6463
7576
  getPageCount: guard(() => this.getPageCount()),
@@ -6479,6 +7592,7 @@ var Grid = class {
6479
7592
  destroy() {
6480
7593
  if (this.destroyed) return;
6481
7594
  this.destroyed = true;
7595
+ liveGrids.delete(this.renderer.root);
6482
7596
  for (const d of this.disposers) d();
6483
7597
  this.renderer.destroy();
6484
7598
  this.events.clear();