@freegridjs/core 1.6.0 → 1.7.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
@@ -1,3 +1,143 @@
1
+ // src/rows/filters.ts
2
+ function isBlank(v) {
3
+ return v == null || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
4
+ }
5
+ var pad = (n) => String(n).padStart(2, "0");
6
+ function toDateKey(v) {
7
+ if (isBlank(v)) return null;
8
+ if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
9
+ const d = v instanceof Date ? v : new Date(v);
10
+ if (Number.isNaN(d.getTime())) return null;
11
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
12
+ }
13
+ function setKey(v) {
14
+ if (isBlank(v)) return "";
15
+ if (v instanceof Date) return toDateKey(v) ?? "";
16
+ return String(v);
17
+ }
18
+ var all = () => true;
19
+ function buildPredicate(model) {
20
+ switch (model.type) {
21
+ case "combined": {
22
+ const parts = (Array.isArray(model.conditions) ? model.conditions : []).map(buildPredicate);
23
+ if (!parts.length) return all;
24
+ return model.operator === "or" ? (v) => parts.some((p) => p(v)) : (v) => parts.every((p) => p(v));
25
+ }
26
+ case "text": {
27
+ const q = (model.value ?? "").toLowerCase();
28
+ const s = (v) => isBlank(v) ? "" : String(v).toLowerCase();
29
+ switch (model.op) {
30
+ case "contains":
31
+ return (v) => s(v).includes(q);
32
+ case "equals":
33
+ return (v) => s(v) === q;
34
+ case "notEquals":
35
+ return (v) => s(v) !== q;
36
+ case "startsWith":
37
+ return (v) => s(v).startsWith(q);
38
+ case "endsWith":
39
+ return (v) => s(v).endsWith(q);
40
+ case "blank":
41
+ return isBlank;
42
+ case "notBlank":
43
+ return (v) => !isBlank(v);
44
+ }
45
+ return all;
46
+ }
47
+ case "number": {
48
+ const n = (v) => isBlank(v) ? NaN : Number(v);
49
+ if (model.op === "blank") return (v) => Number.isNaN(n(v));
50
+ if (model.op === "notBlank") return (v) => !Number.isNaN(n(v));
51
+ const a = model.value;
52
+ if (a == null || Number.isNaN(a)) return all;
53
+ switch (model.op) {
54
+ case "eq":
55
+ return (v) => n(v) === a;
56
+ case "ne":
57
+ return (v) => n(v) !== a;
58
+ case "lt":
59
+ return (v) => n(v) < a;
60
+ case "lte":
61
+ return (v) => n(v) <= a;
62
+ case "gt":
63
+ return (v) => n(v) > a;
64
+ case "gte":
65
+ return (v) => n(v) >= a;
66
+ case "between": {
67
+ const b = model.valueTo;
68
+ if (b == null || Number.isNaN(b)) return all;
69
+ const lo = Math.min(a, b), hi = Math.max(a, b);
70
+ return (v) => {
71
+ const x = n(v);
72
+ return x >= lo && x <= hi;
73
+ };
74
+ }
75
+ }
76
+ return all;
77
+ }
78
+ case "date": {
79
+ if (model.op === "blank") return (v) => toDateKey(v) === null;
80
+ if (model.op === "notBlank") return (v) => toDateKey(v) !== null;
81
+ const a = model.value;
82
+ if (!a) return all;
83
+ const k = (v) => toDateKey(v);
84
+ switch (model.op) {
85
+ case "equals":
86
+ return (v) => k(v) === a;
87
+ case "before":
88
+ return (v) => {
89
+ const x = k(v);
90
+ return x !== null && x < a;
91
+ };
92
+ case "after":
93
+ return (v) => {
94
+ const x = k(v);
95
+ return x !== null && x > a;
96
+ };
97
+ case "between": {
98
+ const b = model.valueTo;
99
+ if (!b) return all;
100
+ const lo = a < b ? a : b, hi = a < b ? b : a;
101
+ return (v) => {
102
+ const x = k(v);
103
+ return x !== null && x >= lo && x <= hi;
104
+ };
105
+ }
106
+ }
107
+ return all;
108
+ }
109
+ case "set": {
110
+ const allowed = new Set(model.values.map(setKey));
111
+ return (v) => allowed.has(setKey(v));
112
+ }
113
+ }
114
+ return all;
115
+ }
116
+ function quickTokens(q) {
117
+ const t = q.trim().toLowerCase();
118
+ return t ? t.split(/\s+/) : [];
119
+ }
120
+ function matchesQuick(tokens, text2) {
121
+ return tokens.every((t) => text2.includes(t));
122
+ }
123
+ function buildAdvanced(f, columnDef, valueOf) {
124
+ const parts = [];
125
+ for (const c of Array.isArray(f?.conditions) ? f.conditions : []) {
126
+ if (!c || typeof c !== "object") continue;
127
+ if ("conditions" in c) {
128
+ const sub = buildAdvanced(c, columnDef, valueOf);
129
+ if (sub) parts.push(sub);
130
+ continue;
131
+ }
132
+ const def = columnDef(c.colId);
133
+ if (!def || !c.model) continue;
134
+ const pred = buildPredicate(c.model);
135
+ parts.push((row) => pred(valueOf(def, row)));
136
+ }
137
+ if (!parts.length) return null;
138
+ return f.operator === "or" ? (row) => parts.some((p) => p(row)) : (row) => parts.every((p) => p(row));
139
+ }
140
+
1
141
  // src/warn.ts
2
142
  var seen = /* @__PURE__ */ new Set();
