@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/cdn/freegrid.css +6 -0
- package/dist/cdn/freegrid.esm.js +22 -22
- package/dist/cdn/freegrid.min.js +22 -22
- package/dist/index.d.ts +13 -4
- package/dist/index.js +834 -456
- package/dist/styles.css +6 -0
- package/package.json +1 -1
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
|
|
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
|
|
406
|
-
if (
|
|
407
|
-
if (a instanceof Date || b instanceof Date) return num(
|
|
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
|
|
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
|
|
722
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
427
723
|
function ymd(d) {
|
|
428
|
-
return `${d.getFullYear()}-${
|
|
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(
|
|
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(
|
|
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 =
|
|
572
|
-
return f.replace(/yyyy/gi, String(d.getFullYear())).replace(/mm/g,
|
|
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) =>
|
|
592
|
-
MONTH: (a) =>
|
|
593
|
-
DAY: (a) =>
|
|
594
|
-
WEEKDAY: (a) =>
|
|
595
|
-
DAYS: (a) => dayNumber(
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
2117
|
-
const
|
|
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.
|
|
2121
|
-
|
|
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" ? `${
|
|
2141
|
-
const
|
|
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.
|
|
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)
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
2641
|
-
panel.style.width = `${this.
|
|
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 =
|
|
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 =
|
|
2897
|
-
let max =
|
|
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
|
|
4426
|
-
const
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
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) ?
|
|
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 :
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 :
|
|
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" ?
|
|
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
|
|
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,
|
|
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
|
|
6521
|
+
var PLAIN_NUMBER2 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
6303
6522
|
var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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) =>
|
|
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 =
|
|
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) :
|
|
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" && !
|
|
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 =
|
|
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() :
|
|
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
|
|
7094
|
-
const
|
|
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
|
|
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.
|
|
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 (
|
|
7896
|
-
|
|
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) =>
|
|
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
|
|
8831
|
-
const v1 = el("input", "fg-input fg-cf-value", { type:
|
|
8832
|
-
const v2 = el("input", "fg-input fg-cf-value-to", { type:
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
9761
|
-
if (
|
|
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
|
|
10232
|
-
return
|
|
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
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
if (
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
if (seen2
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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:
|
|
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,
|