3
143
  function warn(message) {
@@ -6,6 +146,128 @@ function warn(message) {
6
146
  console.warn(`[FreeGrid] ${message}`);
7
147
  }
8
148
 
149
+ // src/i18n/format.ts
150
+ var cache = /* @__PURE__ */ new Map();
151
+ var numberFormats = /* @__PURE__ */ new Map();
152
+ function numberFormat(locale, maxFraction) {
153
+ const key = `${locale}|${maxFraction ?? ""}`;
154
+ let nf = numberFormats.get(key);
155
+ if (!nf) {
156
+ nf = new Intl.NumberFormat(locale, maxFraction == null ? void 0 : { maximumFractionDigits: maxFraction });
157
+ numberFormats.set(key, nf);
158
+ }
159
+ return nf;
160
+ }
161
+ function parseNumberPattern(p) {
162
+ if (p === "integer") return { maximumFractionDigits: 0 };
163
+ if (p === "percent") return { style: "percent" };
164
+ if (p.startsWith("currency:")) return { style: "currency", currency: p.slice(9) };
165
+ if (!/^[#0,]*(\.[#0]+)?%?$/.test(p) || !/[#0]/.test(p)) return null;
166
+ const pct = p.endsWith("%");
167
+ const body = pct ? p.slice(0, -1) : p;
168
+ const frac = body.includes(".") ? body.split(".")[1] : "";
169
+ return {
170
+ useGrouping: body.includes(","),
171
+ minimumFractionDigits: (frac.match(/0/g) ?? []).length,
172
+ maximumFractionDigits: frac.length,
173
+ ...pct ? { style: "percent" } : {}
174
+ };
175
+ }
176
+ var DATE_PRESETS = {
177
+ date: { year: "numeric", month: "2-digit", day: "2-digit" },
178
+ datetime: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" },
179
+ time: { hour: "2-digit", minute: "2-digit" },
180
+ short: { dateStyle: "short" },
181
+ medium: { dateStyle: "medium" },
182
+ long: { dateStyle: "long" }
183
+ };
184
+ function toDate(v) {
185
+ if (isBlank(v)) return null;
186
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
187
+ if (typeof v === "string") {
188
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
189
+ if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
190
+ }
191
+ if (typeof v !== "string" && typeof v !== "number") return null;
192
+ const d = new Date(v);
193
+ return Number.isNaN(d.getTime()) ? null : d;
194
+ }
195
+ function create(type, format, locale) {
196
+ switch (type) {
197
+ case "number": {
198
+ let opts = {};
199
+ if (typeof format === "string") {
200
+ const parsed = parseNumberPattern(format);
201
+ if (!parsed) warn(`unknown number format "${format}"`);
202
+ opts = parsed ?? {};
203
+ } else if (format) opts = format;
204
+ const nf = new Intl.NumberFormat(locale, opts);
205
+ return (v) => {
206
+ if (isBlank(v)) return "";
207
+ const n = typeof v === "number" ? v : Number(v);
208
+ return Number.isNaN(n) ? String(v) : nf.format(n);
209
+ };
210
+ }
211
+ case "date": {
212
+ if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
213
+ const pad4 = (n) => String(n).padStart(2, "0");
214
+ const withTime = format === "datetime";
215
+ return (v) => {
216
+ const d = toDate(v);
217
+ if (!d) return isBlank(v) ? "" : String(v);
218
+ const day = `${d.getFullYear()}-${pad4(d.getMonth() + 1)}-${pad4(d.getDate())}`;
219
+ return withTime ? `${day} ${pad4(d.getHours())}:${pad4(d.getMinutes())}` : day;
220
+ };
221
+ }
222
+ let opts = DATE_PRESETS.date;
223
+ if (typeof format === "string") {
224
+ if (!DATE_PRESETS[format]) warn(`unknown date format "${format}"`);
225
+ opts = DATE_PRESETS[format] ?? DATE_PRESETS.date;
226
+ } else if (format) opts = format;
227
+ const df = new Intl.DateTimeFormat(locale, opts);
228
+ return (v) => {
229
+ const d = toDate(v);
230
+ return d ? df.format(d) : isBlank(v) ? "" : String(v);
231
+ };
232
+ }
233
+ case "boolean":
234
+ return (v) => v === true ? "\u2713" : v === false ? "\u2717" : "";
235
+ default:
236
+ return (v) => isBlank(v) ? "" : v instanceof Date ? v.toISOString() : String(v);
237
+ }
238
+ }
239
+ var sparklineText = (v) => Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v);
240
+ var byDef = /* @__PURE__ */ new WeakMap();
241
+ var retyped = /* @__PURE__ */ new WeakMap();
242
+ function clearFormatterCache() {
243
+ byDef = /* @__PURE__ */ new WeakMap();
244
+ retyped = /* @__PURE__ */ new WeakMap();
245
+ }
246
+ function getFormatterAs(def, type, locale, format = def.format) {
247
+ if (type === def.type && format === def.format) return getFormatter(def, locale);
248
+ if (format !== def.format && typeof format !== "string") return getFormatter({ ...def, type, format }, locale);
249
+ let byKey = retyped.get(def);
250
+ if (!byKey) retyped.set(def, byKey = /* @__PURE__ */ new Map());
251
+ const key = `${type ?? ""}|${format === def.format ? "" : `=${format}`}`;
252
+ let d = byKey.get(key);
253
+ if (!d) byKey.set(key, d = { ...def, type, format });
254
+ return getFormatter(d, locale);
255
+ }
256
+ function getFormatter(def, locale) {
257
+ if (def.sparkline) return sparklineText;
258
+ const f = def.format;
259
+ const hit = byDef.get(def);
260
+ if (hit && hit.locale === locale && hit.type === def.type && hit.format === f) return hit.fmt;
261
+ const key = `${locale}|${def.type ?? "text"}|${typeof f === "string" ? f : JSON.stringify(f ?? null)}`;
262
+ let fmt = cache.get(key);
263
+ if (!fmt) {
264
+ fmt = create(def.type, f, locale);
265
+ cache.set(key, fmt);
266
+ }
267
+ byDef.set(def, { locale, type: def.type, format: f, fmt });
268
+ return fmt;
269
+ }
270
+
9
271
  // src/columns/columnModel.ts
10
272
  var SELECT_COL_ID = "__fg_select__";
11
273
  var SELECT_COLUMN = {
@@ -91,9 +353,12 @@ var ColumnModel = class {
91
353
  this.containerWidth = 0;
92
354
  this.layout = { left: [], center: [], right: [] };
93
355
  this.leafById = /* @__PURE__ */ new Map();
356
+ /** Leaf ids in the order the definitions list them (before any move by the user). */
357
+ this.declared = [];
94
358
  this.setDefs(defs);
95
359
  }
96
360
  setDefs(defs) {
361
+ clearFormatterCache();
97
362
  this.defs = defs;
98
363
  this.byId = /* @__PURE__ */ new Map();
99
364
  let auto = 0;
@@ -133,9 +398,36 @@ var ColumnModel = class {
133
398
  return ok;
134
399
  };
135
400
  this.roots_ = valid(defs).map((d) => build(d, null, 0));
401
+ this.declared = this.leafNodes().map((n) => n.id);
136
402
  this.depth = Math.max(1, ...this.roots_.map(maxDepth));
137
403
  this.relayout();
138
404
  }
405
+ /**
406
+ * New definitions that keep what the user did to the columns still there: a width, visibility,
407
+ * pinning or group state the user changed stays unless the new definition changes that same property,
408
+ * and a column order the user changed stays while the definitions list the same columns in the same order.
409
+ */
410
+ updateDefs(defs) {
411
+ const before = new Map(this.byId);
412
+ const declared = this.declared;
413
+ const order = this.leafNodes().map((n) => n.id);
414
+ this.setDefs(defs);
415
+ for (const [id, node] of this.byId) {
416
+ const old = before.get(id);
417
+ if (!old || !!old.children !== !!node.children) continue;
418
+ const a = old.def, b = node.def;
419
+ if (a.width === b.width && a.flex === b.flex && a.minWidth === b.minWidth && a.maxWidth === b.maxWidth) {
420
+ node.width = old.width;
421
+ node.flex = old.flex;
422
+ }
423
+ if ((a.hide ?? false) === (b.hide ?? false)) node.hide = old.hide;
424
+ if (a.pinned === b.pinned && old.parent?.def.pinned === node.parent?.def.pinned) node.pinned = old.pinned;
425
+ if (!!a.openByDefault === !!b.openByDefault) node.open = old.open;
426
+ }
427
+ const same3 = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
428
+ if (same3(declared, this.declared) && !same3(order, declared)) this.applyState(order.map((colId) => ({ colId })));
429
+ else this.relayout();
430
+ }
139
431
  /** Returns true when the computed layout changed. */
140
432
  setContainerWidth(width) {
141
433
  if (width === this.containerWidth) return false;
@@ -331,7 +623,7 @@ var FormulaError = class extends Error {
331
623
  this.code = code;
332
624
  }
333
625
  };
334
- var ERROR_RE = /^#[A-Z/0]+[!?]$/;
626
+ var ERROR_RE = /^#(?:[A-Z/0]+[!?]|N\/A)$/;
335
627
  var isFormulaError = (v) => typeof v === "string" && ERROR_RE.test(v);
336
628
  function tokenize(src) {
337
629
  const out = [];
@@ -398,13 +690,17 @@ var bool = (v) => typeof v === "boolean" ? v : typeof v === "string" && /^(true|
398
690
  function round(x, digits, mode = "near") {
399
691
  const d = Math.trunc(digits);
400
692
  const f = mode === "near" ? Math.round : mode === "up" ? Math.ceil : Math.floor;
401
- const r = Number(`${f(Number(`${Math.abs(x)}e${d}`))}e${-d}`);
693
+ const shift = (n, e) => {
694
+ const [m, p = "0"] = String(n).split("e");
695
+ return Number(`${m}e${Number(p) + e}`);
696
+ };
697
+ const r = shift(f(shift(Math.abs(x), d)), -d);
402
698
  return x < 0 ? -r : r;
403
699
  }
404
700
  function compare(a, b) {
405
- const numeric = (v) => typeof v === "number" || typeof v === "boolean" || isBlankValue(v);
406
- if (numeric(a) && numeric(b)) return num(a) - num(b);
407
- if (a instanceof Date || b instanceof Date) return num(toDate(a)) - num(toDate(b));
701
+ const numeric2 = (v) => typeof v === "number" || typeof v === "boolean" || isBlankValue(v);
702
+ if (numeric2(a) && numeric2(b)) return num(a) - num(b);
703
+ if (a instanceof Date || b instanceof Date) return num(toDate2(a)) - num(toDate2(b));
408
704
  return text(a).localeCompare(text(b), void 0, { sensitivity: "accent" });
409
705
  }
410
706
  var flat = (a) => a.flatMap((v) => Array.isArray(v) ? v : [v]);
@@ -414,7 +710,7 @@ function noErrors(list) {
414
710
  if (bad !== void 0) throw new FormulaError(bad);
415
711
  return list;
416
712
  }
417
- function toDate(v) {
713
+ function toDate2(v) {
418
714
  if (v instanceof Date) return v;
419
715
  if (typeof v === "number") return new Date(v);
420
716
  if (typeof v === "string") {
@@ -423,9 +719,9 @@ function toDate(v) {
423
719
  }
424
720
  throw new FormulaError("#VALUE!");
425
721
  }
426
- var pad = (n) => String(n).padStart(2, "0");
722
+ var pad2 = (n) => String(n).padStart(2, "0");
427
723
  function ymd(d) {
428
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
724
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
429
725
  }
430
726
  var DAY = 864e5;
431
727
  var dayNumber = (d) => Math.round(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY);
@@ -476,11 +772,11 @@ var FUNCS = {
476
772
  },
477
773
  MIN: (a) => {
478
774
  const n = numsLoose(a);
479
- return n.length ? Math.min(...n) : 0;
775
+ return n.length ? n.reduce((m, v) => Math.min(m, v)) : 0;
480
776
  },
481
777
  MAX: (a) => {
482
778
  const n = numsLoose(a);
483
- return n.length ? Math.max(...n) : 0;
779
+ return n.length ? n.reduce((m, v) => Math.max(m, v)) : 0;
484
780
  },
485
781
  MEDIAN: (a) => {
486
782
  const n = numsLoose(a).sort((x, y) => x - y);
@@ -568,8 +864,8 @@ var FUNCS = {
568
864
  const v = a[0], f = text(a[1]);
569
865
  const dec = /0\.(0+)/.exec(f)?.[1].length ?? 0;
570
866
  if (/[yd]/i.test(f)) {
571
- const d = toDate(v);
572
- return f.replace(/yyyy/gi, String(d.getFullYear())).replace(/mm/g, pad(d.getMonth() + 1)).replace(/dd/gi, pad(d.getDate()));
867
+ const d = toDate2(v);
868
+ return f.replace(/yyyy/gi, String(d.getFullYear())).replace(/mm/g, pad2(d.getMonth() + 1)).replace(/dd/gi, pad2(d.getDate()));
573
869
  }
574
870
  const n = num(v);
575
871
  const s = f.includes("%") ? (n * 100).toFixed(dec) + "%" : f.includes(",") ? n.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec }) : n.toFixed(dec);
@@ -588,20 +884,20 @@ var FUNCS = {
588
884
  TODAY: () => ymd(/* @__PURE__ */ new Date()),
589
885
  NOW: () => /* @__PURE__ */ new Date(),
590
886
  DATE: (a) => ymd(new Date(num(a[0]), num(a[1]) - 1, num(a[2]))),
591
- YEAR: (a) => toDate(a[0]).getFullYear(),
592
- MONTH: (a) => toDate(a[0]).getMonth() + 1,
593
- DAY: (a) => toDate(a[0]).getDate(),
594
- WEEKDAY: (a) => toDate(a[0]).getDay() + 1,
595
- DAYS: (a) => dayNumber(toDate(a[0])) - dayNumber(toDate(a[1])),
887
+ YEAR: (a) => toDate2(a[0]).getFullYear(),
888
+ MONTH: (a) => toDate2(a[0]).getMonth() + 1,
889
+ DAY: (a) => toDate2(a[0]).getDate(),
890
+ WEEKDAY: (a) => toDate2(a[0]).getDay() + 1,
891
+ DAYS: (a) => dayNumber(toDate2(a[0])) - dayNumber(toDate2(a[1])),
596
892
  EDATE: (a) => {
597
- const d = toDate(a[0]);
893
+ const d = toDate2(a[0]);
598
894
  const day = d.getDate();
599
895
  const r = new Date(d.getFullYear(), d.getMonth() + num(a[1]), 1);
600
896
  r.setDate(Math.min(day, new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate()));
601
897
  return ymd(r);
602
898
  },
603
899
  EOMONTH: (a) => {
604
- const d = toDate(a[0]);
900
+ const d = toDate2(a[0]);
605
901
  return ymd(new Date(d.getFullYear(), d.getMonth() + num(a[1]) + 1, 0));
606
902
  },
607
903
  // over columns
@@ -1046,7 +1342,7 @@ var toNumber = (s) => {
1046
1342
  const n = Number(s.replace(/[,\s₩$€¥]/g, ""));
1047
1343
  return s.trim() !== "" && Number.isFinite(n) ? n : null;
1048
1344
  };
1049
- var toDateKey = (s) => {
1345
+ var toDateKey2 = (s) => {
1050
1346
  const m = /^(\d{4})[-./]\s*(\d{1,2})[-./]\s*(\d{1,2})\.?$/.exec(s.trim());
1051
1347
  if (!m) return null;
1052
1348
  const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]);
@@ -1068,11 +1364,11 @@ function parseFilterText(type, text2) {
1068
1364
  return n === null ? void 0 : { type: "number", op: op2 ? op2[1] : "eq", value: n };
1069
1365
  }
1070
1366
  if (range) {
1071
- const a = toDateKey(range[1]), b = toDateKey(range[2]);
1367
+ const a = toDateKey2(range[1]), b = toDateKey2(range[2]);
1072
1368
  return !a || !b ? void 0 : { type: "date", op: "between", value: a < b ? a : b, valueTo: a < b ? b : a };
1073
1369
  }
1074
1370
  const op = DATE_OPS.find(([p]) => t.startsWith(p));
1075
- const d = toDateKey(op ? t.slice(op[0].length) : t);
1371
+ const d = toDateKey2(op ? t.slice(op[0].length) : t);
1076
1372
  return d ? { type: "date", op: op ? op[1] : "equals", value: d } : void 0;
1077
1373
  }
1078
1374
  function filterText(model) {
@@ -1155,325 +1451,93 @@ var FilterRow = class {
1155
1451
  const def = this.grid.columns.getNode(select.dataset.col)?.def;
1156
1452
  if (!def) continue;
1157
1453
  const v = select.value;
1158
- this.fillOptions(select, def);
1159
- if (v && ![...select.options].some((o) => o.value === v)) {
1160
- const o = el("option", "", { value: v });
1161
- o.textContent = String(JSON.parse(v));
1162
- select.append(o);
1163
- }
1164
- select.value = v;
1165
- }
1166
- }
1167
- }
1168
- fillOptions(select, def) {
1169
- const all2 = el("option", "", { value: "" });
1170
- all2.textContent = this.grid.store.state.locale.selectAll;
1171
- const opts = this.distinct(def).map((v) => {
1172
- const o = el("option", "", { value: JSON.stringify(v) });
1173
- o.textContent = v === true ? "\u2713" : v === false ? "\u2717" : String(v);
1174
- return o;
1175
- });
1176
- select.replaceChildren(all2, ...opts);
1177
- }
1178
- cell(c, h) {
1179
- const cell = el("div", "fg-filter-cell");
1180
- cell.style.left = `${c.left}px`;
1181
- cell.style.width = `${c.width}px`;
1182
- cell.style.height = `${h}px`;
1183
- const type = isSystemCol(c.id) ? null : filterTypeOf(c.def);
1184
- if (!type) return cell;
1185
- const loc = this.grid.store.state.locale;
1186
- const label = `${loc.filter}: ${c.def.header ?? c.def.field ?? c.id}`;
1187
- if (type === "set") {
1188
- const select = el("select", "fg-filter-input", { "aria-label": label });
1189
- select.dataset.col = c.id;
1190
- this.fillOptions(select, c.def);
1191
- select.addEventListener("change", () => {
1192
- this.grid.setFilter(c.id, select.value ? { type: "set", values: [JSON.parse(select.value)] } : null);
1193
- });
1194
- cell.append(select);
1195
- return cell;
1196
- }
1197
- const input = el("input", "fg-filter-input", { type: "text", "aria-label": label, spellcheck: "false", autocomplete: "off" });
1198
- input.dataset.col = c.id;
1199
- if (type === "number") input.placeholder = ">0, 10~20";
1200
- if (type === "date") input.placeholder = ">2026-01-01";
1201
- const apply = () => {
1202
- const model = parseFilterText(type, input.value);
1203
- input.classList.toggle("fg-filter-bad", model === void 0);
1204
- if (model === void 0) return;
1205
- this.grid.setFilter(c.id, model);
1206
- };
1207
- input.addEventListener("input", () => {
1208
- clearTimeout(this.timers.get(c.id));
1209
- this.timers.set(c.id, window.setTimeout(apply, 200));
1210
- });
1211
- input.addEventListener("keydown", (e) => {
1212
- e.stopPropagation();
1213
- if (e.key === "Enter") {
1214
- clearTimeout(this.timers.get(c.id));
1215
- apply();
1216
- } else if (e.key === "Escape") {
1217
- input.value = "";
1218
- clearTimeout(this.timers.get(c.id));
1219
- apply();
1220
- }
1221
- });
1222
- cell.append(input);
1223
- return cell;
1224
- }
1225
- /** Values for a set box: distinct, sorted, at most 200. */
1226
- distinct(def) {
1227
- const seen2 = /* @__PURE__ */ new Map();
1228
- for (const row of this.grid.store.state.data) {
1229
- const v = getCellValue(def, row);
1230
- if (v === null || v === void 0 || v === "") continue;
1231
- const k = JSON.stringify(v);
1232
- if (!seen2.has(k)) seen2.set(k, v);
1233
- if (seen2.size >= 200) break;
1234
- }
1235
- return [...seen2.values()].sort((a, b) => String(a).localeCompare(String(b)));
1236
- }
1237
- setTransform(x) {
1238
- this.inners.center.style.transform = `translateX(${-x}px)`;
1239
- }
1240
- destroy() {
1241
- for (const t of this.timers.values()) clearTimeout(t);
1242
- for (const r of REGIONS2) this.inners[r].remove();
1243
- }
1244
- };
1245
-
1246
- // src/rows/filters.ts
1247
- function isBlank(v) {
1248
- return v == null || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
1249
- }
1250
- var pad2 = (n) => String(n).padStart(2, "0");
1251
- function toDateKey2(v) {
1252
- if (isBlank(v)) return null;
1253
- if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
1254
- const d = v instanceof Date ? v : new Date(v);
1255
- if (Number.isNaN(d.getTime())) return null;
1256
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
1257
- }
1258
- function setKey(v) {
1259
- if (isBlank(v)) return "";
1260
- if (v instanceof Date) return toDateKey2(v) ?? "";
1261
- return String(v);
1262
- }
1263
- var all = () => true;
1264
- function buildPredicate(model) {
1265
- switch (model.type) {
1266
- case "combined": {
1267
- const parts = (Array.isArray(model.conditions) ? model.conditions : []).map(buildPredicate);
1268
- if (!parts.length) return all;
1269
- return model.operator === "or" ? (v) => parts.some((p) => p(v)) : (v) => parts.every((p) => p(v));
1270
- }
1271
- case "text": {
1272
- const q = (model.value ?? "").toLowerCase();
1273
- const s = (v) => isBlank(v) ? "" : String(v).toLowerCase();
1274
- switch (model.op) {
1275
- case "contains":
1276
- return (v) => s(v).includes(q);
1277
- case "equals":
1278
- return (v) => s(v) === q;
1279
- case "notEquals":
1280
- return (v) => s(v) !== q;
1281
- case "startsWith":
1282
- return (v) => s(v).startsWith(q);
1283
- case "endsWith":
1284
- return (v) => s(v).endsWith(q);
1285
- case "blank":
1286
- return isBlank;
1287
- case "notBlank":
1288
- return (v) => !isBlank(v);
1289
- }
1290
- return all;
1291
- }
1292
- case "number": {
1293
- const n = (v) => isBlank(v) ? NaN : Number(v);
1294
- if (model.op === "blank") return (v) => Number.isNaN(n(v));
1295
- if (model.op === "notBlank") return (v) => !Number.isNaN(n(v));
1296
- const a = model.value;
1297
- if (a == null || Number.isNaN(a)) return all;
1298
- switch (model.op) {
1299
- case "eq":
1300
- return (v) => n(v) === a;
1301
- case "ne":
1302
- return (v) => n(v) !== a;
1303
- case "lt":
1304
- return (v) => n(v) < a;
1305
- case "lte":
1306
- return (v) => n(v) <= a;
1307
- case "gt":
1308
- return (v) => n(v) > a;
1309
- case "gte":
1310
- return (v) => n(v) >= a;
1311
- case "between": {
1312
- const b = model.valueTo;
1313
- if (b == null || Number.isNaN(b)) return all;
1314
- const lo = Math.min(a, b), hi = Math.max(a, b);
1315
- return (v) => {
1316
- const x = n(v);
1317
- return x >= lo && x <= hi;
1318
- };
1319
- }
1320
- }
1321
- return all;
1322
- }
1323
- case "date": {
1324
- if (model.op === "blank") return (v) => toDateKey2(v) === null;
1325
- if (model.op === "notBlank") return (v) => toDateKey2(v) !== null;
1326
- const a = model.value;
1327
- if (!a) return all;
1328
- const k = (v) => toDateKey2(v);
1329
- switch (model.op) {
1330
- case "equals":
1331
- return (v) => k(v) === a;
1332
- case "before":
1333
- return (v) => {
1334
- const x = k(v);
1335
- return x !== null && x < a;
1336
- };
1337
- case "after":
1338
- return (v) => {
1339
- const x = k(v);
1340
- return x !== null && x > a;
1341
- };
1342
- case "between": {
1343
- const b = model.valueTo;
1344
- if (!b) return all;
1345
- const lo = a < b ? a : b, hi = a < b ? b : a;
1346
- return (v) => {
1347
- const x = k(v);
1348
- return x !== null && x >= lo && x <= hi;
1349
- };
1350
- }
1351
- }
1352
- return all;
1353
- }
1354
- case "set": {
1355
- const allowed = new Set(model.values.map(setKey));
1356
- return (v) => allowed.has(setKey(v));
1357
- }
1358
- }
1359
- return all;
1360
- }
1361
- function quickTokens(q) {
1362
- const t = q.trim().toLowerCase();
1363
- return t ? t.split(/\s+/) : [];
1364
- }
1365
- function matchesQuick(tokens, text2) {
1366
- return tokens.every((t) => text2.includes(t));
1367
- }
1368
- function buildAdvanced(f, columnDef, valueOf) {
1369
- const parts = [];
1370
- for (const c of Array.isArray(f?.conditions) ? f.conditions : []) {
1371
- if (!c || typeof c !== "object") continue;
1372
- if ("conditions" in c) {
1373
- const sub = buildAdvanced(c, columnDef, valueOf);
1374
- if (sub) parts.push(sub);
1375
- continue;
1454
+ this.fillOptions(select, def);
1455
+ if (v && ![...select.options].some((o) => o.value === v)) {
1456
+ const o = el("option", "", { value: v });
1457
+ o.textContent = String(JSON.parse(v));
1458
+ select.append(o);
1459
+ }
1460
+ select.value = v;
1461
+ }
1376
1462
  }
1377
- const def = columnDef(c.colId);
1378
- if (!def || !c.model) continue;
1379
- const pred = buildPredicate(c.model);
1380
- parts.push((row) => pred(valueOf(def, row)));
1381
1463
  }
1382
- if (!parts.length) return null;
1383
- return f.operator === "or" ? (row) => parts.some((p) => p(row)) : (row) => parts.every((p) => p(row));
1384
- }
1385
-
1386
- // src/i18n/format.ts
1387
- var cache = /* @__PURE__ */ new Map();
1388
- function parseNumberPattern(p) {
1389
- if (p === "integer") return { maximumFractionDigits: 0 };
1390
- if (p === "percent") return { style: "percent" };
1391
- if (p.startsWith("currency:")) return { style: "currency", currency: p.slice(9) };
1392
- if (!/^[#0,]*(\.[#0]+)?%?$/.test(p) || !/[#0]/.test(p)) return null;
1393
- const pct = p.endsWith("%");
1394
- const body = pct ? p.slice(0, -1) : p;
1395
- const frac = body.includes(".") ? body.split(".")[1] : "";
1396
- return {
1397
- useGrouping: body.includes(","),
1398
- minimumFractionDigits: (frac.match(/0/g) ?? []).length,
1399
- maximumFractionDigits: frac.length,
1400
- ...pct ? { style: "percent" } : {}
1401
- };
1402
- }
1403
- var DATE_PRESETS = {
1404
- date: { year: "numeric", month: "2-digit", day: "2-digit" },
1405
- datetime: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" },
1406
- time: { hour: "2-digit", minute: "2-digit" },
1407
- short: { dateStyle: "short" },
1408
- medium: { dateStyle: "medium" },
1409
- long: { dateStyle: "long" }
1410
- };
1411
- function toDate2(v) {
1412
- if (isBlank(v)) return null;
1413
- if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
1414
- if (typeof v === "string") {
1415
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
1416
- if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
1464
+ fillOptions(select, def) {
1465
+ const all2 = el("option", "", { value: "" });
1466
+ all2.textContent = this.grid.store.state.locale.selectAll;
1467
+ const opts = this.distinct(def).map((v) => {
1468
+ const o = el("option", "", { value: JSON.stringify(v) });
1469
+ o.textContent = v === true ? "\u2713" : v === false ? "\u2717" : String(v);
1470
+ return o;
1471
+ });
1472
+ select.replaceChildren(all2, ...opts);
1417
1473
  }
1418
- if (typeof v !== "string" && typeof v !== "number") return null;
1419
- const d = new Date(v);
1420
- return Number.isNaN(d.getTime()) ? null : d;
1421
- }
1422
- function create(type, format, locale) {
1423
- switch (type) {
1424
- case "number": {
1425
- let opts = {};
1426
- if (typeof format === "string") {
1427
- const parsed = parseNumberPattern(format);
1428
- if (!parsed) warn(`unknown number format "${format}"`);
1429
- opts = parsed ?? {};
1430
- } else if (format) opts = format;
1431
- const nf = new Intl.NumberFormat(locale, opts);
1432
- return (v) => {
1433
- if (isBlank(v)) return "";
1434
- const n = typeof v === "number" ? v : Number(v);
1435
- return Number.isNaN(n) ? String(v) : nf.format(n);
1436
- };
1474
+ cell(c, h) {
1475
+ const cell = el("div", "fg-filter-cell");
1476
+ cell.style.left = `${c.left}px`;
1477
+ cell.style.width = `${c.width}px`;
1478
+ cell.style.height = `${h}px`;
1479
+ const type = isSystemCol(c.id) ? null : filterTypeOf(c.def);
1480
+ if (!type) return cell;
1481
+ const loc = this.grid.store.state.locale;
1482
+ const label = `${loc.filter}: ${c.def.header ?? c.def.field ?? c.id}`;
1483
+ if (type === "set") {
1484
+ const select = el("select", "fg-filter-input", { "aria-label": label });
1485
+ select.dataset.col = c.id;
1486
+ this.fillOptions(select, c.def);
1487
+ select.addEventListener("change", () => {
1488
+ this.grid.setFilter(c.id, select.value ? { type: "set", values: [JSON.parse(select.value)] } : null);
1489
+ });
1490
+ cell.append(select);
1491
+ return cell;
1437
1492
  }
1438
- case "date": {
1439
- if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
1440
- const pad4 = (n) => String(n).padStart(2, "0");
1441
- const withTime = format === "datetime";
1442
- return (v) => {
1443
- const d = toDate2(v);
1444
- if (!d) return isBlank(v) ? "" : String(v);
1445
- const day = `${d.getFullYear()}-${pad4(d.getMonth() + 1)}-${pad4(d.getDate())}`;
1446
- return withTime ? `${day} ${pad4(d.getHours())}:${pad4(d.getMinutes())}` : day;
1447
- };
1493
+ const input = el("input", "fg-filter-input", { type: "text", "aria-label": label, spellcheck: "false", autocomplete: "off" });
1494
+ input.dataset.col = c.id;
1495
+ if (type === "number") input.placeholder = ">0, 10~20";
1496
+ if (type === "date") input.placeholder = ">2026-01-01";
1497
+ const apply = () => {
1498
+ const model = parseFilterText(type, input.value);
1499
+ input.classList.toggle("fg-filter-bad", model === void 0);
1500
+ if (model === void 0) return;
1501
+ this.grid.setFilter(c.id, model);
1502
+ };
1503
+ input.addEventListener("input", () => {
1504
+ clearTimeout(this.timers.get(c.id));
1505
+ this.timers.set(c.id, window.setTimeout(apply, 200));
1506
+ });
1507
+ input.addEventListener("keydown", (e) => {
1508
+ e.stopPropagation();
1509
+ if (e.key === "Enter") {
1510
+ clearTimeout(this.timers.get(c.id));
1511
+ apply();
1512
+ } else if (e.key === "Escape") {
1513
+ input.value = "";
1514
+ clearTimeout(this.timers.get(c.id));
1515
+ apply();
1448
1516
  }
1449
- let opts = DATE_PRESETS.date;
1450
- if (typeof format === "string") {
1451
- if (!DATE_PRESETS[format]) warn(`unknown date format "${format}"`);
1452
- opts = DATE_PRESETS[format] ?? DATE_PRESETS.date;
1453
- } else if (format) opts = format;
1454
- const df = new Intl.DateTimeFormat(locale, opts);
1455
- return (v) => {
1456
- const d = toDate2(v);
1457
- return d ? df.format(d) : isBlank(v) ? "" : String(v);
1458
- };
1517
+ });
1518
+ cell.append(input);
1519
+ return cell;
1520
+ }
1521
+ /** Values for a set box: distinct, sorted, at most 200. */
1522
+ distinct(def) {
1523
+ const seen2 = /* @__PURE__ */ new Map();
1524
+ for (const row of this.grid.store.state.data) {
1525
+ const v = getCellValue(def, row);
1526
+ if (v === null || v === void 0 || v === "") continue;
1527
+ const k = JSON.stringify(v);
1528
+ if (!seen2.has(k)) seen2.set(k, v);
1529
+ if (seen2.size >= 200) break;
1459
1530
  }
1460
- case "boolean":
1461
- return (v) => v === true ? "\u2713" : v === false ? "\u2717" : "";
1462
- default:
1463
- return (v) => isBlank(v) ? "" : v instanceof Date ? v.toISOString() : String(v);
1531
+ return [...seen2.values()].sort((a, b) => String(a).localeCompare(String(b)));
1464
1532
  }
1465
- }
1466
- function getFormatter(def, locale) {
1467
- if (def.sparkline) return (v) => Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v);
1468
- const f = def.format;
1469
- const key = `${locale}|${def.type ?? "text"}|${typeof f === "string" ? f : JSON.stringify(f ?? null)}`;
1470
- let fmt = cache.get(key);
1471
- if (!fmt) {
1472
- fmt = create(def.type, f, locale);
1473
- cache.set(key, fmt);
1533
+ setTransform(x) {
1534
+ this.inners.center.style.transform = `translateX(${-x}px)`;
1474
1535
  }
1475
- return fmt;
1476
- }
1536
+ destroy() {
1537
+ for (const t of this.timers.values()) clearTimeout(t);
1538
+ for (const r of REGIONS2) this.inners[r].remove();
1539
+ }
1540
+ };
1477
1541
 
1478
1542
  // src/summary.ts
1479
1543
  function numbers(values) {
@@ -1492,7 +1556,9 @@ function extreme(values, pickMax) {
1492
1556
  }
1493
1557
  const nums = numbers(values);
1494
1558
  if (!nums.length) return null;
1495
- return pickMax ? Math.max(...nums) : Math.min(...nums);
1559
+ let out = nums[0];
1560
+ for (const n of nums) if (pickMax ? n > out : n < out) out = n;
1561
+ return out;
1496
1562
  }
1497
1563
  function aggregate(agg, values, rows) {
1498
1564
  if (typeof agg === "function") return agg(values, rows);
@@ -1559,7 +1625,7 @@ function computeSpans(rows, def) {
1559
1625
  }
1560
1626
  return { start, end };
1561
1627
  }
1562
- const same2 = typeof def.merge === "function" ? (a, b) => {
1628
+ const same3 = typeof def.merge === "function" ? (a, b) => {
1563
1629
  try {
1564
1630
  return !!def.merge(getCellValue(def, rows[a]), getCellValue(def, rows[b]), rows[a], rows[b]);
1565
1631
  } catch {
@@ -1574,7 +1640,7 @@ function computeSpans(rows, def) {
1574
1640
  })();
1575
1641
  let s = 0;
1576
1642
  for (let i = 1; i <= n; i++) {
1577
- if (i < n && same2(i - 1, i)) continue;
1643
+ if (i < n && same3(i - 1, i)) continue;
1578
1644
  for (let k = s; k < i; k++) {
1579
1645
  start[k] = s;
1580
1646
  end[k] = i - 1;
@@ -1652,6 +1718,17 @@ var Renderer = class {
1652
1718
  this.activeCellId = "";
1653
1719
  this.idBase = `fg${Math.random().toString(36).slice(2, 8)}`;
1654
1720
  this.painted = { left: /* @__PURE__ */ new Map(), center: /* @__PURE__ */ new Map(), right: /* @__PURE__ */ new Map() };
1721
+ /**
1722
+ * Scroller geometry read once per frame, before any cell is written: reading it after
1723
+ * writing cells would make the browser lay out every painted cell again.
1724
+ */
1725
+ this.geo = { top: 0, left: 0, cw: 0, ch: 0, ow: 0, oh: 0 };
1726
+ /** Sizes written before the rows last frame; when they are unchanged the geometry read at the start still holds. */
1727
+ this.layoutSig = "";
1728
+ /** Row indexes painted this frame: the only ones autoRowHeight has to measure again. */
1729
+ this.touched = /* @__PURE__ */ new Set();
1730
+ /** Measured heights were dropped: measure every painted row next time, not only the repainted ones. */
1731
+ this.measureAll = false;
1655
1732
  this.pool = { left: [], center: [], right: [] };
1656
1733
  this.frame = 0;
1657
1734
  this.headerDirty = true;
@@ -1663,6 +1740,7 @@ var Renderer = class {
1663
1740
  this.summaryEl = null;
1664
1741
  this.summaryPanes = null;
1665
1742
  this.summaryInners = null;
1743
+ this.summaryKey = null;
1666
1744
  /** Summary rows painted inside the scrolling canvases (unpinned mode). */
1667
1745
  this.canvasSummary = { left: [], center: [], right: [] };
1668
1746
  /** Height of unpinned top summary rows; data rows are shifted down by it. */
@@ -1691,7 +1769,7 @@ var Renderer = class {
1691
1769
  if (this.frame) cancelAnimationFrame(this.frame);
1692
1770
  this.frame = 0;
1693
1771
  this.render();
1694
- this.grid.events.emit("scroll", this.getScroll());
1772
+ this.grid.events.emit("scroll", this.cards ? this.getScroll() : { top: this.geo.top, left: this.geo.left });
1695
1773
  };
1696
1774
  this.onSideWheel = (e) => {
1697
1775
  if (!e.deltaY) return;
@@ -1757,6 +1835,10 @@ var Renderer = class {
1757
1835
  get activeScroller() {
1758
1836
  return this.cards?.scroller ?? this.scroller;
1759
1837
  }
1838
+ readGeo() {
1839
+ const s = this.scroller;
1840
+ this.geo = { top: s.scrollTop, left: s.scrollLeft, cw: s.clientWidth, ch: s.clientHeight, ow: s.offsetWidth, oh: s.offsetHeight };
1841
+ }
1760
1842
  getScroll() {
1761
1843
  const s = this.activeScroller;
1762
1844
  return { top: s.scrollTop, left: s.scrollLeft };
@@ -1804,7 +1886,8 @@ var Renderer = class {
1804
1886
  render() {
1805
1887
  const { columns, rowModel, store, options } = this.grid;
1806
1888
  const { rowHeight: rh, headerHeight: hh } = this.opts;
1807
- const vScrollbar = this.scroller.offsetWidth - this.scroller.clientWidth;
1889
+ this.readGeo();
1890
+ const vScrollbar = this.geo.ow - this.geo.cw;
1808
1891
  if (columns.setContainerWidth(Math.max(0, this.main.clientWidth - vScrollbar))) {
1809
1892
  this.headerDirty = true;
1810
1893
  this.version++;
@@ -1845,7 +1928,12 @@ var Renderer = class {
1845
1928
  this.root.classList.add("fg-auto-height");
1846
1929
  this.bodyEl.style.height = `${this.rowsHeight(count) + this.topOffset + bottomExtra + Math.max(0, this.scroller.offsetHeight - this.scroller.clientHeight)}px`;
1847
1930
  }
1848
- const hScrollbar = this.scroller.offsetHeight - this.scroller.clientHeight;
1931
+ const sig = `${headerHeight}|${REGIONS3.map((r) => columns.regionWidth(r)).join(",")}|${this.rowsHeight(count) + this.topOffset + bottomExtra}|${summary && sumPinned ? sumPos : ""}|${sumRows}|${options.autoHeight ? 1 : 0}`;
1932
+ if (sig !== this.layoutSig) {
1933
+ this.layoutSig = sig;
1934
+ this.readGeo();
1935
+ }
1936
+ const hScrollbar = this.geo.oh - this.geo.ch;
1849
1937
  this.bodyPanes.left.style.marginBottom = this.bodyPanes.right.style.marginBottom = `${hScrollbar}px`;
1850
1938
  const g = this.gridEl;
1851
1939
  g.setAttribute("role", options.treeData || this.grid.getGroupBy().length ? "treegrid" : "grid");
@@ -1868,7 +1956,7 @@ var Renderer = class {
1868
1956
  this.orderSections(summary && sumPinned ? sumPos : null);
1869
1957
  this.paintOverlay(count);
1870
1958
  this.overlay.style.top = `${headerHeight}px`;
1871
- const { scrollTop, scrollLeft } = this.scroller;
1959
+ const { top: scrollTop, left: scrollLeft } = this.geo;
1872
1960
  this.bodyPanes.left.scrollTop = this.bodyPanes.right.scrollTop = scrollTop;
1873
1961
  this.headerInners.center.style.transform = `translateX(${-scrollLeft}px)`;
1874
1962
  this.filterRow?.setTransform(scrollLeft);
@@ -1951,9 +2039,12 @@ var Renderer = class {
1951
2039
  const s = section ?? new Section(`fg-pinned-rows fg-pinned-${position}`);
1952
2040
  const { columns } = this.grid;
1953
2041
  const rh = this.opts.rowHeight;
1954
- s.size(columns, rows.length * rh, this.scroller.offsetWidth - this.scroller.clientWidth);
2042
+ s.size(columns, rows.length * rh, this.geo.ow - this.geo.cw);
1955
2043
  const center = columns.region("center");
1956
- const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
2044
+ const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.geo.left, this.geo.cw, COL_OVERSCAN);
2045
+ const key = [rows, rows.length, this.version, this.grid.valuesVersion, cr.first, cr.last, rh, this.grid.store.state.locale.code];
2046
+ if (section && sameKey(key, s.key)) return s;
2047
+ s.key = key;
1957
2048
  for (const r of REGIONS3) {
1958
2049
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
1959
2050
  s.inners[r].replaceChildren(...rows.map((data, k) => {
@@ -1984,12 +2075,12 @@ var Renderer = class {
1984
2075
  const { grid } = this;
1985
2076
  const state = grid.store.state;
1986
2077
  const total = grid.rowModel.server ? count : grid.viewRows().length;
1987
- const key = `${this.version}|${count}|${total}|${state.selected.size}|${JSON.stringify(state.range)}|${state.locale.code}`;
2078
+ const key = `${this.version}.${grid.valuesVersion}|${count}|${total}|${state.selected.size}|${JSON.stringify(state.range)}|${state.locale.code}`;
1988
2079
  if (this.statusEl && key === this.statusKey) return;
1989
2080
  this.statusKey = key;
1990
2081
  this.statusEl ?? (this.statusEl = el("div", "fg-statusbar", { role: "status", "aria-live": "polite" }));
1991
2082
  const loc = state.locale;
1992
- const nf = new Intl.NumberFormat(loc.code, { maximumFractionDigits: 2 });
2083
+ const nf = numberFormat(loc.code, 2);
1993
2084
  const left = el("span", "fg-status-rows");
1994
2085
  left.textContent = (count === total ? loc.statusRows.replace("{n}", nf.format(total)) : loc.statusFiltered.replace("{shown}", nf.format(count)).replace("{total}", nf.format(total))) + (state.selected.size ? ` \xB7 ${loc.statusSelected.replace("{n}", nf.format(state.selected.size))}` : "");
1995
2086
  const right = el("span", "fg-status-aggs");
@@ -2098,27 +2189,39 @@ var Renderer = class {
2098
2189
  return cell;
2099
2190
  }
2100
2191
  selectAllBox(locale) {
2101
- const rows = this.grid.selectableRows();
2102
- const sel = this.grid.store.state.selected;
2103
- let n = 0;
2104
- for (const r of rows) if (sel.has(r.id)) n++;
2105
2192
  const box = el("input", "fg-checkbox fg-select-all", { type: "checkbox", tabindex: "-1", "aria-label": locale.selectAllRows });
2106
- box.checked = rows.length > 0 && n === rows.length;
2107
- box.indeterminate = n > 0 && n < rows.length;
2193
+ this.paintSelectAll(box);
2108
2194
  box.hidden = this.grid.options.rowSelection !== "multiple" || this.grid.options.headerCheckbox === false;
2109
2195
  return box;
2110
2196
  }
2197
+ paintSelectAll(box) {
2198
+ const ids = this.grid.selectableIds();
2199
+ let n = 0;
2200
+ for (const id of this.grid.store.state.selected) if (ids.has(id)) n++;
2201
+ box.checked = ids.size > 0 && n === ids.size;
2202
+ box.indeterminate = n > 0 && n < ids.size;
2203
+ }
2204
+ /** Only the selection changed: update the select-all box instead of building the header again. */
2205
+ selectionChanged() {
2206
+ const boxes = this.headerEl.querySelectorAll(".fg-select-all");
2207
+ if (!boxes.length) return;
2208
+ for (const box of boxes) this.paintSelectAll(box);
2209
+ }
2111
2210
  renderRows(count) {
2112
2211
  const { columns, rowModel, store } = this.grid;
2113
2212
  const { selected, focus } = store.state;
2114
2213
  const allBounds = this.grid.allRangeBounds();
2115
2214
  const spans = this.grid.mergeSpans();
2116
- const top = Math.max(0, this.scroller.scrollTop - this.topOffset);
2117
- const range = this.grid.options.autoHeight && !rowModel.server ? { first: 0, last: count - 1 } : this.offsets ? rowRangeByOffsets(this.offsets, top, this.scroller.clientHeight, count, ROW_OVERSCAN) : rowRange(top, this.scroller.clientHeight, this.opts.rowHeight, count, ROW_OVERSCAN);
2215
+ const { geo } = this;
2216
+ const top = Math.max(0, geo.top - this.topOffset);
2217
+ const range = this.grid.options.autoHeight && !rowModel.server ? { first: 0, last: count - 1 } : this.offsets ? rowRangeByOffsets(this.offsets, top, geo.ch, count, ROW_OVERSCAN) : rowRange(top, geo.ch, this.opts.rowHeight, count, ROW_OVERSCAN);
2118
2218
  this.lastRange = range;
2119
2219
  const center = columns.region("center");
2120
- const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
2121
- if (center.some((c) => c.def.colSpan)) cr.first = Math.max(0, cr.first - MAX_COLSPAN_REACH);
2220
+ const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.geo.left, this.geo.cw, COL_OVERSCAN);
2221
+ const colSpans = center.some((c) => c.def.colSpan);
2222
+ if (colSpans) cr.first = Math.max(0, cr.first - MAX_COLSPAN_REACH);
2223
+ const slide = !colSpans && !spans.size;
2224
+ this.touched.clear();
2122
2225
  for (const r of REGIONS3) {
2123
2226
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
2124
2227
  const map = this.painted[r];
@@ -2137,8 +2240,9 @@ var Renderer = class {
2137
2240
  const hits = allBounds.filter((b) => i >= b.r1 && i <= b.r2);
2138
2241
  const inRange = hits.map((b) => `${b.c1}-${b.c2}`).join(",");
2139
2242
  const merge2 = this.mergeState(spans, cols, i, range.first);
2140
- const pos = node.kind === "detail" ? `${this.scroller.scrollLeft}:${this.scroller.clientWidth}` : "";
2141
- const key = `${node.id}|${this.version}.${this.rowVersions.get(node.id) ?? 0}|${colKey}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
2243
+ const pos = node.kind === "detail" ? `${geo.left}:${geo.cw}` : "";
2244
+ const base = `${node.id}|${this.version}.${this.rowVersions.get(node.id) ?? 0}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
2245
+ const key = `${colKey}|${base}`;
2142
2246
  let row = map.get(i);
2143
2247
  if (!row) {
2144
2248
  const rowEl = this.pool[r].pop() ?? el("div", "fg-row", { role: "row" });
@@ -2147,7 +2251,15 @@ var Renderer = class {
2147
2251
  map.set(i, row);
2148
2252
  }
2149
2253
  if (row.key === key) continue;
2254
+ this.touched.add(i);
2255
+ if (slide && r === "center" && !node.kind && row.base === base && this.slideCells(row, node, i, center, cr, isSel, focusCol, hits)) {
2256
+ row.key = key;
2257
+ continue;
2258
+ }
2150
2259
  row.key = key;
2260
+ row.base = r === "center" && !node.kind ? base : void 0;
2261
+ row.first = cr.first;
2262
+ row.last = cr.last;
2151
2263
  if (row.el.dataset.kind !== (node.kind ?? "")) {
2152
2264
  row.el.replaceChildren();
2153
2265
  row.el.dataset.kind = node.kind ?? "";
@@ -2159,6 +2271,29 @@ var Renderer = class {
2159
2271
  }
2160
2272
  }
2161
2273
  }
2274
+ /**
2275
+ * Same row, only the column window moved: drop the cells that left and paint the ones that came in.
2276
+ * False when the windows do not overlap (a full paint is as cheap).
2277
+ */
2278
+ slideCells(row, node, i, center, cr, selected, focusCol, bounds) {
2279
+ const of = row.first, ol = row.last;
2280
+ if (cr.first > ol || cr.last < of || row.el.childElementCount !== ol - of + 1) return false;
2281
+ const rowEl = row.el;
2282
+ for (let k = of; k < cr.first; k++) rowEl.firstElementChild.remove();
2283
+ for (let k = ol; k > cr.last; k--) rowEl.lastElementChild.remove();
2284
+ const paint = (col) => {
2285
+ const cellEl = el("div", "fg-cell", { role: "gridcell" });
2286
+ this.paintCell(cellEl, node, i, col, selected, focusCol === col.id);
2287
+ if (col.id === this.firstDataId) this.decorate(cellEl, node);
2288
+ if (!isSystemCol(col.id) && bounds.some((b) => col.index >= b.c1 && col.index <= b.c2)) cellEl.classList.add("fg-cell-range");
2289
+ return cellEl;
2290
+ };
2291
+ if (cr.first < of) rowEl.prepend(...center.slice(cr.first, of).map(paint));
2292
+ if (cr.last > ol) rowEl.append(...center.slice(ol + 1, cr.last + 1).map(paint));
2293
+ row.first = cr.first;
2294
+ row.last = cr.last;
2295
+ return true;
2296
+ }
2162
2297
  /** Per-row merge signature: n = not merged, d = draws the merged block, h = hidden inside a block. */
2163
2298
  mergeState(spans, cols, i, first) {
2164
2299
  if (!spans.size) return "";
@@ -2379,19 +2514,26 @@ var Renderer = class {
2379
2514
  this.summaryInners[r].style.width = `${w}px`;
2380
2515
  this.summaryInners[r].style.height = `${height}px`;
2381
2516
  }
2382
- const vScrollbar = this.scroller.offsetWidth - this.scroller.clientWidth;
2517
+ const vScrollbar = this.geo.ow - this.geo.cw;
2383
2518
  this.summaryPanes.center.style.marginRight = `${vScrollbar}px`;
2384
2519
  }
2385
2520
  renderSummary(count, defs, values, pinned, position) {
2521
+ if (!defs && !this.summaryKey) return;
2522
+ const center = this.grid.columns.region("center");
2523
+ const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.geo.left, this.geo.cw, COL_OVERSCAN);
2524
+ const key = [defs, values, pinned, position, this.summaryInners, this.version, cr.first, cr.last, this.opts.rowHeight, pinned ? 0 : this.rowsHeight(count), this.grid.store.state.locale.code];
2525
+ if (sameKey(key, this.summaryKey)) return;
2526
+ this.summaryKey = key;
2386
2527
  for (const r of REGIONS3) {
2387
2528
  for (const old of this.canvasSummary[r]) old.remove();
2388
2529
  this.canvasSummary[r] = [];
2389
2530
  }
2390
- if (!defs) return;
2531
+ if (!defs) {
2532
+ this.summaryKey = null;
2533
+ return;
2534
+ }
2391
2535
  const { columns } = this.grid;
2392
2536
  const rh = this.opts.rowHeight;
2393
- const center = columns.region("center");
2394
- const cr = colRange(center.map((c) => c.left), center.map((c) => c.width), this.scroller.scrollLeft, this.scroller.clientWidth, COL_OVERSCAN);
2395
2537
  const dataCols = columns.displayed().filter((c) => !isSystemCol(c.id));
2396
2538
  for (const r of REGIONS3) {
2397
2539
  const cols = r === "center" ? center.slice(cr.first, cr.last + 1) : columns.region(r);
@@ -2433,10 +2575,10 @@ var Renderer = class {
2433
2575
  let text2;
2434
2576
  if (value == null || value === "") text2 = "";
2435
2577
  else if (typeof value === "string") text2 = value;
2436
- else if (s.agg === "count") text2 = new Intl.NumberFormat(code).format(value);
2578
+ else if (s.agg === "count") text2 = numberFormat(code).format(value);
2437
2579
  else {
2438
2580
  const type = cd.type ?? (typeof value === "number" ? "number" : value instanceof Date ? "date" : void 0);
2439
- text2 = getFormatter({ ...cd, type, format: s.format ?? cd.format }, code)(value);
2581
+ text2 = getFormatterAs(cd, type, code, s.format ?? cd.format)(value);
2440
2582
  }
2441
2583
  cell.textContent = s.prefix && text2 ? `${s.prefix} ${text2}` : text2;
2442
2584
  return cell;
@@ -2458,24 +2600,30 @@ var Renderer = class {
2458
2600
  if (this.heightOf) this.heightsVersion++;
2459
2601
  if (!this.rowHeights.size) return;
2460
2602
  this.rowHeights.clear();
2603
+ this.measureAll = true;
2461
2604
  this.heightsVersion++;
2462
2605
  }
2463
2606
  /** Measure wrapped cells of the painted rows; when a height changes, lay out again next frame. */
2464
2607
  measureRows() {
2465
2608
  const base = this.opts.rowHeight;
2466
2609
  const need = /* @__PURE__ */ new Map();
2467
- for (const r of REGIONS3) {
2468
- for (const [i, row] of this.painted[r]) {
2469
- const node = this.grid.rowModel.getRow(i);
2470
- if (!node || node.kind) continue;
2471
- let h = need.get(i) ?? base;
2610
+ const rows = this.measureAll ? new Set(REGIONS3.flatMap((r) => [...this.painted[r].keys()])) : this.touched;
2611
+ this.measureAll = false;
2612
+ for (const i of rows) {
2613
+ const node = this.grid.rowModel.getRow(i);
2614
+ if (!node || node.kind) continue;
2615
+ let h = base;
2616
+ for (const r of REGIONS3) {
2617
+ const row = this.painted[r].get(i);
2618
+ if (!row) continue;
2472
2619
  for (const cell of row.el.querySelectorAll(".fg-cell-wrap")) {
2473
2620
  if (cell.classList.contains("fg-cell-merged") || cell.classList.contains("fg-cell-merge-hidden")) continue;
2474
2621
  h = Math.max(h, Math.ceil(cell.scrollHeight));
2475
2622
  }
2476
- need.set(i, h);
2477
2623
  }
2624
+ need.set(i, h);
2478
2625
  }
2626
+ this.touched.clear();
2479
2627
  let changed = false;
2480
2628
  for (const [i, h] of need) {
2481
2629
  const id = this.grid.rowModel.getRow(i).id;
@@ -2596,7 +2744,7 @@ var Renderer = class {
2596
2744
  const def2 = grid.columns.getNode(g.colId)?.def;
2597
2745
  const label = el("span", "fg-group-label");
2598
2746
  const value2 = isBlank(g.value) ? loc.blanks : def2 ? getFormatter(def2, loc.code)(g.value) : String(g.value);
2599
- const nf = new Intl.NumberFormat(loc.code);
2747
+ const nf = numberFormat(loc.code);
2600
2748
  const count = g.count ?? (g.aggregates ? null : g.rows.length);
2601
2749
  label.textContent = `${def2?.header ?? def2?.field ?? g.colId}: ${value2}${count == null ? "" : ` (${nf.format(count)})`}`;
2602
2750
  cell.replaceChildren(toggle, label);
@@ -2607,10 +2755,10 @@ var Renderer = class {
2607
2755
  cell.className = `fg-cell fg-align-${align}${focused}`;
2608
2756
  const value = def.aggFunc && !isSystemCol(col.id) ? grid.groupAggregate(node, col.id) : void 0;
2609
2757
  if (value == null || value === "") cell.textContent = "";
2610
- else if (def.aggFunc === "count") cell.textContent = new Intl.NumberFormat(loc.code).format(value);
2758
+ else if (def.aggFunc === "count") cell.textContent = numberFormat(loc.code).format(value);
2611
2759
  else {
2612
2760
  const type = def.type ?? (typeof value === "number" ? "number" : value instanceof Date ? "date" : void 0);
2613
- cell.textContent = getFormatter({ ...def, type }, loc.code)(value);
2761
+ cell.textContent = getFormatterAs(def, type, loc.code)(value);
2614
2762
  }
2615
2763
  if (cell.textContent) cell.classList.add("fg-cell-agg");
2616
2764
  });
@@ -2637,8 +2785,8 @@ var Renderer = class {
2637
2785
  }
2638
2786
  this.details.set(master.id, panel);
2639
2787
  }
2640
- panel.style.left = `${this.scroller.scrollLeft}px`;
2641
- panel.style.width = `${this.scroller.clientWidth || this.grid.columns.regionWidth("center")}px`;
2788
+ panel.style.left = `${this.geo.left}px`;
2789
+ panel.style.width = `${this.geo.cw || this.grid.columns.regionWidth("center")}px`;
2642
2790
  if (rowEl.firstChild !== panel || rowEl.childNodes.length !== 1) rowEl.replaceChildren(panel);
2643
2791
  }
2644
2792
  /** Server rows that are on their way: grey bars in the data cells. */
@@ -2688,7 +2836,7 @@ var Renderer = class {
2688
2836
  });
2689
2837
  }
2690
2838
  this.pagerEl.setAttribute("aria-label", loc.pageSize);
2691
- const nf = new Intl.NumberFormat(loc.code);
2839
+ const nf = numberFormat(loc.code);
2692
2840
  const from = total ? page * size + 1 : 0, to = Math.min(total, (page + 1) * size);
2693
2841
  const range = el("span", "fg-pager-range");
2694
2842
  range.textContent = loc.pagerRange.replace("{from}", nf.format(from)).replace("{to}", nf.format(to)).replace("{total}", nf.format(total));
@@ -2855,6 +3003,8 @@ var Renderer = class {
2855
3003
  };
2856
3004
  var Section = class {
2857
3005
  constructor(cls) {
3006
+ /** What the rows were last built from. */
3007
+ this.key = null;
2858
3008
  const mk = (c) => ({
2859
3009
  left: el("div", `${c} ${c}-left`),
2860
3010
  center: el("div", `${c} ${c}-center`),
@@ -2882,6 +3032,9 @@ var Section = class {
2882
3032
  this.panes.center.style.marginRight = `${vScrollbar}px`;
2883
3033
  }
2884
3034
  };
3035
+ function sameKey(a, b) {
3036
+ return !!b && a.length === b.length && a.every((k, i) => k === b[i]);
3037
+ }
2885
3038
  var SVG_NS = "http://www.w3.org/2000/svg";
2886
3039
  function sparkline(nums, opt, width, rowHeight) {
2887
3040
  const w = Math.max(20, width - 16), h = Math.max(10, rowHeight - 14);
@@ -2893,8 +3046,8 @@ function sparkline(nums, opt, width, rowHeight) {
2893
3046
  svgEl.setAttribute("aria-hidden", "true");
2894
3047
  const color = opt.color ?? "var(--fg-accent)";
2895
3048
  const bars = opt.type === "bar";
2896
- let min = bars ? Math.min(0, ...nums) : Math.min(...nums);
2897
- let max = bars ? Math.max(0, ...nums) : Math.max(...nums);
3049
+ let min = nums.reduce((m, v) => Math.min(m, v), bars ? 0 : Infinity);
3050
+ let max = nums.reduce((m, v) => Math.max(m, v), bars ? 0 : -Infinity);
2898
3051
  if (min === max) {
2899
3052
  min -= 1;
2900
3053
  max += 1;
@@ -4422,27 +4575,69 @@ var Finder = class {
4422
4575
  this.lookup.clear();
4423
4576
  this.index = -1;
4424
4577
  if (this.query) {
4425
- const { grid } = this;
4426
- const code = grid.store.state.locale.code;
4427
- const cols = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
4428
- const fmts = cols.map((c) => getFormatter(c.def, code));
4429
- const rows = grid.rowModel.rows();
4430
- outer: for (let r = 0; r < rows.length; r++) {
4431
- if (rows[r].kind) continue;
4432
- for (let c = 0; c < cols.length; c++) {
4433
- const text3 = fmts[c](getCellValue(cols[c].def, rows[r].data)).toLowerCase();
4434
- if (!text3.includes(this.query)) continue;
4435
- this.lookup.set(`${r}|${cols[c].id}`, this.matches.length);
4436
- this.matches.push({ rowIndex: r, colId: cols[c].id });
4437
- if (this.matches.length >= MAX_MATCHES) break outer;
4438
- }
4439
- }
4578
+ const rows = this.grid.rowModel.rows();
4579
+ const scan = this.scanner();
4580
+ for (let r = 0; r < rows.length && this.matches.length < MAX_MATCHES; r++) scan(r, this.matches);
4581
+ this.matches.length = Math.min(this.matches.length, MAX_MATCHES);
4582
+ this.reindex();
4440
4583
  if (this.matches.length) this.go(0);
4441
4584
  }
4442
4585
  this.updateCount();
4443
4586
  this.grid.renderer.invalidate({ rows: true });
4444
4587
  return this.matches.length;
4445
4588
  }
4589
+ /** Adds the matching cells of one display row, in column order. */
4590
+ scanner() {
4591
+ const { grid } = this;
4592
+ const code = grid.store.state.locale.code;
4593
+ const cols = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
4594
+ const fmts = cols.map((c) => getFormatter(c.def, code));
4595
+ const rows = grid.rowModel.rows();
4596
+ return (r, out) => {
4597
+ const node = rows[r];
4598
+ if (!node || node.kind) return;
4599
+ for (let c = 0; c < cols.length; c++) {
4600
+ if (fmts[c](getCellValue(cols[c].def, node.data)).toLowerCase().includes(this.query)) out.push({ rowIndex: r, colId: cols[c].id });
4601
+ }
4602
+ };
4603
+ }
4604
+ reindex() {
4605
+ this.lookup.clear();
4606
+ this.matches.forEach((m, i) => this.lookup.set(`${m.rowIndex}|${m.colId}`, i));
4607
+ }
4608
+ /**
4609
+ * Values changed in place (same rows, same order): check only those rows again
4610
+ * instead of formatting every cell of the grid.
4611
+ */
4612
+ refreshRows(ids) {
4613
+ if (!this.query) return;
4614
+ if (this.matches.length >= MAX_MATCHES) return this.refresh();
4615
+ const { rowModel } = this.grid;
4616
+ const changed = /* @__PURE__ */ new Set();
4617
+ for (const id of ids) {
4618
+ const r = rowModel.indexOfId(id);
4619
+ if (r >= 0) changed.add(r);
4620
+ }
4621
+ if (!changed.size) return;
4622
+ const current = this.matches[this.index];
4623
+ const scan = this.scanner();
4624
+ const fresh = [];
4625
+ for (const r of changed) scan(r, fresh);
4626
+ const before = this.matches.length;
4627
+ const old = this.matches.filter((m) => changed.has(m.rowIndex));
4628
+ const sig = (list) => list.map((m) => `${m.rowIndex}|${m.colId}`).sort().join(",");
4629
+ if (sig(old) === sig(fresh)) return;
4630
+ const order = new Map(this.grid.columns.displayed().map((c, i) => [c.id, i]));
4631
+ this.matches = [...this.matches.filter((m) => !changed.has(m.rowIndex)), ...fresh].sort((a, b) => a.rowIndex - b.rowIndex || (order.get(a.colId) ?? 0) - (order.get(b.colId) ?? 0)).slice(0, MAX_MATCHES);
4632
+ this.reindex();
4633
+ const kept = current ? this.lookup.get(`${current.rowIndex}|${current.colId}`) : void 0;
4634
+ const was = this.index;
4635
+ if (kept !== void 0) this.index = kept;
4636
+ else if (this.matches.length) this.index = Math.min(Math.max(this.index, 0), this.matches.length - 1);
4637
+ else this.index = -1;
4638
+ if (before !== this.matches.length || was !== this.index || kept === void 0) this.updateCount();
4639
+ this.grid.renderer.invalidate({ rows: true });
4640
+ }
4446
4641
  /** Re-run the current query after data, sort or filter changes. */
4447
4642
  refresh() {
4448
4643
  if (this.query) this.find(this.query);
@@ -4534,7 +4729,7 @@ function fillSeries(source, count) {
4534
4729
  const last2 = nums[nums.length - 1];
4535
4730
  return Array.from({ length: count }, (_, i) => Math.round((last2 + step * (i + 1)) * 1e12) / 1e12);
4536
4731
  }
4537
- const dates = source.map((v) => v instanceof Date || typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v) ? toDate2(v) : null);
4732
+ const dates = source.map((v) => v instanceof Date || typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v) ? toDate(v) : null);
4538
4733
  if (dates.every(Boolean) && source.length >= 2) {
4539
4734
  const times = dates.map((d) => Math.round(d.getTime() / DAY2));
4540
4735
  const step = times[1] - times[0];
@@ -4543,7 +4738,7 @@ function fillSeries(source, count) {
4543
4738
  const asDate = source[source.length - 1] instanceof Date;
4544
4739
  return Array.from({ length: count }, (_, i) => {
4545
4740
  const d = new Date(lastDate.getFullYear(), lastDate.getMonth(), lastDate.getDate() + step * (i + 1));
4546
- return asDate ? d : toDateKey2(d);
4741
+ return asDate ? d : toDateKey(d);
4547
4742
  });
4548
4743
  }
4549
4744
  return repeat();
@@ -4634,7 +4829,7 @@ function copyValue(def, value, locale) {
4634
4829
  return Number.isNaN(n) ? String(value) : String(n);
4635
4830
  }
4636
4831
  case "date":
4637
- return toDateKey2(value) ?? String(value);
4832
+ return toDateKey(value) ?? String(value);
4638
4833
  case "boolean":
4639
4834
  return value === true ? "TRUE" : value === false ? "FALSE" : String(value);
4640
4835
  default:
@@ -4658,7 +4853,8 @@ function parseNumber(text2) {
4658
4853
  let n = Number(s);
4659
4854
  if (percent) n /= 100;
4660
4855
  if (negative) n = -n;
4661
- return { ok: true, value: Math.round(n * 1e12) / 1e12 };
4856
+ if (!Number.isFinite(n)) return { ok: false };
4857
+ return { ok: true, value: Math.abs(n) < 1e15 ? Math.round(n * 1e12) / 1e12 : n };
4662
4858
  }
4663
4859
  function validDate(y, m, d) {
4664
4860
  const date = new Date(y, m - 1, d);
@@ -4671,9 +4867,9 @@ function parseDate(text2, previous) {
4671
4867
  let m = /^(\d{4})\s*[-./년]\s*(\d{1,2})\s*[-./월]\s*(\d{1,2})\s*[.일]?$/.exec(s);
4672
4868
  if (m) date = validDate(+m[1], +m[2], +m[3]);
4673
4869
  else if (m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s)) date = validDate(+m[3], +m[1], +m[2]);
4674
- else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date = toDate2(s);
4870
+ else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date = toDate(s);
4675
4871
  if (!date) return { ok: false };
4676
- return { ok: true, value: previous instanceof Date ? date : toDateKey2(date) };
4872
+ return { ok: true, value: previous instanceof Date ? date : toDateKey(date) };
4677
4873
  }
4678
4874
  function parseValue(def, text2, previous) {
4679
4875
  switch (def.type) {
@@ -4825,7 +5021,7 @@ var CellEditor = class {
4825
5021
  input.type = def.type === "date" && initialText === void 0 ? "date" : "text";
4826
5022
  if (def.type === "number") input.inputMode = "decimal";
4827
5023
  }
4828
- const start = def.type === "date" ? toDateKey2(value) ?? "" : isBlank(value) ? "" : String(value);
5024
+ const start = def.type === "date" ? toDateKey(value) ?? "" : isBlank(value) ? "" : String(value);
4829
5025
  input.value = initialText ?? start;
4830
5026
  control = input;
4831
5027
  read = () => ({ text: input.value });
@@ -5836,8 +6032,26 @@ function argbOf(c) {
5836
6032
  }
5837
6033
 
5838
6034
  // src/rows/sort.ts
6035
+ var PLAIN_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
6036
+ function numeric(v) {
6037
+ if (typeof v === "number") return v;
6038
+ if (typeof v !== "string" || !v) return NaN;
6039
+ const c = v.charCodeAt(0);
6040
+ if (!(c >= 48 && c <= 57 || c === 43 || c === 45 || c === 46)) return NaN;
6041
+ return PLAIN_NUMBER.test(v) ? Number(v) : NaN;
6042
+ }
5839
6043
  function compareNonBlank(a, b, collator) {
5840
6044
  if (typeof a === "number" && typeof b === "number") return a - b;
6045
+ if (typeof a === "number" || typeof b === "number") {
6046
+ const x = numeric(a), y = numeric(b);
6047
+ const nx = !Number.isNaN(x), ny = !Number.isNaN(y);
6048
+ if (nx && ny) return x - y;
6049
+ if (nx !== ny) return nx ? -1 : 1;
6050
+ } else if (typeof a === "string" && typeof b === "string") {
6051
+ const x = numeric(a), y = numeric(b);
6052
+ if (!Number.isNaN(x) && !Number.isNaN(y)) return x - y;
6053
+ if (!Number.isNaN(x) || !Number.isNaN(y)) return Number.isNaN(x) ? 1 : -1;
6054
+ }
5841
6055
  if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
5842
6056
  if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
5843
6057
  return collator.compare(String(a), String(b));
@@ -5848,7 +6062,7 @@ function normalize(def, v) {
5848
6062
  const n = typeof v === "number" ? v : Number(v);
5849
6063
  return Number.isNaN(n) ? null : n;
5850
6064
  }
5851
- if (def.type === "date") return toDate2(v)?.getTime() ?? null;
6065
+ if (def.type === "date") return toDate(v)?.getTime() ?? null;
5852
6066
  return v;
5853
6067
  }
5854
6068
  function compareValues(a, b, collator) {
@@ -5856,6 +6070,11 @@ function compareValues(a, b, collator) {
5856
6070
  if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
5857
6071
  return compareNonBlank(a, b, collator);
5858
6072
  }
6073
+ function compareGroupValues(a, b, collator, dir) {
6074
+ const ba = isBlank(a), bb = isBlank(b);
6075
+ if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
6076
+ return compareNonBlank(a, b, collator) * dir;
6077
+ }
5859
6078
  function sortNodes(nodes, sort, resolve, collator) {
5860
6079
  const keys = sort.map((s) => ({ def: resolve(s.colId), mul: s.dir === "desc" ? -1 : 1 })).filter((k) => !!k.def);
5861
6080
  if (!keys.length) return nodes;
@@ -6075,7 +6294,7 @@ function sheetNames(sheets) {
6075
6294
  return sheets.map((s, i) => {
6076
6295
  let base = (s.name ?? `Sheet${i + 1}`).replace(/[[\]:*?/\\]/g, " ").slice(0, 31).trim() || `Sheet${i + 1}`;
6077
6296
  let name = base;
6078
- for (let k = 2; used.has(name.toLowerCase()); k++) name = `${base.slice(0, 28)} (${k})`;
6297
+ for (let k = 2; used.has(name.toLowerCase()); k++) name = `${base.slice(0, 31 - ` (${k})`.length)} (${k})`;
6079
6298
  used.add(name.toLowerCase());
6080
6299
  return name;
6081
6300
  });
@@ -6299,7 +6518,7 @@ function exportRows(grid, mode) {
6299
6518
  }
6300
6519
  var header = (def, id) => def.header ?? def.field ?? id;
6301
6520
  var ALIGNS = ["left", "center", "right"];
6302
- var PLAIN_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
6521
+ var PLAIN_NUMBER2 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
6303
6522
  var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
6304
6523
  var PAPERS = ["A4", "A3", "A5", "B4", "B5", "letter", "legal"];
6305
6524
  var PORTRAIT_WIDTH = 760;
@@ -6327,7 +6546,7 @@ function printLines(grid, rows, groupBy) {
6327
6546
  b.rows.push(r);
6328
6547
  }
6329
6548
  const dir = sort.find((x) => x.colId === colId)?.dir === "desc" ? -1 : 1;
6330
- for (const b of [...buckets.values()].sort((a, c) => compareValues(a.value, c.value, collator) * dir)) {
6549
+ for (const b of [...buckets.values()].sort((a, c) => compareGroupValues(a.value, c.value, collator, dir))) {
6331
6550
  out.push({ kind: "group", colId, value: b.value, rows: b.rows, level });
6332
6551
  walk(b.rows, level + 1);
6333
6552
  }
@@ -6362,7 +6581,7 @@ function printModel(grid, opts = {}) {
6362
6581
  const loc = grid.store.state.locale;
6363
6582
  const code = loc.code;
6364
6583
  const fmts = cols.map((c) => getFormatter(c.def, code));
6365
- const nf = new Intl.NumberFormat(code);
6584
+ const nf = numberFormat(code);
6366
6585
  const numbered = opts.rowNumbers ?? !!grid.options.rowNumbers;
6367
6586
  const align = (def) => ALIGNS.includes(def.align) ? def.align : def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left";
6368
6587
  const chains = cols.map((c) => {
@@ -6395,7 +6614,7 @@ function printModel(grid, opts = {}) {
6395
6614
  if (v == null || v === "") return "";
6396
6615
  if (agg === "count") return nf.format(v);
6397
6616
  const type = c.def.type ?? (typeof v === "number" ? "number" : v instanceof Date ? "date" : void 0);
6398
- return type === c.def.type ? fmts[i](v) : getFormatter({ ...c.def, type }, code)(v);
6617
+ return type === c.def.type ? fmts[i](v) : getFormatterAs(c.def, type, code)(v);
6399
6618
  };
6400
6619
  const labelled = (kind, label, values, level = 0) => {
6401
6620
  let lead = 0;
@@ -6592,7 +6811,7 @@ function buildCsv(grid, opts = {}) {
6592
6811
  const v = getCellValue(c.def, r);
6593
6812
  const t = c.def.type;
6594
6813
  const value = copyValue(c.def, v, code);
6595
- return { value, text: !(t === "number" || t === "date" || t === "boolean") || typeof v === "string" && !PLAIN_NUMBER.test(value) };
6814
+ return { value, text: typeof v !== "number" && (!(t === "number" || t === "date" || t === "boolean") || typeof v === "string" && !PLAIN_NUMBER2.test(value)) };
6596
6815
  }));
6597
6816
  if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
6598
6817
  return toCsv(matrix, sep);
@@ -6736,7 +6955,7 @@ function buildXlsxSheet(grid, opts = {}) {
6736
6955
  return Number.isFinite(n) ? { v: n, fmt } : String(v);
6737
6956
  }
6738
6957
  case "date": {
6739
- const d = toDate2(v);
6958
+ const d = toDate(v);
6740
6959
  return d ? { v: d, fmt } : String(v);
6741
6960
  }
6742
6961
  case "boolean":
@@ -6826,7 +7045,7 @@ function asText(v) {
6826
7045
  if (v === null || v === void 0) return "";
6827
7046
  if (v instanceof Date) {
6828
7047
  const hasTime = v.getHours() || v.getMinutes() || v.getSeconds();
6829
- return hasTime ? v.toISOString() : toDateKey2(v) ?? "";
7048
+ return hasTime ? v.toISOString() : toDateKey(v) ?? "";
6830
7049
  }
6831
7050
  return String(v);
6832
7051
  }
@@ -7090,8 +7309,9 @@ function drawScatter(root, spec, series, top, bottom, nf, short) {
7090
7309
  const allX = xs.filter(finite);
7091
7310
  const allY = others.flatMap((s) => s.values.filter(finite));
7092
7311
  if (!allX.length || !allY.length) return;
7093
- const xt = niceTicks(Math.min(...allX), Math.max(...allX));
7094
- const yt = niceTicks(Math.min(...allY), Math.max(...allY));
7312
+ const lo = (a) => a.reduce((m, v) => Math.min(m, v)), hi = (a) => a.reduce((m, v) => Math.max(m, v));
7313
+ const xt = niceTicks(lo(allX), hi(allX));
7314
+ const yt = niceTicks(lo(allY), hi(allY));
7095
7315
  const left = 56, right = W - 16;
7096
7316
  const x = (v) => left + (v - xt[0]) / (xt[xt.length - 1] - xt[0]) * (right - left);
7097
7317
  const y = (v) => bottom - (v - yt[0]) / (yt[yt.length - 1] - yt[0]) * (bottom - top);
@@ -7239,16 +7459,38 @@ function normalizePivot(cfg, known) {
7239
7459
  totals: cfg.totals !== false
7240
7460
  };
7241
7461
  }
7462
+ function samePivot(a, b) {
7463
+ const same3 = (x, y) => x.length === y.length && x.every((v, i) => v === y[i]);
7464
+ return a.totals === b.totals && same3(a.rows, b.rows) && same3(a.columns, b.columns) && a.values.length === b.values.length && a.values.every((v, i) => v.colId === b.values[i].colId && v.aggFunc === b.values[i].aggFunc);
7465
+ }
7242
7466
  function buildPivot(source, config, defOf, labels, locale) {
7243
7467
  const cfg = normalizePivot(config, (id) => !!defOf(id));
7244
7468
  const collator = new Intl.Collator(locale, { numeric: true, sensitivity: "base" });
7245
7469
  const def = (id) => defOf(id);
7246
7470
  const keyOf = (ids, r) => ids.map((id) => setKey(getCellValue(def(id), r))).join("");
7247
7471
  const colCombos = /* @__PURE__ */ new Map();
7472
+ const comboOf2 = /* @__PURE__ */ new WeakMap();
7248
7473
  for (const r of source) {
7249
7474
  const k = keyOf(cfg.columns, r);
7475
+ comboOf2.set(r, k);
7250
7476
  if (!colCombos.has(k)) colCombos.set(k, cfg.columns.map((id) => getCellValue(def(id), r)));
7251
7477
  }
7478
+ const bucketsOf = /* @__PURE__ */ new WeakMap();
7479
+ const groupSource = (rows2) => {
7480
+ let b = bucketsOf.get(rows2);
7481
+ if (!b) {
7482
+ const all2 = rows2.flatMap((r) => SOURCE.get(r) ?? []);
7483
+ const byCombo = /* @__PURE__ */ new Map();
7484
+ for (const r of all2) {
7485
+ const k = comboOf2.get(r) ?? keyOf(cfg.columns, r);
7486
+ let list = byCombo.get(k);
7487
+ if (!list) byCombo.set(k, list = []);
7488
+ list.push(r);
7489
+ }
7490
+ bucketsOf.set(rows2, b = { all: all2, byCombo });
7491
+ }
7492
+ return b;
7493
+ };
7252
7494
  const combos = [...colCombos.entries()].sort((a, b) => {
7253
7495
  for (let i = 0; i < cfg.columns.length; i++) {
7254
7496
  const c = compareValues(a[1][i], b[1][i], collator);
@@ -7263,7 +7505,6 @@ function buildPivot(source, config, defOf, labels, locale) {
7263
7505
  const src = def(v.colId);
7264
7506
  const id = `pivot|${ci}|${vi}`;
7265
7507
  leaves.push({ id, comboKey, valueIndex: vi });
7266
- const matches = (r) => comboKey === null || keyOf(cfg.columns, r) === comboKey;
7267
7508
  const agg = v.aggFunc;
7268
7509
  return {
7269
7510
  id,
@@ -7274,7 +7515,8 @@ function buildPivot(source, config, defOf, labels, locale) {
7274
7515
  width: 130,
7275
7516
  // group rows: aggregate the matching source rows, not the leaf results
7276
7517
  aggFunc: (_values, rows2) => {
7277
- const srcRows = rows2.flatMap((r) => SOURCE.get(r) ?? []).filter(matches);
7518
+ const g = groupSource(rows2);
7519
+ const srcRows = comboKey === null ? g.all : g.byCombo.get(comboKey) ?? [];
7278
7520
  return srcRows.length ? aggregate(agg, srcRows.map((r) => getCellValue(src, r)), srcRows) : null;
7279
7521
  }
7280
7522
  };
@@ -7353,13 +7595,14 @@ function buildPivot(source, config, defOf, labels, locale) {
7353
7595
  }
7354
7596
 
7355
7597
  // src/version.ts
7356
- var version = "1.6.0";
7598
+ var version = "1.7.0";
7357
7599
 
7358
7600
  // src/license.ts
7359
7601
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
7360
7602
  var PRICING_URL = "https://freegrid.app/pricing/";
7361
7603
  var USAGE_URL = "https://freegrid.app/api/usage";
7362
7604
  var STATUS_URL = "https://freegrid.app/api/domain-status";
7605
+ var majorOf = (v) => Number.parseInt(String(v).split(".")[0], 10);
7363
7606
  var b64urlDecode = (s) => {
7364
7607
  const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
7365
7608
  return Uint8Array.from(bin, (c) => c.charCodeAt(0));
@@ -7393,6 +7636,7 @@ async function verifyLicense(key, host, opts = {}) {
7393
7636
  try {
7394
7637
  info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
7395
7638
  if (!Array.isArray(info.domains) || !isDay(info.expires)) return { ok: false, reason: "format" };
7639
+ if (info.major !== void 0 && !(Number.isInteger(info.major) && info.major > 0)) return { ok: false, reason: "format" };
7396
7640
  } catch {
7397
7641
  return { ok: false, reason: "format" };
7398
7642
  }
@@ -7408,6 +7652,7 @@ async function verifyLicense(key, host, opts = {}) {
7408
7652
  }
7409
7653
  if (today(opts.now ?? /* @__PURE__ */ new Date()) > info.expires) return { ok: false, reason: "expired", info };
7410
7654
  if (!domainMatches(host, info.domains)) return { ok: false, reason: "domain", info };
7655
+ if (info.major !== void 0 && majorOf(opts.version ?? version) > info.major) return { ok: false, reason: "version", info };
7411
7656
  return { ok: true, info };
7412
7657
  }
7413
7658
  var globalKey;
@@ -7422,7 +7667,8 @@ FreeGrid\uB294 \uAC1C\uC778\uC5D0\uAC8C \uBB34\uB8CC\uC785\uB2C8\uB2E4. \uD68C\u
7422
7667
  format: "FreeGrid: the license key is not in the right format. / \uB77C\uC774\uC120\uC2A4 \uD0A4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
7423
7668
  signature: "FreeGrid: the license key could not be verified. / \uB77C\uC774\uC120\uC2A4 \uD0A4\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
7424
7669
  expired: "FreeGrid: the license key has expired. / \uB77C\uC774\uC120\uC2A4 \uAE30\uAC04\uC774 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4.",
7425
- domain: "FreeGrid: the license key is for a different domain. / \uB2E4\uB978 \uB3C4\uBA54\uC778\uC6A9 \uB77C\uC774\uC120\uC2A4 \uD0A4\uC785\uB2C8\uB2E4."
7670
+ domain: "FreeGrid: the license key is for a different domain. / \uB2E4\uB978 \uB3C4\uBA54\uC778\uC6A9 \uB77C\uC774\uC120\uC2A4 \uD0A4\uC785\uB2C8\uB2E4.",
7671
+ version: `FreeGrid ${version}: this perpetual license covers an earlier major version. / \uC774 \uC601\uAD6C \uB77C\uC774\uC120\uC2A4\uB294 \uC774\uC804 \uBA54\uC774\uC800 \uBC84\uC804\uC6A9\uC785\uB2C8\uB2E4.`
7426
7672
  };
7427
7673
  function reportUsage(info, host = typeof location !== "undefined" ? location.hostname : "") {
7428
7674
  try {
@@ -7452,7 +7698,7 @@ function checkLicense(key = globalKey, host = typeof location !== "undefined" ?
7452
7698
  status = verifyLicense(k, host).then((res) => {
7453
7699
  reportUsage({ version, licensed: res.ok, company: res.ok ? res.info.company : void 0 }, host);
7454
7700
  if (res.ok || res.reason === "unsupported") return res.ok || res.reason === "unsupported";
7455
- const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.expires})` : "";
7701
+ const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.major ? `${res.info.major}.x` : res.info.expires})` : "";
7456
7702
  console.warn(`${NOTICE[res.reason]}${extra}
7457
7703
  ${PRICING_URL}`);
7458
7704
  return false;
@@ -7789,6 +8035,8 @@ var RowModel = class {
7789
8035
  this.display = [];
7790
8036
  this.indexById = null;
7791
8037
  this.layoutCache = null;
8038
+ /** Searchable text of each row, kept between quick filter keystrokes. */
8039
+ this.quickCache = null;
7792
8040
  }
7793
8041
  update(input) {
7794
8042
  const collator = this.deps.collator();
@@ -7883,17 +8131,29 @@ var RowModel = class {
7883
8131
  this.layoutCache = { rows: this.display, rh: rowHeight, dh: detailHeight, hv: heightsVersion, offsets, uniform };
7884
8132
  return this.layoutCache;
7885
8133
  }
8134
+ quickTexts(cols) {
8135
+ const key = [this.nodes, this.deps.textVersion?.() ?? "", ...cols];
8136
+ const c = this.quickCache;
8137
+ if (c && c.key.length === key.length && c.key.every((k, i) => k === key[i])) return c.texts;
8138
+ this.quickCache = { key, texts: /* @__PURE__ */ new WeakMap() };
8139
+ return this.quickCache.texts;
8140
+ }
7886
8141
  filter(input) {
7887
8142
  const preds = Object.entries(input.filters).map(([colId, model]) => ({ def: this.deps.columnDef(colId), pred: buildPredicate(model) })).filter((p) => !!p.def);
7888
8143
  const tokens = quickTokens(input.quickFilter);
7889
8144
  const quickCols = tokens.length ? this.deps.quickColumns() : [];
7890
8145
  const advanced = input.advancedFilter ? buildAdvanced(input.advancedFilter, (id) => this.deps.columnDef(id), getCellValue) : null;
7891
8146
  if (!preds.length && !tokens.length && !advanced) return this.nodes;
8147
+ const texts = tokens.length ? this.quickTexts(quickCols) : null;
7892
8148
  const matches = this.nodes.filter((n) => {
7893
8149
  for (const p of preds) if (!p.pred(getCellValue(p.def, n.data))) return false;
7894
8150
  if (advanced && !advanced(n.data)) return false;
7895
- if (tokens.length) {
7896
- const text2 = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
8151
+ if (texts) {
8152
+ let text2 = texts.get(n.data);
8153
+ if (text2 === void 0) {
8154
+ text2 = quickCols.map((d) => this.deps.format(d, getCellValue(d, n.data))).join("").toLowerCase();
8155
+ texts.set(n.data, text2);
8156
+ }
7897
8157
  if (!matchesQuick(tokens, text2)) return false;
7898
8158
  }
7899
8159
  return true;
@@ -7952,7 +8212,7 @@ var RowModel = class {
7952
8212
  b.nodes.push(n);
7953
8213
  }
7954
8214
  const dir = sort.find((s) => s.colId === colId)?.dir === "desc" ? -1 : 1;
7955
- const ordered = [...buckets.entries()].sort((a, b) => compareValues(a[1].value, b[1].value, collator) * dir);
8215
+ const ordered = [...buckets.entries()].sort((a, b) => compareGroupValues(a[1].value, b[1].value, collator, dir));
7956
8216
  for (const [k, b] of ordered) {
7957
8217
  const key = `${path}${colId}:${k}`;
7958
8218
  const expanded = this.isOpen(d, key, level);
@@ -7985,6 +8245,25 @@ var RowModel = class {
7985
8245
  list.push(n);
7986
8246
  } else roots.push(n);
7987
8247
  }
8248
+ const reached = /* @__PURE__ */ new Set();
8249
+ const mark = (from) => {
8250
+ const stack = [from];
8251
+ while (stack.length) {
8252
+ const x = stack.pop();
8253
+ if (reached.has(x.id)) continue;
8254
+ reached.add(x.id);
8255
+ for (const k of children.get(x.id) ?? []) stack.push(k);
8256
+ }
8257
+ };
8258
+ for (const r of roots) mark(r);
8259
+ for (const n of this.sorted) {
8260
+ if (reached.has(n.id)) continue;
8261
+ const list = children.get(String(d.getParentId(n.data)));
8262
+ const at = list ? list.indexOf(n) : -1;
8263
+ if (at >= 0) list.splice(at, 1);
8264
+ roots.push(n);
8265
+ mark(n);
8266
+ }
7988
8267
  const visit = (nodes, level, seen2) => {
7989
8268
  for (const n of nodes) {
7990
8269
  if (seen2.has(n.id)) continue;
@@ -8455,6 +8734,47 @@ var Store = class {
8455
8734
  }
8456
8735
  };
8457
8736
 
8737
+ // src/same.ts
8738
+ var plain = (v) => !!v && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
8739
+ var MAX_DEPTH2 = 8;
8740
+ function snapshot(v, depth = 0, seen2 = /* @__PURE__ */ new Map()) {
8741
+ if (depth > MAX_DEPTH2 || !(Array.isArray(v) || plain(v))) return v;
8742
+ const done = seen2.get(v);
8743
+ if (done) return done;
8744
+ if (Array.isArray(v)) {
8745
+ const out2 = [];
8746
+ seen2.set(v, out2);
8747
+ for (const x of v) out2.push(snapshot(x, depth + 1, seen2));
8748
+ return out2;
8749
+ }
8750
+ const out = {};
8751
+ seen2.set(v, out);
8752
+ for (const [k, x] of Object.entries(v)) out[k] = snapshot(x, depth + 1, seen2);
8753
+ return out;
8754
+ }
8755
+ function same2(a, b, depth = 0, seen2 = /* @__PURE__ */ new Map()) {
8756
+ if (a === b) return true;
8757
+ if (depth > MAX_DEPTH2 || !a || !b || typeof a !== "object" || typeof b !== "object") return false;
8758
+ const pairs = seen2.get(a);
8759
+ if (pairs?.has(b)) return true;
8760
+ if (pairs) pairs.add(b);
8761
+ else seen2.set(a, /* @__PURE__ */ new Set([b]));
8762
+ if (Array.isArray(a) || Array.isArray(b)) {
8763
+ return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((x, i) => same2(x, b[i], depth + 1, seen2));
8764
+ }
8765
+ if (!plain(a) || !plain(b)) return false;
8766
+ const keys = Object.keys(a);
8767
+ return keys.length === Object.keys(b).length && keys.every((k) => k in b && same2(a[k], b[k], depth + 1, seen2));
8768
+ }
8769
+ function changeTracker(initial) {
8770
+ let last = snapshot(initial);
8771
+ return (next) => {
8772
+ if (same2(next, last)) return false;
8773
+ last = snapshot(next);
8774
+ return true;
8775
+ };
8776
+ }
8777
+
8458
8778
  // src/interactions/tooltip.ts
8459
8779
  var DELAY = 500;
8460
8780
  function attachTooltips(grid) {
@@ -8827,9 +9147,9 @@ var FormatDialog = class {
8827
9147
  o.textContent = opName(op);
8828
9148
  opSel.append(o);
8829
9149
  }
8830
- const numeric = node.def.type === "number";
8831
- const v1 = el("input", "fg-input fg-cf-value", { type: numeric ? "number" : "text", placeholder: loc.value, "aria-label": loc.value });
8832
- const v2 = el("input", "fg-input fg-cf-value-to", { type: numeric ? "number" : "text", placeholder: loc.valueTo, "aria-label": loc.valueTo });
9150
+ const numeric2 = node.def.type === "number";
9151
+ const v1 = el("input", "fg-input fg-cf-value", { type: numeric2 ? "number" : "text", placeholder: loc.value, "aria-label": loc.value });
9152
+ const v2 = el("input", "fg-input fg-cf-value-to", { type: numeric2 ? "number" : "text", placeholder: loc.valueTo, "aria-label": loc.valueTo });
8833
9153
  const formula = el("input", "fg-input fg-cf-wide fg-cf-formula", { type: "text", placeholder: `=${node.def.field ?? colId} > 100`, "aria-label": cf.types.formula, spellcheck: "false" });
8834
9154
  const rank = el("input", "fg-input fg-cf-rank", { type: "number", min: "1", value: "10", "aria-label": cf.count });
8835
9155
  const pctLabel = el("label", "fg-cf-pct");
@@ -8869,7 +9189,7 @@ var FormatDialog = class {
8869
9189
  const read = () => {
8870
9190
  const type = typeSel.value;
8871
9191
  const rule = { columns: [colId], type };
8872
- const val = (input) => numeric && input.value !== "" ? Number(input.value) : input.value;
9192
+ const val = (input) => numeric2 && input.value !== "" ? Number(input.value) : input.value;
8873
9193
  if (type === "value") {
8874
9194
  rule.op = opSel.value;
8875
9195
  if (!NO_VALUE3.includes(rule.op)) {
@@ -9383,9 +9703,22 @@ var Grid = class {
9383
9703
  }
9384
9704
  };
9385
9705
  this.cfRules = [];
9706
+ this.cfRowsSrc = null;
9707
+ /** Copies of the last column definitions and summary options, to skip re-applying the same content. */
9708
+ this.sourceSnapshot = void 0;
9709
+ this.summarySnapshot = null;
9710
+ this.cfRows = [];
9386
9711
  this.formats = new ConditionalFormatter({
9387
9712
  rules: () => this.cfRules,
9388
- rows: () => this.rowModel.dataRows().map((n) => n.data),
9713
+ // same array while the row set is unchanged, so the formatter's stats cache holds across cells
9714
+ rows: () => {
9715
+ const src = this.rowModel.dataRows();
9716
+ if (this.cfRowsSrc !== src) {
9717
+ this.cfRowsSrc = src;
9718
+ this.cfRows = src.map((n) => n.data);
9719
+ }
9720
+ return this.cfRows;
9721
+ },
9389
9722
  value: (colId, row) => {
9390
9723
  const def = this.columns.getNode(colId)?.def;
9391
9724
  return def ? getCellValue(def, row) : void 0;
@@ -9420,8 +9753,12 @@ var Grid = class {
9420
9753
  }
9421
9754
  };
9422
9755
  this.density = "normal";
9756
+ /** Count / sum / min / max over the selected range (numbers only for the math). */
9757
+ this.aggMemo = null;
9423
9758
  /** Bumped on every selection change (group row checkboxes depend on their rows). */
9424
9759
  this.selectionVersion = 0;
9760
+ /** Rows select-all acts on: displayed data rows that are selectable. */
9761
+ this.selectableCache = null;
9425
9762
  if (!Array.isArray(options.columns)) warn("options.columns must be an array");
9426
9763
  if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
9427
9764
  this.store = new Store({
@@ -9439,6 +9776,7 @@ var Grid = class {
9439
9776
  theme: options.theme ?? "light"
9440
9777
  });
9441
9778
  this.sourceDefs = Array.isArray(options.columns) ? options.columns : [];
9779
+ this.sourceSnapshot = snapshot(this.sourceDefs);
9442
9780
  this.columns = new ColumnModel(this.withSelect(this.sourceDefs));
9443
9781
  this.bindFormulas();
9444
9782
  const pg = options.pagination;
@@ -9459,7 +9797,8 @@ var Grid = class {
9459
9797
  columnDef: (id) => this.columns.getNode(id)?.def,
9460
9798
  quickColumns: () => this.columns.displayed().filter((c) => !isSystemCol(c.id)).map((c) => c.def),
9461
9799
  format: (def, v) => getFormatter(def, this.store.state.locale.code)(v),
9462
- collator: () => this.collator()
9800
+ collator: () => this.collator(),
9801
+ textVersion: () => `${this.store.state.locale.code}|${this.editVersion}`
9463
9802
  });
9464
9803
  this.groupBy = this.validGroupBy(options.groupBy ?? []);
9465
9804
  if (Array.isArray(options.conditionalFormats)) this.cfRules = options.conditionalFormats.filter((r) => r && typeof r === "object" && Array.isArray(r.columns)).map((r) => ({ ...r, columns: [...r.columns] }));
@@ -9467,6 +9806,7 @@ var Grid = class {
9467
9806
  if (options.treeData && !options.getRowId) warn("treeData needs getRowId so parent ids can be matched");
9468
9807
  this.updateRows();
9469
9808
  this.summaryOpts = options.summary ?? null;
9809
+ this.summarySnapshot = snapshot(this.summaryOpts);
9470
9810
  this.refreshSummary();
9471
9811
  const explicit = options.fontSize != null && options.fontSize > 0 ? options.fontSize : null;
9472
9812
  if (options.fontSize != null && !(options.fontSize > 0)) warn("fontSize must be a positive number");
@@ -9522,6 +9862,10 @@ var Grid = class {
9522
9862
  this.api = this.createApi();
9523
9863
  liveGrids.set(this.renderer.root, this);
9524
9864
  }
9865
+ /** Bumped whenever cell values change (edits, transactions, undo). */
9866
+ get valuesVersion() {
9867
+ return this.editVersion;
9868
+ }
9525
9869
  /** The column definitions as given (setColumns), without the system columns. */
9526
9870
  columnDefs() {
9527
9871
  return [...this.sourceDefs];
@@ -9653,7 +9997,9 @@ var Grid = class {
9653
9997
  }
9654
9998
  const known = new Set(this.sourceLeaves().map((l) => l.id));
9655
9999
  if (!this.pivotCfg) this.savedGroup = { groupBy: this.groupBy, expanded: this.expanded, defaultExpanded: this.defaultExpanded };
9656
- this.pivotCfg = normalizePivot(cfg, (id) => known.has(id));
10000
+ const next = normalizePivot(cfg, (id) => known.has(id));
10001
+ if (this.pivotCfg && samePivot(next, this.pivotCfg)) return;
10002
+ this.pivotCfg = next;
9657
10003
  this.expanded = /* @__PURE__ */ new Map();
9658
10004
  this.defaultExpanded = -1;
9659
10005
  this.rebuildPivot(false);
@@ -9757,8 +10103,10 @@ var Grid = class {
9757
10103
  if (next === this.store.state.data && !this.viewReads(changed.fields)) {
9758
10104
  if (this.options.autoRowHeight || this.options.getRowHeight) this.renderer.invalidateHeights();
9759
10105
  this.refreshSummary();
9760
- this.finder.refresh();
9761
- if (this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan || c.def.formula || c.def.formulas) || this.formats.dependsOnOtherRows()) {
10106
+ const formulas = this.columns.displayed().some((c) => c.def.formula || c.def.formulas);
10107
+ if (formulas) this.finder.refresh();
10108
+ else this.finder.refreshRows(changed.ids);
10109
+ if (this.options.getRowHeight || formulas || this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan) || this.formats.dependsOnOtherRows()) {
9762
10110
  this.renderer.invalidate({ rows: true });
9763
10111
  } else this.renderer.invalidateRowIds(changed.ids);
9764
10112
  } else {
@@ -10228,8 +10576,8 @@ var Grid = class {
10228
10576
  const p = this.rawParent(r);
10229
10577
  if (p !== null && String(p) === id) return p;
10230
10578
  }
10231
- const numeric = this.store.state.data.some((r) => typeof this.rawParent(r) === "number");
10232
- return numeric && id.trim() !== "" && Number.isFinite(Number(id)) ? Number(id) : id;
10579
+ const numeric2 = this.store.state.data.some((r) => typeof this.rawParent(r) === "number");
10580
+ return numeric2 && id.trim() !== "" && Number.isFinite(Number(id)) ? Number(id) : id;
10233
10581
  }
10234
10582
  /** Can `row` land here? Trees refuse the row's own subtree; then the user's canDrop rule decides. */
10235
10583
  canDropRow(row, target, position, from = this) {
@@ -10369,6 +10717,11 @@ var Grid = class {
10369
10717
  warn("setSummary: summary.rows must be an array");
10370
10718
  return;
10371
10719
  }
10720
+ if (same2(opts, this.summarySnapshot)) {
10721
+ this.summaryOpts = opts;
10722
+ return;
10723
+ }
10724
+ this.summarySnapshot = snapshot(opts);
10372
10725
  this.summaryOpts = opts;
10373
10726
  this.refreshSummary();
10374
10727
  this.renderer.invalidate({ header: true, rows: true });
@@ -10658,38 +11011,44 @@ var Grid = class {
10658
11011
  this.spanCache = { rows, ver, spans };
10659
11012
  return spans;
10660
11013
  }
10661
- /** Count / sum / min / max over the selected range (numbers only for the math). */
10662
11014
  rangeAggregates() {
10663
11015
  const all2 = this.allRangeBounds();
10664
11016
  if (!all2.length) return null;
10665
11017
  const displayed = this.columns.displayed();
11018
+ const key = [JSON.stringify(all2), this.rowModel.rows(), this.editVersion, ...displayed.map((c) => c.def)];
11019
+ const memo = this.aggMemo;
11020
+ if (memo && memo.key.length === key.length && memo.key.every((k, i) => k === key[i])) return memo.value && { ...memo.value };
10666
11021
  const out = { cells: 0, count: 0, numbers: 0, sum: 0, min: Infinity, max: -Infinity };
10667
11022
  const seen2 = all2.length > 1 ? /* @__PURE__ */ new Set() : null;
10668
- for (const b of all2) for (let r = b.r1; r <= b.r2; r++) {
10669
- const node = this.rowModel.getRow(r);
10670
- if (!node || node.kind) continue;
10671
- const row = node.data;
10672
- for (const c of displayed.slice(b.c1, b.c2 + 1)) {
10673
- if (isSystemCol(c.id)) continue;
10674
- if (seen2) {
10675
- const k = `${r},${c.index}`;
10676
- if (seen2.has(k)) continue;
10677
- seen2.add(k);
10678
- }
10679
- out.cells++;
10680
- const v = getCellValue(c.def, row);
10681
- if (isBlank(v)) continue;
10682
- out.count++;
10683
- if (typeof v === "number" || c.def.type === "number" && !Number.isNaN(Number(v))) {
10684
- const n = Number(v);
10685
- out.numbers++;
10686
- out.sum += n;
10687
- if (n < out.min) out.min = n;
10688
- if (n > out.max) out.max = n;
11023
+ const width = displayed.length;
11024
+ for (const b of all2) {
11025
+ const cols = displayed.slice(b.c1, b.c2 + 1).filter((c) => !isSystemCol(c.id));
11026
+ for (let r = b.r1; r <= b.r2; r++) {
11027
+ const node = this.rowModel.getRow(r);
11028
+ if (!node || node.kind) continue;
11029
+ const row = node.data;
11030
+ for (const c of cols) {
11031
+ if (seen2) {
11032
+ const k = r * width + c.index;
11033
+ if (seen2.has(k)) continue;
11034
+ seen2.add(k);
11035
+ }
11036
+ out.cells++;
11037
+ const v = getCellValue(c.def, row);
11038
+ if (isBlank(v)) continue;
11039
+ out.count++;
11040
+ if (typeof v === "number" || c.def.type === "number" && !Number.isNaN(Number(v))) {
11041
+ const n = Number(v);
11042
+ out.numbers++;
11043
+ out.sum += n;
11044
+ if (n < out.min) out.min = n;
11045
+ if (n > out.max) out.max = n;
11046
+ }
10689
11047
  }
10690
11048
  }
10691
11049
  }
10692
- return out;
11050
+ this.aggMemo = { key, value: out };
11051
+ return { ...out };
10693
11052
  }
10694
11053
  /** Fill from the current selection into `target`, Excel-style (one undo step). */
10695
11054
  fillRange(target) {
@@ -11116,7 +11475,15 @@ var Grid = class {
11116
11475
  const i = this.rowModel.indexOfId(id);
11117
11476
  return i < 0 ? null : i;
11118
11477
  }
11119
- /** Rows select-all acts on: displayed data rows that are selectable. */
11478
+ /** Ids of selectableRows(), kept while the rows are the same. */
11479
+ selectableIds() {
11480
+ const rows = this.rowModel.dataRows(), fn = this.options.isRowSelectable;
11481
+ const c = this.selectableCache;
11482
+ if (c && c.rows === rows && c.fn === fn && c.ver === this.editVersion) return c.ids;
11483
+ const ids = new Set(this.selectableRows().map((n) => n.id));
11484
+ this.selectableCache = { rows, fn, ver: this.editVersion, ids };
11485
+ return ids;
11486
+ }
11120
11487
  selectableRows() {
11121
11488
  const rows = this.rowModel.dataRows();
11122
11489
  return this.options.isRowSelectable ? rows.filter((n) => this.rowSelectable(n.data)) : rows;
@@ -11179,13 +11546,18 @@ var Grid = class {
11179
11546
  warn("setColumns: columns must be an array");
11180
11547
  return;
11181
11548
  }
11549
+ if (same2(defs, this.sourceSnapshot)) {
11550
+ this.sourceDefs = defs;
11551
+ return;
11552
+ }
11182
11553
  this.menu.close();
11183
11554
  this.sourceDefs = defs;
11555
+ this.sourceSnapshot = snapshot(defs);
11184
11556
  if (this.pivotCfg) {
11185
11557
  this.rebuildPivot();
11186
11558
  return;
11187
11559
  }
11188
- this.columns.setDefs(this.withSelect(defs));
11560
+ this.columns.updateDefs(this.withSelect(defs));
11189
11561
  this.bindFormulas();
11190
11562
  const known = (id) => !!this.columns.getNode(id);
11191
11563
  const groupBy = this.groupBy.filter(known);
@@ -11197,7 +11569,7 @@ var Grid = class {
11197
11569
  this.store.set({
11198
11570
  sort: this.store.state.sort.filter((s) => known(s.colId)),
11199
11571
  filters: Object.fromEntries(Object.entries(this.store.state.filters).filter(([k]) => known(k))),
11200
- focus: null
11572
+ focus: this.store.state.focus && known(this.store.state.focus.colId) ? this.store.state.focus : null
11201
11573
  });
11202
11574
  this.columnsChanged();
11203
11575
  }
@@ -11222,8 +11594,10 @@ var Grid = class {
11222
11594
  if (has("locale")) this.groupPanel?.refresh();
11223
11595
  if (has("filters")) this.sideBar?.refresh();
11224
11596
  if (has("theme")) this.renderer.applyTheme();
11597
+ const header2 = has("sort", "filters", "quickFilter", "advancedFilter", "locale", "data");
11598
+ if (!header2 && has("selected")) this.renderer.selectionChanged();
11225
11599
  this.renderer.invalidate({
11226
- header: has("sort", "filters", "quickFilter", "advancedFilter", "locale", "selected", "data"),
11600
+ header: header2,
11227
11601
  rows: has("data", "sort", "filters", "quickFilter", "advancedFilter", "locale")
11228
11602
  });
11229
11603
  }
@@ -11456,9 +11830,12 @@ function saveChanges(grid, url, options = {}) {
11456
11830
  saving.set(grid, run2);
11457
11831
  return run2;
11458
11832
  }
11833
+ var loads = /* @__PURE__ */ new WeakMap();
11459
11834
  async function loadData(grid, url, options = {}) {
11835
+ const seq = (loads.get(grid) ?? 0) + 1;
11836
+ loads.set(grid, seq);
11460
11837
  const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });
11461
- grid.setData(rows);
11838
+ if (loads.get(grid) === seq) grid.setData(rows);
11462
11839
  return rows;
11463
11840
  }
11464
11841
  function csrfHeaders(doc = document) {
@@ -11634,6 +12011,7 @@ function connectLive(api, options) {
11634
12011
  }
11635
12012
  export {
11636
12013
  buildWorkbook,
12014
+ changeTracker,
11637
12015
  connectLive,
11638
12016
  createGrid,
11639
12017
  csrfHeaders,