@freegridjs/core 1.0.1 → 1.6.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 +51 -2
- package/dist/cdn/freegrid.esm.js +49 -15
- package/dist/cdn/freegrid.min.js +49 -15
- package/dist/index.d.ts +71 -4
- package/dist/index.js +2590 -273
- package/dist/locales/de.d.ts +1 -1
- package/dist/locales/de.js +17 -0
- package/dist/locales/en.d.ts +1 -1
- package/dist/locales/en.js +17 -0
- package/dist/locales/es.d.ts +1 -1
- package/dist/locales/es.js +17 -0
- package/dist/locales/fr.d.ts +1 -1
- package/dist/locales/fr.js +17 -0
- package/dist/locales/id.d.ts +1 -1
- package/dist/locales/id.js +17 -0
- package/dist/locales/it.d.ts +1 -1
- package/dist/locales/it.js +17 -0
- package/dist/locales/ja.d.ts +1 -1
- package/dist/locales/ja.js +17 -0
- package/dist/locales/ko.d.ts +1 -1
- package/dist/locales/ko.js +17 -0
- package/dist/locales/pt-BR.d.ts +1 -1
- package/dist/locales/pt-BR.js +17 -0
- package/dist/locales/ru.d.ts +1 -1
- package/dist/locales/ru.js +17 -0
- package/dist/locales/th.d.ts +1 -1
- package/dist/locales/th.js +17 -0
- package/dist/locales/vi.d.ts +1 -1
- package/dist/locales/vi.js +17 -0
- package/dist/locales/zh-CN.d.ts +1 -1
- package/dist/locales/zh-CN.js +17 -0
- package/dist/locales/zh-TW.d.ts +1 -1
- package/dist/locales/zh-TW.js +17 -0
- package/dist/styles.css +51 -2
- package/dist/{types-bFz-Q6ac.d.ts → types-DPOWDM8R.d.ts} +286 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -331,6 +331,8 @@ var FormulaError = class extends Error {
|
|
|
331
331
|
this.code = code;
|
|
332
332
|
}
|
|
333
333
|
};
|
|
334
|
+
var ERROR_RE = /^#[A-Z/0]+[!?]$/;
|
|
335
|
+
var isFormulaError = (v) => typeof v === "string" && ERROR_RE.test(v);
|
|
334
336
|
function tokenize(src) {
|
|
335
337
|
const out = [];
|
|
336
338
|
let i = 0;
|
|
@@ -366,7 +368,7 @@ function tokenize(src) {
|
|
|
366
368
|
out.push({ t: "id", v: src.slice(i + 1, end) });
|
|
367
369
|
i = end + 1;
|
|
368
370
|
} else if (/[A-Za-z_À-]/.test(c)) {
|
|
369
|
-
const m = /^[A-Za-z_À-][\w
|
|
371
|
+
const m = /^[A-Za-z_À-][\wÀ-.]*/.exec(src.slice(i));
|
|
370
372
|
out.push({ t: "id", v: m[0] });
|
|
371
373
|
i += m[0].length;
|
|
372
374
|
} else {
|
|
@@ -374,8 +376,8 @@ function tokenize(src) {
|
|
|
374
376
|
if (two === "<>" || two === "<=" || two === ">=") {
|
|
375
377
|
out.push({ t: "op", v: two });
|
|
376
378
|
i += 2;
|
|
377
|
-
} else if ("+-*/%^&=<>()
|
|
378
|
-
out.push({ t: "op", v: c });
|
|
379
|
+
} else if ("+-*/%^&=<>(),;".includes(c)) {
|
|
380
|
+
out.push({ t: "op", v: c === ";" ? "," : c });
|
|
379
381
|
i++;
|
|
380
382
|
} else throw new FormulaError("#ERROR!");
|
|
381
383
|
}
|
|
@@ -387,35 +389,271 @@ function num(v) {
|
|
|
387
389
|
if (typeof v === "number") return v;
|
|
388
390
|
if (typeof v === "boolean") return v ? 1 : 0;
|
|
389
391
|
if (isBlankValue(v)) return 0;
|
|
392
|
+
if (v instanceof Date) return v.getTime();
|
|
390
393
|
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
391
394
|
throw new FormulaError("#VALUE!");
|
|
392
395
|
}
|
|
393
|
-
var text = (v) => isBlankValue(v) ? "" : typeof v === "boolean" ? v ? "TRUE" : "FALSE" : String(v);
|
|
394
|
-
|
|
396
|
+
var text = (v) => isBlankValue(v) ? "" : typeof v === "boolean" ? v ? "TRUE" : "FALSE" : v instanceof Date ? ymd(v) : String(v);
|
|
397
|
+
var bool = (v) => typeof v === "boolean" ? v : typeof v === "string" && /^(true|false)$/i.test(v) ? v.toLowerCase() === "true" : num(v) !== 0;
|
|
398
|
+
function round(x, digits, mode = "near") {
|
|
395
399
|
const d = Math.trunc(digits);
|
|
396
|
-
const
|
|
400
|
+
const f = mode === "near" ? Math.round : mode === "up" ? Math.ceil : Math.floor;
|
|
401
|
+
const r = Number(`${f(Number(`${Math.abs(x)}e${d}`))}e${-d}`);
|
|
397
402
|
return x < 0 ? -r : r;
|
|
398
403
|
}
|
|
404
|
+
function compare(a, b) {
|
|
405
|
+
const numeric = (v) => typeof v === "number" || typeof v === "boolean" || isBlankValue(v);
|
|
406
|
+
if (numeric(a) && numeric(b)) return num(a) - num(b);
|
|
407
|
+
if (a instanceof Date || b instanceof Date) return num(toDate(a)) - num(toDate(b));
|
|
408
|
+
return text(a).localeCompare(text(b), void 0, { sensitivity: "accent" });
|
|
409
|
+
}
|
|
410
|
+
var flat = (a) => a.flatMap((v) => Array.isArray(v) ? v : [v]);
|
|
411
|
+
var numsLoose = (a) => a.flatMap((v) => Array.isArray(v) ? noErrors(v).filter((x) => typeof x === "number" && Number.isFinite(x)) : isBlankValue(v) ? [] : [num(v)]);
|
|
412
|
+
function noErrors(list) {
|
|
413
|
+
const bad = list.find(isFormulaError);
|
|
414
|
+
if (bad !== void 0) throw new FormulaError(bad);
|
|
415
|
+
return list;
|
|
416
|
+
}
|
|
417
|
+
function toDate(v) {
|
|
418
|
+
if (v instanceof Date) return v;
|
|
419
|
+
if (typeof v === "number") return new Date(v);
|
|
420
|
+
if (typeof v === "string") {
|
|
421
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/.exec(v.trim());
|
|
422
|
+
if (m) return new Date(+m[1], +m[2] - 1, +m[3], +(m[4] ?? 0), +(m[5] ?? 0), +(m[6] ?? 0));
|
|
423
|
+
}
|
|
424
|
+
throw new FormulaError("#VALUE!");
|
|
425
|
+
}
|
|
426
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
427
|
+
function ymd(d) {
|
|
428
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
429
|
+
}
|
|
430
|
+
var DAY = 864e5;
|
|
431
|
+
var dayNumber = (d) => Math.round(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY);
|
|
432
|
+
function criteria(c) {
|
|
433
|
+
if (typeof c !== "string") return (v) => compare(v, c) === 0 && !isBlankValue(v) === !isBlankValue(c);
|
|
434
|
+
const m = /^(<=|>=|<>|<|>|=)?(.*)$/s.exec(c);
|
|
435
|
+
const op = m[1] ?? "=";
|
|
436
|
+
const raw = m[2];
|
|
437
|
+
const target = raw.trim() !== "" && Number.isFinite(Number(raw)) ? Number(raw) : raw;
|
|
438
|
+
if ((op === "=" || op === "<>") && typeof target === "string" && /[*?]/.test(target)) {
|
|
439
|
+
const re = new RegExp(`^${target.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
|
|
440
|
+
return op === "=" ? (v) => re.test(text(v)) : (v) => !re.test(text(v));
|
|
441
|
+
}
|
|
442
|
+
return (v) => {
|
|
443
|
+
if (op === "=" && target === "") return isBlankValue(v);
|
|
444
|
+
if (op === "<>" && target === "") return !isBlankValue(v);
|
|
445
|
+
if (typeof target === "number" && (typeof v !== "number" && !(typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))))) return op === "<>";
|
|
446
|
+
let r;
|
|
447
|
+
try {
|
|
448
|
+
r = compare(v, target);
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
return op === "=" ? r === 0 : op === "<>" ? r !== 0 : op === "<" ? r < 0 : op === ">" ? r > 0 : op === "<=" ? r <= 0 : r >= 0;
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function arr(v) {
|
|
456
|
+
if (Array.isArray(v)) return v;
|
|
457
|
+
throw new FormulaError("#VALUE!");
|
|
458
|
+
}
|
|
459
|
+
function ifs(a, test, crit) {
|
|
460
|
+
const t = arr(test), keep = criteria(crit);
|
|
461
|
+
const out = [];
|
|
462
|
+
const vals = noErrors(a.length ? arr(a[0]) : t);
|
|
463
|
+
t.forEach((v, i) => {
|
|
464
|
+
const x = vals[i];
|
|
465
|
+
if (keep(v) && typeof x === "number" && Number.isFinite(x)) out.push(x);
|
|
466
|
+
});
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
399
469
|
var FUNCS = {
|
|
470
|
+
// math
|
|
471
|
+
SUM: (a) => numsLoose(a).reduce((s, v) => s + v, 0),
|
|
472
|
+
AVG: (a) => {
|
|
473
|
+
const n = numsLoose(a);
|
|
474
|
+
if (!n.length) throw new FormulaError("#DIV/0!");
|
|
475
|
+
return n.reduce((s, v) => s + v, 0) / n.length;
|
|
476
|
+
},
|
|
477
|
+
MIN: (a) => {
|
|
478
|
+
const n = numsLoose(a);
|
|
479
|
+
return n.length ? Math.min(...n) : 0;
|
|
480
|
+
},
|
|
481
|
+
MAX: (a) => {
|
|
482
|
+
const n = numsLoose(a);
|
|
483
|
+
return n.length ? Math.max(...n) : 0;
|
|
484
|
+
},
|
|
485
|
+
MEDIAN: (a) => {
|
|
486
|
+
const n = numsLoose(a).sort((x, y) => x - y);
|
|
487
|
+
if (!n.length) throw new FormulaError("#NUM!");
|
|
488
|
+
const m = n.length >> 1;
|
|
489
|
+
return n.length % 2 ? n[m] : (n[m - 1] + n[m]) / 2;
|
|
490
|
+
},
|
|
491
|
+
COUNT: (a) => flat(a).filter((v) => typeof v === "number" && Number.isFinite(v)).length,
|
|
492
|
+
COUNTA: (a) => flat(a).filter((v) => !isBlankValue(v)).length,
|
|
493
|
+
COUNTBLANK: (a) => flat(a).filter(isBlankValue).length,
|
|
494
|
+
PRODUCT: (a) => numsLoose(a).reduce((s, v) => s * v, 1),
|
|
400
495
|
ROUND: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0),
|
|
496
|
+
ROUNDUP: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0, "up"),
|
|
497
|
+
ROUNDDOWN: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0, "down"),
|
|
498
|
+
INT: (a) => Math.floor(num(a[0])),
|
|
499
|
+
TRUNC: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0, num(a[0]) < 0 ? "up" : "down"),
|
|
401
500
|
ABS: (a) => Math.abs(num(a[0])),
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
501
|
+
SIGN: (a) => Math.sign(num(a[0])),
|
|
502
|
+
SQRT: (a) => {
|
|
503
|
+
const x = num(a[0]);
|
|
504
|
+
if (x < 0) throw new FormulaError("#NUM!");
|
|
505
|
+
return Math.sqrt(x);
|
|
506
|
+
},
|
|
507
|
+
POWER: (a) => num(a[0]) ** num(a[1]),
|
|
508
|
+
EXP: (a) => Math.exp(num(a[0])),
|
|
509
|
+
LN: (a) => {
|
|
510
|
+
const x = num(a[0]);
|
|
511
|
+
if (x <= 0) throw new FormulaError("#NUM!");
|
|
512
|
+
return Math.log(x);
|
|
513
|
+
},
|
|
514
|
+
LOG10: (a) => {
|
|
515
|
+
const x = num(a[0]);
|
|
516
|
+
if (x <= 0) throw new FormulaError("#NUM!");
|
|
517
|
+
return Math.log10(x);
|
|
518
|
+
},
|
|
519
|
+
MOD: (a) => {
|
|
520
|
+
const d = num(a[1]);
|
|
521
|
+
if (d === 0) throw new FormulaError("#DIV/0!");
|
|
522
|
+
const x = num(a[0]);
|
|
523
|
+
return x - d * Math.floor(x / d);
|
|
524
|
+
},
|
|
525
|
+
CEILING: (a) => {
|
|
526
|
+
const s = a.length > 1 ? num(a[1]) : 1;
|
|
527
|
+
return s === 0 ? 0 : Math.ceil(num(a[0]) / s) * s;
|
|
528
|
+
},
|
|
529
|
+
FLOOR: (a) => {
|
|
530
|
+
const s = a.length > 1 ? num(a[1]) : 1;
|
|
531
|
+
return s === 0 ? 0 : Math.floor(num(a[0]) / s) * s;
|
|
532
|
+
},
|
|
533
|
+
PI: () => Math.PI,
|
|
534
|
+
// text
|
|
535
|
+
CONCAT: (a) => flat(a).map(text).join(""),
|
|
536
|
+
TEXTJOIN: (a) => flat(a.slice(2)).filter((v) => !bool(a[1]) || !isBlankValue(v)).map(text).join(text(a[0])),
|
|
537
|
+
LEN: (a) => text(a[0]).length,
|
|
538
|
+
LEFT: (a) => text(a[0]).slice(0, a.length > 1 ? Math.max(0, num(a[1])) : 1),
|
|
539
|
+
RIGHT: (a) => {
|
|
540
|
+
const s = text(a[0]);
|
|
541
|
+
const n = a.length > 1 ? Math.max(0, num(a[1])) : 1;
|
|
542
|
+
return n ? s.slice(-n) : "";
|
|
543
|
+
},
|
|
544
|
+
MID: (a) => text(a[0]).substr(Math.max(0, num(a[1]) - 1), Math.max(0, num(a[2]))),
|
|
545
|
+
UPPER: (a) => text(a[0]).toUpperCase(),
|
|
546
|
+
LOWER: (a) => text(a[0]).toLowerCase(),
|
|
547
|
+
PROPER: (a) => text(a[0]).toLowerCase().replace(/(^|[^\p{L}])(\p{L})/gu, (_, p, c) => p + c.toUpperCase()),
|
|
548
|
+
TRIM: (a) => text(a[0]).trim().replace(/\s+/g, " "),
|
|
549
|
+
SUBSTITUTE: (a) => text(a[0]).split(text(a[1])).join(text(a[2])),
|
|
550
|
+
REPLACE: (a) => {
|
|
551
|
+
const s = text(a[0]);
|
|
552
|
+
const at = Math.max(0, num(a[1]) - 1);
|
|
553
|
+
return s.slice(0, at) + text(a[3]) + s.slice(at + Math.max(0, num(a[2])));
|
|
554
|
+
},
|
|
555
|
+
REPT: (a) => text(a[0]).repeat(Math.max(0, Math.min(1e4, num(a[1])))),
|
|
556
|
+
FIND: (a) => {
|
|
557
|
+
const i = text(a[1]).indexOf(text(a[0]), a.length > 2 ? num(a[2]) - 1 : 0);
|
|
558
|
+
if (i < 0) throw new FormulaError("#VALUE!");
|
|
559
|
+
return i + 1;
|
|
560
|
+
},
|
|
561
|
+
SEARCH: (a) => {
|
|
562
|
+
const i = text(a[1]).toLowerCase().indexOf(text(a[0]).toLowerCase(), a.length > 2 ? num(a[2]) - 1 : 0);
|
|
563
|
+
if (i < 0) throw new FormulaError("#VALUE!");
|
|
564
|
+
return i + 1;
|
|
565
|
+
},
|
|
566
|
+
VALUE: (a) => num(text(a[0]).replace(/,/g, "")),
|
|
567
|
+
TEXT: (a) => {
|
|
568
|
+
const v = a[0], f = text(a[1]);
|
|
569
|
+
const dec = /0\.(0+)/.exec(f)?.[1].length ?? 0;
|
|
570
|
+
if (/[yd]/i.test(f)) {
|
|
571
|
+
const d = toDate(v);
|
|
572
|
+
return f.replace(/yyyy/gi, String(d.getFullYear())).replace(/mm/g, pad(d.getMonth() + 1)).replace(/dd/gi, pad(d.getDate()));
|
|
573
|
+
}
|
|
574
|
+
const n = num(v);
|
|
575
|
+
const s = f.includes("%") ? (n * 100).toFixed(dec) + "%" : f.includes(",") ? n.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec }) : n.toFixed(dec);
|
|
576
|
+
return s;
|
|
577
|
+
},
|
|
578
|
+
// logic and types
|
|
579
|
+
AND: (a) => flat(a).every(bool),
|
|
580
|
+
OR: (a) => flat(a).some(bool),
|
|
581
|
+
XOR: (a) => flat(a).filter(bool).length % 2 === 1,
|
|
582
|
+
NOT: (a) => !bool(a[0]),
|
|
583
|
+
ISBLANK: (a) => isBlankValue(a[0]),
|
|
584
|
+
ISNUMBER: (a) => typeof a[0] === "number" && Number.isFinite(a[0]),
|
|
585
|
+
ISTEXT: (a) => typeof a[0] === "string" && !isFormulaError(a[0]),
|
|
586
|
+
ISERROR: (a) => isFormulaError(a[0]),
|
|
587
|
+
// dates (as "YYYY-MM-DD" text, like date columns)
|
|
588
|
+
TODAY: () => ymd(/* @__PURE__ */ new Date()),
|
|
589
|
+
NOW: () => /* @__PURE__ */ new Date(),
|
|
590
|
+
DATE: (a) => ymd(new Date(num(a[0]), num(a[1]) - 1, num(a[2]))),
|
|
591
|
+
YEAR: (a) => toDate(a[0]).getFullYear(),
|
|
592
|
+
MONTH: (a) => toDate(a[0]).getMonth() + 1,
|
|
593
|
+
DAY: (a) => toDate(a[0]).getDate(),
|
|
594
|
+
WEEKDAY: (a) => toDate(a[0]).getDay() + 1,
|
|
595
|
+
DAYS: (a) => dayNumber(toDate(a[0])) - dayNumber(toDate(a[1])),
|
|
596
|
+
EDATE: (a) => {
|
|
597
|
+
const d = toDate(a[0]);
|
|
598
|
+
const day = d.getDate();
|
|
599
|
+
const r = new Date(d.getFullYear(), d.getMonth() + num(a[1]), 1);
|
|
600
|
+
r.setDate(Math.min(day, new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate()));
|
|
601
|
+
return ymd(r);
|
|
602
|
+
},
|
|
603
|
+
EOMONTH: (a) => {
|
|
604
|
+
const d = toDate(a[0]);
|
|
605
|
+
return ymd(new Date(d.getFullYear(), d.getMonth() + num(a[1]) + 1, 0));
|
|
606
|
+
},
|
|
607
|
+
// over columns
|
|
608
|
+
SUMIF: (a) => ifs(a.slice(2), arr(a[0]), a[1]).reduce((s, v) => s + v, 0),
|
|
609
|
+
COUNTIF: (a) => {
|
|
610
|
+
const keep = criteria(a[1]);
|
|
611
|
+
return arr(a[0]).filter(keep).length;
|
|
612
|
+
},
|
|
613
|
+
AVERAGEIF: (a) => {
|
|
614
|
+
const n = ifs(a.slice(2), arr(a[0]), a[1]);
|
|
615
|
+
if (!n.length) throw new FormulaError("#DIV/0!");
|
|
616
|
+
return n.reduce((s, v) => s + v, 0) / n.length;
|
|
617
|
+
},
|
|
618
|
+
LOOKUP: (a) => {
|
|
619
|
+
const keys = arr(a[1]), vals = a.length > 2 ? arr(a[2]) : keys;
|
|
620
|
+
const i = keys.findIndex((k) => {
|
|
621
|
+
try {
|
|
622
|
+
return compare(k, a[0]) === 0;
|
|
623
|
+
} catch {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
if (i < 0) {
|
|
628
|
+
if (a.length > 3) return a[3];
|
|
629
|
+
throw new FormulaError("#N/A");
|
|
630
|
+
}
|
|
631
|
+
return vals[i];
|
|
632
|
+
},
|
|
633
|
+
RANK: (a) => {
|
|
634
|
+
const v = num(a[0]);
|
|
635
|
+
const list = numsLoose([arr(a[1])]);
|
|
636
|
+
const asc = a.length > 2 && bool(a[2]);
|
|
637
|
+
return 1 + list.filter((x) => asc ? x < v : x > v).length;
|
|
408
638
|
}
|
|
409
639
|
};
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
640
|
+
FUNCS.AVERAGE = FUNCS.AVG;
|
|
641
|
+
FUNCS.XLOOKUP = FUNCS.LOOKUP;
|
|
642
|
+
FUNCS.CONCATENATE = FUNCS.CONCAT;
|
|
643
|
+
var custom = /* @__PURE__ */ new Map();
|
|
644
|
+
function registerFormulaFunction(name, fn) {
|
|
645
|
+
if (typeof name !== "string" || !/^[A-Za-z_][\w.]*$/.test(name) || typeof fn !== "function") return;
|
|
646
|
+
custom.set(name.toUpperCase(), fn);
|
|
647
|
+
compiled.clear();
|
|
648
|
+
}
|
|
649
|
+
function formulaFunctions() {
|
|
650
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(FUNCS), ...LAZY, ...SPECIAL, ...custom.keys()])].sort();
|
|
651
|
+
}
|
|
652
|
+
var LAZY = ["IF", "IFS", "IFERROR", "IFNA", "SWITCH", "CHOOSE"];
|
|
653
|
+
var SPECIAL = ["COL", "PREV", "NEXT", "ROW"];
|
|
415
654
|
var Parser = class {
|
|
416
|
-
constructor(tokens
|
|
655
|
+
constructor(tokens) {
|
|
417
656
|
this.tokens = tokens;
|
|
418
|
-
this.resolve = resolve;
|
|
419
657
|
this.i = 0;
|
|
420
658
|
}
|
|
421
659
|
parse() {
|
|
@@ -435,52 +673,46 @@ var Parser = class {
|
|
|
435
673
|
comparison() {
|
|
436
674
|
const a = this.concat();
|
|
437
675
|
for (const op of ["=", "<>", "<=", ">=", "<", ">"]) {
|
|
438
|
-
if (this.take(op)) {
|
|
439
|
-
const c = compare(a, this.concat());
|
|
440
|
-
return op === "=" ? c === 0 : op === "<>" ? c !== 0 : op === "<" ? c < 0 : op === ">" ? c > 0 : op === "<=" ? c <= 0 : c >= 0;
|
|
441
|
-
}
|
|
676
|
+
if (this.take(op)) return { k: "bin", op, a, b: this.concat() };
|
|
442
677
|
}
|
|
443
678
|
return a;
|
|
444
679
|
}
|
|
445
680
|
concat() {
|
|
446
681
|
let v = this.additive();
|
|
447
|
-
while (this.take("&")) v =
|
|
682
|
+
while (this.take("&")) v = { k: "bin", op: "&", a: v, b: this.additive() };
|
|
448
683
|
return v;
|
|
449
684
|
}
|
|
450
685
|
additive() {
|
|
451
686
|
let v = this.multiplicative();
|
|
452
687
|
for (; ; ) {
|
|
453
|
-
if (this.take("+")) v =
|
|
454
|
-
else if (this.take("-")) v =
|
|
688
|
+
if (this.take("+")) v = { k: "bin", op: "+", a: v, b: this.multiplicative() };
|
|
689
|
+
else if (this.take("-")) v = { k: "bin", op: "-", a: v, b: this.multiplicative() };
|
|
455
690
|
else return v;
|
|
456
691
|
}
|
|
457
692
|
}
|
|
458
693
|
multiplicative() {
|
|
459
694
|
let v = this.unary();
|
|
460
695
|
for (; ; ) {
|
|
461
|
-
if (this.take("*")) v =
|
|
462
|
-
else if (this.take("/")
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (d === 0) throw new FormulaError("#DIV/0!");
|
|
466
|
-
v = op === "/" ? num(v) / d : num(v) % d;
|
|
467
|
-
} else return v;
|
|
696
|
+
if (this.take("*")) v = { k: "bin", op: "*", a: v, b: this.unary() };
|
|
697
|
+
else if (this.take("/")) v = { k: "bin", op: "/", a: v, b: this.unary() };
|
|
698
|
+
else if (this.take("%")) v = { k: "bin", op: "%", a: v, b: this.unary() };
|
|
699
|
+
else return v;
|
|
468
700
|
}
|
|
469
701
|
}
|
|
470
702
|
unary() {
|
|
471
|
-
if (this.take("-")) return -
|
|
472
|
-
if (this.take("+")) return
|
|
703
|
+
if (this.take("-")) return { k: "un", op: "-", a: this.unary() };
|
|
704
|
+
if (this.take("+")) return { k: "un", op: "+", a: this.unary() };
|
|
473
705
|
return this.power();
|
|
474
706
|
}
|
|
475
707
|
power() {
|
|
476
708
|
const base = this.primary();
|
|
477
|
-
if (this.take("^")) return
|
|
709
|
+
if (this.take("^")) return { k: "bin", op: "^", a: base, b: this.unary() };
|
|
478
710
|
return base;
|
|
479
711
|
}
|
|
480
712
|
primary() {
|
|
481
713
|
const t = this.tokens[this.i++];
|
|
482
714
|
if (!t) throw new FormulaError("#ERROR!");
|
|
483
|
-
if (t.t === "num" || t.t === "str") return t.v;
|
|
715
|
+
if (t.t === "num" || t.t === "str") return { k: "lit", v: t.v };
|
|
484
716
|
if (t.t === "op") {
|
|
485
717
|
if (t.v !== "(") throw new FormulaError("#ERROR!");
|
|
486
718
|
const v = this.comparison();
|
|
@@ -498,46 +730,296 @@ var Parser = class {
|
|
|
498
730
|
if (!this.take(")")) throw new FormulaError("#ERROR!");
|
|
499
731
|
}
|
|
500
732
|
const upper = name.toUpperCase();
|
|
501
|
-
if (upper === "
|
|
502
|
-
if (args.length
|
|
503
|
-
const cond = typeof args[0] === "boolean" ? args[0] : num(args[0]) !== 0;
|
|
504
|
-
return cond ? args[1] : args.length > 2 ? args[2] : false;
|
|
733
|
+
if (upper === "COL" || upper === "PREV" || upper === "NEXT") {
|
|
734
|
+
if (!args.length || args[0].k !== "ref") throw new FormulaError("#ERROR!");
|
|
505
735
|
}
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
return fn(args);
|
|
736
|
+
const rowFree = upper !== "PREV" && upper !== "NEXT" && upper !== "ROW" && args.every((a) => isRowFree(a, upper === "COL"));
|
|
737
|
+
return { k: "call", name: upper, args, rowFree };
|
|
509
738
|
}
|
|
510
|
-
if (/^(true|false)$/i.test(name)) return name.toLowerCase() === "true";
|
|
511
|
-
return
|
|
739
|
+
if (/^(true|false)$/i.test(name)) return { k: "lit", v: name.toLowerCase() === "true" };
|
|
740
|
+
return { k: "ref", name };
|
|
512
741
|
}
|
|
513
742
|
};
|
|
743
|
+
function isRowFree(n, inCol = false) {
|
|
744
|
+
if (n.k === "lit") return true;
|
|
745
|
+
if (n.k === "ref") return inCol;
|
|
746
|
+
if (n.k === "un") return isRowFree(n.a);
|
|
747
|
+
if (n.k === "bin") return isRowFree(n.a) && isRowFree(n.b);
|
|
748
|
+
return n.rowFree;
|
|
749
|
+
}
|
|
750
|
+
var compiled = /* @__PURE__ */ new Map();
|
|
751
|
+
var MAX_COMPILED = 2e3;
|
|
752
|
+
function compile(formula) {
|
|
753
|
+
let n = compiled.get(formula);
|
|
754
|
+
if (!n) {
|
|
755
|
+
try {
|
|
756
|
+
n = new Parser(tokenize(formula.slice(1))).parse();
|
|
757
|
+
} catch (e) {
|
|
758
|
+
n = e instanceof FormulaError ? e : new FormulaError("#ERROR!");
|
|
759
|
+
}
|
|
760
|
+
if (compiled.size >= MAX_COMPILED) compiled.delete(compiled.keys().next().value);
|
|
761
|
+
compiled.set(formula, n);
|
|
762
|
+
}
|
|
763
|
+
if (n instanceof FormulaError) throw n;
|
|
764
|
+
return n;
|
|
765
|
+
}
|
|
766
|
+
var caches = /* @__PURE__ */ new WeakMap();
|
|
767
|
+
function cacheOf(scope) {
|
|
768
|
+
const data = scope.rows();
|
|
769
|
+
const version2 = scope.version();
|
|
770
|
+
let c = caches.get(scope);
|
|
771
|
+
if (!c || c.data !== data || c.version !== version2) {
|
|
772
|
+
c = { data, version: version2, index: null, cols: /* @__PURE__ */ new Map(), values: /* @__PURE__ */ new WeakMap(), rowFree: /* @__PURE__ */ new Map(), filled: /* @__PURE__ */ new Map() };
|
|
773
|
+
caches.set(scope, c);
|
|
774
|
+
}
|
|
775
|
+
return c;
|
|
776
|
+
}
|
|
777
|
+
var scopes = /* @__PURE__ */ new WeakMap();
|
|
778
|
+
function setFormulaScope(defs, scope) {
|
|
779
|
+
for (const d of defs) if (d && typeof d === "object") scopes.set(d, scope);
|
|
780
|
+
}
|
|
514
781
|
var MAX_DEPTH = 32;
|
|
515
|
-
function
|
|
782
|
+
function cellOf(ctx, name) {
|
|
783
|
+
const { row, scope } = ctx;
|
|
784
|
+
if (row && Object.hasOwn(row, name)) {
|
|
785
|
+
const v = row[name];
|
|
786
|
+
if (typeof v === "string" && v.startsWith("=") && v.length > 1) return formulaOf(ctx, name, v);
|
|
787
|
+
return v;
|
|
788
|
+
}
|
|
789
|
+
const f = scope?.computed(name);
|
|
790
|
+
if (f !== void 0) return formulaOf(ctx, name, f);
|
|
791
|
+
throw new FormulaError("#REF!");
|
|
792
|
+
}
|
|
793
|
+
function formulaOf(ctx, name, formula) {
|
|
794
|
+
if (ctx.seen.has(name) || ctx.depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
|
|
795
|
+
let r;
|
|
796
|
+
if (ctx.memo.has(name)) r = ctx.memo.get(name);
|
|
797
|
+
else {
|
|
798
|
+
const shared = ctx.scope ? cacheOf(ctx.scope).values.get(ctx.row) : void 0;
|
|
799
|
+
if (shared?.has(name)) r = shared.get(name);
|
|
800
|
+
else {
|
|
801
|
+
r = run(formula, { ...ctx, seen: /* @__PURE__ */ new Set([...ctx.seen, name]), depth: ctx.depth + 1 });
|
|
802
|
+
ctx.memo.set(name, r);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (isFormulaError(r)) throw new FormulaError(r);
|
|
806
|
+
return r;
|
|
807
|
+
}
|
|
808
|
+
function cellAt(ctx, index, name) {
|
|
809
|
+
const scope = ctx.scope;
|
|
810
|
+
if (!scope) throw new FormulaError("#REF!");
|
|
811
|
+
const c = cacheOf(scope);
|
|
812
|
+
if (index < 0 || index >= c.data.length) return null;
|
|
813
|
+
const row = c.data[index];
|
|
814
|
+
const done = c.filled.get(name) ?? -1;
|
|
815
|
+
if (index > done + 1 && index - done > 1) {
|
|
816
|
+
for (let k = done + 1; k < index; k++) valueIn(scope, c, c.data[k], name);
|
|
817
|
+
}
|
|
818
|
+
const v = valueIn(scope, c, row, name);
|
|
819
|
+
if (index > (c.filled.get(name) ?? -1)) c.filled.set(name, index);
|
|
820
|
+
if (isFormulaError(v)) throw new FormulaError(v);
|
|
821
|
+
return v;
|
|
822
|
+
}
|
|
823
|
+
function valueIn(scope, c, row, name) {
|
|
824
|
+
let m = c.values.get(row);
|
|
825
|
+
if (m?.has(name)) return m.get(name);
|
|
826
|
+
let v;
|
|
516
827
|
try {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
828
|
+
v = cellOf({ row, scope, seen: /* @__PURE__ */ new Set(), depth: 0, memo: /* @__PURE__ */ new Map() }, name);
|
|
829
|
+
} catch (e) {
|
|
830
|
+
v = e instanceof FormulaError ? e.code : "#ERROR!";
|
|
831
|
+
}
|
|
832
|
+
if (!m) c.values.set(row, m = /* @__PURE__ */ new Map());
|
|
833
|
+
m.set(name, v);
|
|
834
|
+
return v;
|
|
835
|
+
}
|
|
836
|
+
function indexOf(ctx) {
|
|
837
|
+
if (!ctx.scope) throw new FormulaError("#REF!");
|
|
838
|
+
const c = cacheOf(ctx.scope);
|
|
839
|
+
if (!c.index) c.index = new Map(c.data.map((r, i2) => [r, i2]));
|
|
840
|
+
const i = c.index.get(ctx.row);
|
|
841
|
+
if (i === void 0) throw new FormulaError("#REF!");
|
|
842
|
+
return i;
|
|
843
|
+
}
|
|
844
|
+
function column(ctx, name) {
|
|
845
|
+
if (!ctx.scope) throw new FormulaError("#REF!");
|
|
846
|
+
const scope = ctx.scope;
|
|
847
|
+
const c = cacheOf(scope);
|
|
848
|
+
let col = c.cols.get(name);
|
|
849
|
+
if (!col) {
|
|
850
|
+
c.cols.set(name, CYCLE);
|
|
851
|
+
col = c.data.map((r) => valueIn(scope, c, r, name));
|
|
852
|
+
c.cols.set(name, col);
|
|
853
|
+
}
|
|
854
|
+
if (col === CYCLE) throw new FormulaError("#CYCLE!");
|
|
855
|
+
return col;
|
|
856
|
+
}
|
|
857
|
+
var CYCLE = [];
|
|
858
|
+
function evaluate(n, ctx) {
|
|
859
|
+
switch (n.k) {
|
|
860
|
+
case "lit":
|
|
861
|
+
return n.v;
|
|
862
|
+
case "ref":
|
|
863
|
+
return cellOf(ctx, n.name);
|
|
864
|
+
case "un":
|
|
865
|
+
return n.op === "-" ? -num(evaluate(n.a, ctx)) : num(evaluate(n.a, ctx));
|
|
866
|
+
case "bin": {
|
|
867
|
+
const a = evaluate(n.a, ctx), b = evaluate(n.b, ctx);
|
|
868
|
+
switch (n.op) {
|
|
869
|
+
case "+":
|
|
870
|
+
return num(a) + num(b);
|
|
871
|
+
case "-":
|
|
872
|
+
return num(a) - num(b);
|
|
873
|
+
case "*":
|
|
874
|
+
return num(a) * num(b);
|
|
875
|
+
case "/":
|
|
876
|
+
case "%": {
|
|
877
|
+
const d = num(b);
|
|
878
|
+
if (d === 0) throw new FormulaError("#DIV/0!");
|
|
879
|
+
return n.op === "/" ? num(a) / d : num(a) % d;
|
|
880
|
+
}
|
|
881
|
+
case "^":
|
|
882
|
+
return num(a) ** num(b);
|
|
883
|
+
case "&":
|
|
884
|
+
return text(a) + text(b);
|
|
885
|
+
default: {
|
|
886
|
+
const c = compare(a, b);
|
|
887
|
+
return n.op === "=" ? c === 0 : n.op === "<>" ? c !== 0 : n.op === "<" ? c < 0 : n.op === ">" ? c > 0 : n.op === "<=" ? c <= 0 : c >= 0;
|
|
527
888
|
}
|
|
528
|
-
if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
|
|
529
|
-
return r;
|
|
530
889
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
890
|
+
}
|
|
891
|
+
case "call": {
|
|
892
|
+
if (n.rowFree && ctx.scope) {
|
|
893
|
+
const c = cacheOf(ctx.scope);
|
|
894
|
+
if (c.rowFree.has(n)) {
|
|
895
|
+
const v = c.rowFree.get(n);
|
|
896
|
+
if (v instanceof FormulaError) throw v;
|
|
897
|
+
return v;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
const v = call(n, ctx);
|
|
901
|
+
c.rowFree.set(n, v);
|
|
902
|
+
return v;
|
|
903
|
+
} catch (e) {
|
|
904
|
+
if (e instanceof FormulaError) c.rowFree.set(n, e);
|
|
905
|
+
throw e;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return call(n, ctx);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function call(n, ctx) {
|
|
913
|
+
const { name, args } = n;
|
|
914
|
+
const ev = (i) => evaluate(args[i], ctx);
|
|
915
|
+
switch (name) {
|
|
916
|
+
case "IF": {
|
|
917
|
+
if (args.length < 2) throw new FormulaError("#ERROR!");
|
|
918
|
+
return bool(ev(0)) ? ev(1) : args.length > 2 ? ev(2) : false;
|
|
919
|
+
}
|
|
920
|
+
case "IFS":
|
|
921
|
+
for (let i = 0; i + 1 < args.length; i += 2) if (bool(ev(i))) return ev(i + 1);
|
|
922
|
+
throw new FormulaError("#N/A");
|
|
923
|
+
case "IFERROR":
|
|
924
|
+
case "IFNA": {
|
|
925
|
+
try {
|
|
926
|
+
const v = ev(0);
|
|
927
|
+
if (isFormulaError(v) && (name === "IFERROR" || v === "#N/A")) return args.length > 1 ? ev(1) : "";
|
|
928
|
+
return v;
|
|
929
|
+
} catch (e) {
|
|
930
|
+
if (!(e instanceof FormulaError) || name === "IFNA" && e.code !== "#N/A") throw e;
|
|
931
|
+
return args.length > 1 ? ev(1) : "";
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
case "SWITCH": {
|
|
935
|
+
const v = ev(0);
|
|
936
|
+
for (let i = 1; i + 1 < args.length; i += 2) if (compare(v, ev(i)) === 0) return ev(i + 1);
|
|
937
|
+
if (args.length % 2 === 0) return ev(args.length - 1);
|
|
938
|
+
throw new FormulaError("#N/A");
|
|
939
|
+
}
|
|
940
|
+
case "CHOOSE": {
|
|
941
|
+
const i = Math.trunc(num(ev(0)));
|
|
942
|
+
if (i < 1 || i >= args.length) throw new FormulaError("#VALUE!");
|
|
943
|
+
return ev(i);
|
|
944
|
+
}
|
|
945
|
+
case "COL":
|
|
946
|
+
return column(ctx, args[0].name);
|
|
947
|
+
case "ROW":
|
|
948
|
+
return indexOf(ctx) + 1;
|
|
949
|
+
case "PREV":
|
|
950
|
+
case "NEXT": {
|
|
951
|
+
const step = args.length > 1 ? Math.trunc(num(ev(1))) : 1;
|
|
952
|
+
const at = indexOf(ctx) + (name === "PREV" ? -step : step);
|
|
953
|
+
const fieldName = args[0].name;
|
|
954
|
+
if (name === "NEXT" && ctx.depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
|
|
955
|
+
return name === "PREV" ? cellAt(ctx, at, fieldName) : nextCell(ctx, at, fieldName);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const fn = (Object.hasOwn(FUNCS, name) ? FUNCS[name] : void 0) ?? custom.get(name);
|
|
959
|
+
if (!fn) throw new FormulaError("#NAME?");
|
|
960
|
+
const values = args.map((_, i) => ev(i));
|
|
961
|
+
if (custom.has(name) && !Object.hasOwn(FUNCS, name)) {
|
|
962
|
+
try {
|
|
963
|
+
return fn(values);
|
|
964
|
+
} catch (e) {
|
|
965
|
+
throw e instanceof FormulaError ? e : new FormulaError("#VALUE!");
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return fn(values);
|
|
969
|
+
}
|
|
970
|
+
function nextCell(ctx, index, name) {
|
|
971
|
+
const c = ctx.scope ? cacheOf(ctx.scope) : null;
|
|
972
|
+
if (!c) throw new FormulaError("#REF!");
|
|
973
|
+
if (index < 0 || index >= c.data.length) return null;
|
|
974
|
+
return cellOf({ ...ctx, row: c.data[index], seen: /* @__PURE__ */ new Set(), depth: ctx.depth + 1, memo: /* @__PURE__ */ new Map() }, name);
|
|
975
|
+
}
|
|
976
|
+
function run(formula, ctx) {
|
|
977
|
+
try {
|
|
978
|
+
const v = evaluate(compile(formula), ctx);
|
|
534
979
|
if (typeof v === "number" && !Number.isFinite(v)) return "#NUM!";
|
|
980
|
+
if (Array.isArray(v)) return "#VALUE!";
|
|
535
981
|
return v;
|
|
536
982
|
} catch (e) {
|
|
983
|
+
if (e instanceof RangeError) return "#CYCLE!";
|
|
537
984
|
return e instanceof FormulaError ? e.code : "#ERROR!";
|
|
538
985
|
}
|
|
539
986
|
}
|
|
987
|
+
function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0, memo = /* @__PURE__ */ new Map(), scope = null) {
|
|
988
|
+
return run(formula, { row, scope, seen: seen2, depth, memo });
|
|
989
|
+
}
|
|
990
|
+
function columnFormulaValue(def, row) {
|
|
991
|
+
const scope = scopes.get(def) ?? null;
|
|
992
|
+
const id = def.id ?? def.field;
|
|
993
|
+
if (def.formula) {
|
|
994
|
+
if (scope && id) {
|
|
995
|
+
const c = cacheOf(scope);
|
|
996
|
+
return valueIn(scope, c, row, id);
|
|
997
|
+
}
|
|
998
|
+
return run(def.formula, { row, scope, seen: new Set(id ? [id] : []), depth: 0, memo: /* @__PURE__ */ new Map() });
|
|
999
|
+
}
|
|
1000
|
+
if (scope) return valueIn(scope, cacheOf(scope), row, def.field);
|
|
1001
|
+
return run(row[def.field], { row, scope, seen: /* @__PURE__ */ new Set([def.field]), depth: 0, memo: /* @__PURE__ */ new Map() });
|
|
1002
|
+
}
|
|
540
1003
|
var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
|
|
1004
|
+
function formulaRefs(formula) {
|
|
1005
|
+
let n;
|
|
1006
|
+
try {
|
|
1007
|
+
n = compile(formula);
|
|
1008
|
+
} catch {
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
const out = /* @__PURE__ */ new Set();
|
|
1012
|
+
const walk = (x) => {
|
|
1013
|
+
if (x.k === "ref") out.add(x.name);
|
|
1014
|
+
else if (x.k === "un") walk(x.a);
|
|
1015
|
+
else if (x.k === "bin") {
|
|
1016
|
+
walk(x.a);
|
|
1017
|
+
walk(x.b);
|
|
1018
|
+
} else if (x.k === "call") x.args.forEach(walk);
|
|
1019
|
+
};
|
|
1020
|
+
walk(n);
|
|
1021
|
+
return [...out];
|
|
1022
|
+
}
|
|
541
1023
|
|
|
542
1024
|
// src/rows/values.ts
|
|
543
1025
|
function getCellValue(def, row) {
|
|
@@ -549,9 +1031,10 @@ function getCellValue(def, row) {
|
|
|
549
1031
|
return void 0;
|
|
550
1032
|
}
|
|
551
1033
|
}
|
|
1034
|
+
if (typeof def.formula === "string" && isFormula(def.formula)) return columnFormulaValue(def, row);
|
|
552
1035
|
if (def.field == null) return void 0;
|
|
553
1036
|
const raw = row[def.field];
|
|
554
|
-
return def.formulas && isFormula(raw) ?
|
|
1037
|
+
return def.formulas && isFormula(raw) ? columnFormulaValue(def, row) : raw;
|
|
555
1038
|
}
|
|
556
1039
|
|
|
557
1040
|
// src/interactions/filterRow.ts
|
|
@@ -764,13 +1247,13 @@ var FilterRow = class {
|
|
|
764
1247
|
function isBlank(v) {
|
|
765
1248
|
return v == null || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
|
|
766
1249
|
}
|
|
767
|
-
var
|
|
1250
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
768
1251
|
function toDateKey2(v) {
|
|
769
1252
|
if (isBlank(v)) return null;
|
|
770
1253
|
if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
|
|
771
1254
|
const d = v instanceof Date ? v : new Date(v);
|
|
772
1255
|
if (Number.isNaN(d.getTime())) return null;
|
|
773
|
-
return `${d.getFullYear()}-${
|
|
1256
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
774
1257
|
}
|
|
775
1258
|
function setKey(v) {
|
|
776
1259
|
if (isBlank(v)) return "";
|
|
@@ -925,7 +1408,7 @@ var DATE_PRESETS = {
|
|
|
925
1408
|
medium: { dateStyle: "medium" },
|
|
926
1409
|
long: { dateStyle: "long" }
|
|
927
1410
|
};
|
|
928
|
-
function
|
|
1411
|
+
function toDate2(v) {
|
|
929
1412
|
if (isBlank(v)) return null;
|
|
930
1413
|
if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
|
|
931
1414
|
if (typeof v === "string") {
|
|
@@ -954,13 +1437,13 @@ function create(type, format, locale) {
|
|
|
954
1437
|
}
|
|
955
1438
|
case "date": {
|
|
956
1439
|
if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
|
|
957
|
-
const
|
|
1440
|
+
const pad4 = (n) => String(n).padStart(2, "0");
|
|
958
1441
|
const withTime = format === "datetime";
|
|
959
1442
|
return (v) => {
|
|
960
|
-
const d =
|
|
1443
|
+
const d = toDate2(v);
|
|
961
1444
|
if (!d) return isBlank(v) ? "" : String(v);
|
|
962
|
-
const day = `${d.getFullYear()}-${
|
|
963
|
-
return withTime ? `${day} ${
|
|
1445
|
+
const day = `${d.getFullYear()}-${pad4(d.getMonth() + 1)}-${pad4(d.getDate())}`;
|
|
1446
|
+
return withTime ? `${day} ${pad4(d.getHours())}:${pad4(d.getMinutes())}` : day;
|
|
964
1447
|
};
|
|
965
1448
|
}
|
|
966
1449
|
let opts = DATE_PRESETS.date;
|
|
@@ -970,7 +1453,7 @@ function create(type, format, locale) {
|
|
|
970
1453
|
} else if (format) opts = format;
|
|
971
1454
|
const df = new Intl.DateTimeFormat(locale, opts);
|
|
972
1455
|
return (v) => {
|
|
973
|
-
const d =
|
|
1456
|
+
const d = toDate2(v);
|
|
974
1457
|
return d ? df.format(d) : isBlank(v) ? "" : String(v);
|
|
975
1458
|
};
|
|
976
1459
|
}
|
|
@@ -1173,6 +1656,8 @@ var Renderer = class {
|
|
|
1173
1656
|
this.frame = 0;
|
|
1174
1657
|
this.headerDirty = true;
|
|
1175
1658
|
this.version = 0;
|
|
1659
|
+
/** Per-row repaint counters on top of `version` (live updates paint only the rows that changed). */
|
|
1660
|
+
this.rowVersions = /* @__PURE__ */ new Map();
|
|
1176
1661
|
this.lastRange = { first: 0, last: -1 };
|
|
1177
1662
|
/** Pinned summary section (top or bottom) and its per-region inners. */
|
|
1178
1663
|
this.summaryEl = null;
|
|
@@ -1298,7 +1783,15 @@ var Renderer = class {
|
|
|
1298
1783
|
}
|
|
1299
1784
|
invalidate(what = {}) {
|
|
1300
1785
|
if (what.header) this.headerDirty = true;
|
|
1301
|
-
if (what.rows)
|
|
1786
|
+
if (what.rows) {
|
|
1787
|
+
this.version++;
|
|
1788
|
+
this.rowVersions.clear();
|
|
1789
|
+
}
|
|
1790
|
+
this.schedule();
|
|
1791
|
+
}
|
|
1792
|
+
/** Paint these rows again on the next frame; the others stay as they are. */
|
|
1793
|
+
invalidateRowIds(ids) {
|
|
1794
|
+
for (const id of ids) this.rowVersions.set(id, (this.rowVersions.get(id) ?? 0) + 1);
|
|
1302
1795
|
this.schedule();
|
|
1303
1796
|
}
|
|
1304
1797
|
schedule() {
|
|
@@ -1416,18 +1909,18 @@ var Renderer = class {
|
|
|
1416
1909
|
return;
|
|
1417
1910
|
}
|
|
1418
1911
|
const filtered = !rowModel.server && store.state.data.length > 0;
|
|
1419
|
-
const
|
|
1912
|
+
const custom2 = kind === "loading" ? options.loadingOverlay : options.noRowsOverlay;
|
|
1420
1913
|
const key = `${kind}|${filtered}|${store.state.locale.code}`;
|
|
1421
1914
|
if (key === this.overlayKey) return;
|
|
1422
1915
|
this.overlayKey = key;
|
|
1423
|
-
this.overlay.classList.toggle("fg-overlay-custom", typeof
|
|
1424
|
-
if (
|
|
1916
|
+
this.overlay.classList.toggle("fg-overlay-custom", typeof custom2 === "function");
|
|
1917
|
+
if (custom2 === void 0 || custom2 === null) {
|
|
1425
1918
|
this.overlay.textContent = kind === "loading" ? store.state.locale.loading : store.state.locale.noRows;
|
|
1426
|
-
} else if (typeof
|
|
1427
|
-
this.overlay.textContent =
|
|
1919
|
+
} else if (typeof custom2 === "string") {
|
|
1920
|
+
this.overlay.textContent = custom2;
|
|
1428
1921
|
} else {
|
|
1429
1922
|
try {
|
|
1430
|
-
const out =
|
|
1923
|
+
const out = custom2({ filtered });
|
|
1431
1924
|
if (typeof out === "string") this.overlay.textContent = out;
|
|
1432
1925
|
else this.overlay.replaceChildren(out);
|
|
1433
1926
|
} catch {
|
|
@@ -1645,7 +2138,7 @@ var Renderer = class {
|
|
|
1645
2138
|
const inRange = hits.map((b) => `${b.c1}-${b.c2}`).join(",");
|
|
1646
2139
|
const merge2 = this.mergeState(spans, cols, i, range.first);
|
|
1647
2140
|
const pos = node.kind === "detail" ? `${this.scroller.scrollLeft}:${this.scroller.clientWidth}` : "";
|
|
1648
|
-
const key = `${node.id}|${this.version}|${colKey}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
|
|
2141
|
+
const key = `${node.id}|${this.version}.${this.rowVersions.get(node.id) ?? 0}|${colKey}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
|
|
1649
2142
|
let row = map.get(i);
|
|
1650
2143
|
if (!row) {
|
|
1651
2144
|
const rowEl = this.pool[r].pop() ?? el("div", "fg-row", { role: "row" });
|
|
@@ -1775,8 +2268,11 @@ var Renderer = class {
|
|
|
1775
2268
|
if (def.wrapText) cls += " fg-cell-wrap";
|
|
1776
2269
|
if (typeof def.editable === "function" && i >= 0 && !cellEditable(def, node.data, i)) cls += " fg-cell-readonly";
|
|
1777
2270
|
if (i >= 0) cls += this.grid.finder.classFor(i, col.id);
|
|
1778
|
-
|
|
2271
|
+
cls += this.grid.flashClass(node.id, col.id);
|
|
2272
|
+
const cf = i >= 0 && !node.kind ? this.grid.cellFormat(node.data, col.id, value) : null;
|
|
2273
|
+
if (cf?.bar) cls += " fg-cf-bar";
|
|
1779
2274
|
cell.className = cls;
|
|
2275
|
+
this.applyFormat(cell, cf);
|
|
1780
2276
|
if (def.sparkline && !def.renderer) {
|
|
1781
2277
|
const nums = Array.isArray(value) ? value.map(Number).filter(Number.isFinite) : [];
|
|
1782
2278
|
if (nums.length) cell.replaceChildren(sparkline(nums, def.sparkline === true ? {} : def.sparkline, col.width, this.opts.rowHeight));
|
|
@@ -1786,6 +2282,12 @@ var Renderer = class {
|
|
|
1786
2282
|
}
|
|
1787
2283
|
if (!def.renderer) {
|
|
1788
2284
|
cell.textContent = formatted;
|
|
2285
|
+
if (cf?.icon) {
|
|
2286
|
+
const icon = el("span", "fg-cf-icon", { "aria-hidden": "true" });
|
|
2287
|
+
icon.textContent = cf.icon.char;
|
|
2288
|
+
icon.style.color = cf.icon.color;
|
|
2289
|
+
cell.prepend(icon);
|
|
2290
|
+
}
|
|
1789
2291
|
return;
|
|
1790
2292
|
}
|
|
1791
2293
|
try {
|
|
@@ -1797,6 +2299,36 @@ var Renderer = class {
|
|
|
1797
2299
|
cell.textContent = "";
|
|
1798
2300
|
}
|
|
1799
2301
|
}
|
|
2302
|
+
/** Inline styles of a conditional format; pooled cells are reset first. */
|
|
2303
|
+
applyFormat(cell, cf) {
|
|
2304
|
+
const st = cell.style;
|
|
2305
|
+
if (!cf) {
|
|
2306
|
+
if (cell.dataset.cf) {
|
|
2307
|
+
st.color = st.background = st.fontWeight = st.fontStyle = st.textDecoration = "";
|
|
2308
|
+
st.removeProperty("--fg-bar-from");
|
|
2309
|
+
st.removeProperty("--fg-bar-to");
|
|
2310
|
+
st.removeProperty("--fg-bar-color");
|
|
2311
|
+
delete cell.dataset.cf;
|
|
2312
|
+
}
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
cell.dataset.cf = "1";
|
|
2316
|
+
const s = cf.style;
|
|
2317
|
+
st.color = s.color ?? "";
|
|
2318
|
+
st.background = s.background ?? "";
|
|
2319
|
+
st.fontWeight = s.bold ? "600" : "";
|
|
2320
|
+
st.fontStyle = s.italic ? "italic" : "";
|
|
2321
|
+
st.textDecoration = s.strike ? "line-through" : "";
|
|
2322
|
+
if (cf.bar) {
|
|
2323
|
+
st.setProperty("--fg-bar-from", `${(cf.bar.from * 100).toFixed(2)}%`);
|
|
2324
|
+
st.setProperty("--fg-bar-to", `${(cf.bar.to * 100).toFixed(2)}%`);
|
|
2325
|
+
st.setProperty("--fg-bar-color", cf.bar.color);
|
|
2326
|
+
} else {
|
|
2327
|
+
st.removeProperty("--fg-bar-from");
|
|
2328
|
+
st.removeProperty("--fg-bar-to");
|
|
2329
|
+
st.removeProperty("--fg-bar-color");
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
1800
2332
|
cellAt(target) {
|
|
1801
2333
|
const cell = target instanceof Element ? target.closest(".fg-cell") : null;
|
|
1802
2334
|
const row = cell?.parentElement;
|
|
@@ -2236,9 +2768,9 @@ var Renderer = class {
|
|
|
2236
2768
|
}
|
|
2237
2769
|
const { scroller, canvas } = this.cards;
|
|
2238
2770
|
const cols = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
|
|
2239
|
-
const
|
|
2771
|
+
const line2 = Math.round(this.opts.rowHeight * 0.75);
|
|
2240
2772
|
const select = !!grid.options.rowSelection;
|
|
2241
|
-
const cardH = 20 + cols.length *
|
|
2773
|
+
const cardH = 20 + cols.length * line2 + (select ? line2 : 0);
|
|
2242
2774
|
const gap = 10;
|
|
2243
2775
|
canvas.style.height = `${count * (cardH + gap)}px`;
|
|
2244
2776
|
const first = Math.max(0, Math.floor(scroller.scrollTop / (cardH + gap)) - 2);
|
|
@@ -2252,7 +2784,7 @@ var Renderer = class {
|
|
|
2252
2784
|
card.dataset.row = String(i);
|
|
2253
2785
|
card.style.transform = `translateY(${i * (cardH + gap)}px)`;
|
|
2254
2786
|
card.style.height = `${cardH}px`;
|
|
2255
|
-
card.style.setProperty("--fg-card-line", `${
|
|
2787
|
+
card.style.setProperty("--fg-card-line", `${line2}px`);
|
|
2256
2788
|
if (node.kind === "group" && node.group) {
|
|
2257
2789
|
const def = grid.columns.getNode(node.group.colId)?.def;
|
|
2258
2790
|
card.textContent = `${def?.header ?? node.group.colId}: ${String(node.group.value ?? locale.blanks)} (${node.group.rows.length})`;
|
|
@@ -2582,6 +3114,7 @@ var en = {
|
|
|
2582
3114
|
clearFilters: "Clear all filters",
|
|
2583
3115
|
exportCsv: "Export CSV",
|
|
2584
3116
|
exportExcel: "Export Excel",
|
|
3117
|
+
print: "Print\u2026",
|
|
2585
3118
|
pivot: "Pivot",
|
|
2586
3119
|
pivotMode: "Pivot mode",
|
|
2587
3120
|
pivotRows: "Row groups",
|
|
@@ -2598,6 +3131,22 @@ var en = {
|
|
|
2598
3131
|
chartCombo: "Bar and line chart",
|
|
2599
3132
|
chartScatter: "Scatter chart",
|
|
2600
3133
|
freeBadge: "Free version",
|
|
3134
|
+
exportPdf: "Export PDF",
|
|
3135
|
+
exportJson: "Export JSON",
|
|
3136
|
+
ai: { title: "AI assistant", placeholder: "Ask about the data or tell the grid what to do", send: "Send", thinking: "Working\u2026", undo: "Undo", undone: "Undone", failed: "The assistant did not answer", skipped: "Skipped", done: "Done" },
|
|
3137
|
+
cf: {
|
|
3138
|
+
title: "Conditional formatting",
|
|
3139
|
+
add: "Add rule",
|
|
3140
|
+
clear: "Clear rules",
|
|
3141
|
+
none: "No rules for this column",
|
|
3142
|
+
type: "Rule",
|
|
3143
|
+
style: "Format",
|
|
3144
|
+
wholeRow: "Whole row",
|
|
3145
|
+
count: "Count",
|
|
3146
|
+
types: { value: "Cell value", formula: "Formula", top: "Top", bottom: "Bottom", aboveAverage: "Above average", belowAverage: "Below average", duplicate: "Duplicate values", unique: "Unique values", dataBar: "Data bars", colorScale: "Color scale", iconSet: "Icon set" },
|
|
3147
|
+
ops: { notBetween: "Not between", notContains: "Does not contain" },
|
|
3148
|
+
styles: { red: "Light red fill", yellow: "Yellow fill", green: "Green fill", blue: "Blue fill", redText: "Red text", bold: "Bold" }
|
|
3149
|
+
},
|
|
2601
3150
|
ops: {
|
|
2602
3151
|
contains: "Contains",
|
|
2603
3152
|
equals: "Equals",
|
|
@@ -2702,9 +3251,9 @@ function nextFocus(key, mods, p, s) {
|
|
|
2702
3251
|
case "End":
|
|
2703
3252
|
return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
|
|
2704
3253
|
case "Tab": {
|
|
2705
|
-
const
|
|
2706
|
-
if (
|
|
2707
|
-
return { rowIndex: Math.floor(
|
|
3254
|
+
const flat2 = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
|
|
3255
|
+
if (flat2 < 0 || flat2 >= s.rows * s.cols) return null;
|
|
3256
|
+
return { rowIndex: Math.floor(flat2 / s.cols), colIndex: flat2 % s.cols };
|
|
2708
3257
|
}
|
|
2709
3258
|
default:
|
|
2710
3259
|
return null;
|
|
@@ -2924,7 +3473,7 @@ function attachBody(grid) {
|
|
|
2924
3473
|
const box = under?.getBoundingClientRect();
|
|
2925
3474
|
let target = null;
|
|
2926
3475
|
let position = "after";
|
|
2927
|
-
let
|
|
3476
|
+
let line2;
|
|
2928
3477
|
if (under && box) {
|
|
2929
3478
|
const over = into.rowModel.getRow(Number(under.dataset.row));
|
|
2930
3479
|
if (!over || over.kind === "group") return;
|
|
@@ -2933,17 +3482,17 @@ function attachBody(grid) {
|
|
|
2933
3482
|
if (over.kind === "detail") position = "after";
|
|
2934
3483
|
else if (into.options.treeData) position = y < 0.25 ? "before" : y > 0.75 ? "after" : "inside";
|
|
2935
3484
|
else position = y < 0.5 ? "before" : "after";
|
|
2936
|
-
|
|
3485
|
+
line2 = position === "before" ? box.top : box.bottom;
|
|
2937
3486
|
} else {
|
|
2938
3487
|
if (!point.closest(".fg-body")) return;
|
|
2939
3488
|
const rows = intoRoot.querySelectorAll(".fg-body-pane-center .fg-row[data-row]");
|
|
2940
|
-
|
|
3489
|
+
line2 = rows.length ? Math.max(...[...rows].map((r) => r.getBoundingClientRect().bottom)) : intoRoot.querySelector(".fg-body").getBoundingClientRect().top;
|
|
2941
3490
|
}
|
|
2942
3491
|
if (!into.canDropRow(row, target, position, grid)) return;
|
|
2943
3492
|
drop = { into, target, position };
|
|
2944
3493
|
const top = intoRoot.getBoundingClientRect().top;
|
|
2945
3494
|
marker.classList.toggle("fg-row-drop-inside", position === "inside");
|
|
2946
|
-
marker.style.top = `${position === "inside" ? box.top - top :
|
|
3495
|
+
marker.style.top = `${position === "inside" ? box.top - top : line2 - top - 1}px`;
|
|
2947
3496
|
marker.style.height = position === "inside" ? `${box.height}px` : "";
|
|
2948
3497
|
intoRoot.append(marker);
|
|
2949
3498
|
};
|
|
@@ -3527,6 +4076,7 @@ var SideBar = class {
|
|
|
3527
4076
|
this.grid.renderer.invalidate({ header: true, rows: true });
|
|
3528
4077
|
}
|
|
3529
4078
|
syncTabs() {
|
|
4079
|
+
this.el.classList.toggle("fg-sidebar-open", this.isOpen);
|
|
3530
4080
|
for (const [p, tab] of this.tabs) {
|
|
3531
4081
|
const on = this.isOpen && p === this.current;
|
|
3532
4082
|
tab.setAttribute("aria-expanded", String(on));
|
|
@@ -3640,9 +4190,13 @@ var ContextMenu = class {
|
|
|
3640
4190
|
items.push(
|
|
3641
4191
|
"separator",
|
|
3642
4192
|
{ label: loc.exportCsv, action: () => grid.api.exportCsv() },
|
|
3643
|
-
{ label: loc.exportExcel, action: () => grid.api.exportXlsx() }
|
|
4193
|
+
{ label: loc.exportExcel, action: () => grid.api.exportXlsx() },
|
|
4194
|
+
{ label: loc.exportJson, action: () => grid.api.exportJson() },
|
|
4195
|
+
...grid.options.assistant?.url ? [{ label: `${loc.ai.title}\u2026`, action: () => grid.api.openAssistant() }] : [],
|
|
4196
|
+
{ label: loc.exportPdf, action: () => grid.api.exportPdf() }
|
|
3644
4197
|
);
|
|
3645
4198
|
if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
|
|
4199
|
+
items.push("separator", { label: loc.print, action: () => grid.api.print() });
|
|
3646
4200
|
return items;
|
|
3647
4201
|
}
|
|
3648
4202
|
pasteFromClipboard() {
|
|
@@ -3665,11 +4219,11 @@ var ContextMenu = class {
|
|
|
3665
4219
|
value: node && col && !isSystemCol(colId) ? getCellValue(col.def, node.data) : void 0,
|
|
3666
4220
|
defaultItems: defaults
|
|
3667
4221
|
};
|
|
3668
|
-
const
|
|
4222
|
+
const custom2 = grid.options.contextMenu;
|
|
3669
4223
|
let items = defaults;
|
|
3670
|
-
if (typeof
|
|
4224
|
+
if (typeof custom2 === "function") {
|
|
3671
4225
|
try {
|
|
3672
|
-
items =
|
|
4226
|
+
items = custom2(params);
|
|
3673
4227
|
} catch {
|
|
3674
4228
|
items = defaults;
|
|
3675
4229
|
}
|
|
@@ -3929,10 +4483,10 @@ var Finder = class {
|
|
|
3929
4483
|
const bar = el("div", "fg-findbar", { role: "search" });
|
|
3930
4484
|
const input = el("input", "fg-input", { type: "search", placeholder: loc.findPlaceholder, "aria-label": loc.findPlaceholder });
|
|
3931
4485
|
this.countEl = el("span", "fg-findbar-count", { "aria-live": "polite" });
|
|
3932
|
-
const btn = (label, text2,
|
|
4486
|
+
const btn = (label, text2, run2) => {
|
|
3933
4487
|
const b = el("button", "fg-findbar-btn", { type: "button", "aria-label": label, title: label });
|
|
3934
4488
|
b.textContent = text2;
|
|
3935
|
-
b.addEventListener("click",
|
|
4489
|
+
b.addEventListener("click", run2);
|
|
3936
4490
|
return b;
|
|
3937
4491
|
};
|
|
3938
4492
|
bar.append(
|
|
@@ -3968,7 +4522,7 @@ var Finder = class {
|
|
|
3968
4522
|
};
|
|
3969
4523
|
|
|
3970
4524
|
// src/fill.ts
|
|
3971
|
-
var
|
|
4525
|
+
var DAY2 = 864e5;
|
|
3972
4526
|
function fillSeries(source, count) {
|
|
3973
4527
|
if (!source.length || count <= 0) return [];
|
|
3974
4528
|
const repeat = () => Array.from({ length: count }, (_, i) => source[i % source.length]);
|
|
@@ -3980,9 +4534,9 @@ function fillSeries(source, count) {
|
|
|
3980
4534
|
const last2 = nums[nums.length - 1];
|
|
3981
4535
|
return Array.from({ length: count }, (_, i) => Math.round((last2 + step * (i + 1)) * 1e12) / 1e12);
|
|
3982
4536
|
}
|
|
3983
|
-
const dates = source.map((v) => v instanceof Date || typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v) ?
|
|
4537
|
+
const dates = source.map((v) => v instanceof Date || typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v) ? toDate2(v) : null);
|
|
3984
4538
|
if (dates.every(Boolean) && source.length >= 2) {
|
|
3985
|
-
const times = dates.map((d) => Math.round(d.getTime() /
|
|
4539
|
+
const times = dates.map((d) => Math.round(d.getTime() / DAY2));
|
|
3986
4540
|
const step = times[1] - times[0];
|
|
3987
4541
|
if (times.every((t, i) => i === 0 || t - times[i - 1] === step)) {
|
|
3988
4542
|
const lastDate = dates[dates.length - 1];
|
|
@@ -4117,7 +4671,7 @@ function parseDate(text2, previous) {
|
|
|
4117
4671
|
let m = /^(\d{4})\s*[-./년]\s*(\d{1,2})\s*[-./월]\s*(\d{1,2})\s*[.일]?$/.exec(s);
|
|
4118
4672
|
if (m) date = validDate(+m[1], +m[2], +m[3]);
|
|
4119
4673
|
else if (m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s)) date = validDate(+m[3], +m[1], +m[2]);
|
|
4120
|
-
else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date =
|
|
4674
|
+
else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date = toDate2(s);
|
|
4121
4675
|
if (!date) return { ok: false };
|
|
4122
4676
|
return { ok: true, value: previous instanceof Date ? date : toDateKey2(date) };
|
|
4123
4677
|
}
|
|
@@ -4141,16 +4695,16 @@ function parseValue(def, text2, previous) {
|
|
|
4141
4695
|
|
|
4142
4696
|
// src/editing/editor.ts
|
|
4143
4697
|
var normalizeOptions = (opts) => opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o), disabled: false });
|
|
4144
|
-
var
|
|
4698
|
+
var pad3 = (n) => String(n).padStart(2, "0");
|
|
4145
4699
|
function localDateTime(v) {
|
|
4146
4700
|
const d = v instanceof Date ? v : typeof v === "string" && v ? new Date(v) : null;
|
|
4147
4701
|
if (!d || Number.isNaN(d.getTime())) return "";
|
|
4148
|
-
return `${d.getFullYear()}-${
|
|
4702
|
+
return `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}T${pad3(d.getHours())}:${pad3(d.getMinutes())}`;
|
|
4149
4703
|
}
|
|
4150
4704
|
function timeOf(v) {
|
|
4151
|
-
if (v instanceof Date) return `${
|
|
4705
|
+
if (v instanceof Date) return `${pad3(v.getHours())}:${pad3(v.getMinutes())}`;
|
|
4152
4706
|
const m = typeof v === "string" ? /(\d{1,2}):(\d{2})/.exec(v) : null;
|
|
4153
|
-
return m ? `${
|
|
4707
|
+
return m ? `${pad3(Number(m[1]))}:${m[2]}` : "";
|
|
4154
4708
|
}
|
|
4155
4709
|
var CellEditor = class {
|
|
4156
4710
|
constructor(grid) {
|
|
@@ -4225,20 +4779,20 @@ var CellEditor = class {
|
|
|
4225
4779
|
grid.setFocus(rowIndex, colId);
|
|
4226
4780
|
renderer.ensureVisible(rowIndex, colId);
|
|
4227
4781
|
renderer.render();
|
|
4228
|
-
const
|
|
4229
|
-
this.place(
|
|
4782
|
+
const wrap2 = el("div", "fg-editor");
|
|
4783
|
+
this.place(wrap2, rowIndex, colId);
|
|
4230
4784
|
let control;
|
|
4231
4785
|
let read;
|
|
4232
4786
|
let dispose = () => {
|
|
4233
4787
|
};
|
|
4234
4788
|
const spec = def.editor;
|
|
4235
4789
|
if (typeof spec === "function") {
|
|
4236
|
-
const
|
|
4237
|
-
control =
|
|
4238
|
-
read = () => ({ value:
|
|
4239
|
-
dispose = () =>
|
|
4240
|
-
|
|
4241
|
-
queueMicrotask(() =>
|
|
4790
|
+
const custom2 = spec({ value, row, column: def, initialText });
|
|
4791
|
+
control = custom2.el;
|
|
4792
|
+
read = () => ({ value: custom2.getValue() });
|
|
4793
|
+
dispose = () => custom2.destroy?.();
|
|
4794
|
+
wrap2.append(control);
|
|
4795
|
+
queueMicrotask(() => custom2.focus?.());
|
|
4242
4796
|
} else if (spec?.type === "select") {
|
|
4243
4797
|
const options = normalizeOptions(typeof spec.options === "function" ? spec.options(row) : spec.options);
|
|
4244
4798
|
const select = el("select", "fg-editor-control");
|
|
@@ -4252,7 +4806,7 @@ var CellEditor = class {
|
|
|
4252
4806
|
select.selectedIndex = Math.max(0, options.findIndex((o) => Object.is(o.value, value)));
|
|
4253
4807
|
control = select;
|
|
4254
4808
|
read = () => ({ value: options[select.selectedIndex]?.value ?? null });
|
|
4255
|
-
|
|
4809
|
+
wrap2.append(select);
|
|
4256
4810
|
} else if (spec?.type === "time" || spec?.type === "datetime" || !spec && def.type === "date" && def.format === "datetime") {
|
|
4257
4811
|
const isTime = spec?.type === "time";
|
|
4258
4812
|
const input = el("input", "fg-editor-control", { type: isTime ? "time" : "datetime-local" });
|
|
@@ -4263,7 +4817,7 @@ var CellEditor = class {
|
|
|
4263
4817
|
if (isTime) return { value: input.value };
|
|
4264
4818
|
return { value: value instanceof Date ? new Date(input.value) : input.value };
|
|
4265
4819
|
};
|
|
4266
|
-
|
|
4820
|
+
wrap2.append(input);
|
|
4267
4821
|
} else {
|
|
4268
4822
|
const textarea = spec?.type === "textarea";
|
|
4269
4823
|
const input = textarea ? el("textarea", "fg-editor-control") : el("input", "fg-editor-control");
|
|
@@ -4275,11 +4829,11 @@ var CellEditor = class {
|
|
|
4275
4829
|
input.value = initialText ?? start;
|
|
4276
4830
|
control = input;
|
|
4277
4831
|
read = () => ({ text: input.value });
|
|
4278
|
-
|
|
4832
|
+
wrap2.append(input);
|
|
4279
4833
|
}
|
|
4280
4834
|
control.setAttribute("aria-label", def.header ?? def.field ?? colId);
|
|
4281
4835
|
const msg = el("div", "fg-editor-msg", { role: "alert" });
|
|
4282
|
-
|
|
4836
|
+
wrap2.append(msg);
|
|
4283
4837
|
const onKey = (e) => {
|
|
4284
4838
|
e.stopPropagation();
|
|
4285
4839
|
if (e.key === "Escape") {
|
|
@@ -4295,26 +4849,26 @@ var CellEditor = class {
|
|
|
4295
4849
|
}
|
|
4296
4850
|
};
|
|
4297
4851
|
const onFocusOut = (e) => {
|
|
4298
|
-
if (this.cur?.wrap ===
|
|
4852
|
+
if (this.cur?.wrap === wrap2 && !wrap2.contains(e.relatedTarget)) this.commit(null);
|
|
4299
4853
|
};
|
|
4300
4854
|
const onScroll = () => this.commit(null);
|
|
4301
|
-
|
|
4302
|
-
|
|
4855
|
+
wrap2.addEventListener("keydown", onKey);
|
|
4856
|
+
wrap2.addEventListener("focusout", onFocusOut);
|
|
4303
4857
|
renderer.scroller.addEventListener("scroll", onScroll, { passive: true });
|
|
4304
|
-
renderer.root.append(
|
|
4858
|
+
renderer.root.append(wrap2);
|
|
4305
4859
|
this.cur = {
|
|
4306
4860
|
rowIndex,
|
|
4307
4861
|
colId,
|
|
4308
4862
|
row,
|
|
4309
4863
|
def,
|
|
4310
|
-
wrap,
|
|
4864
|
+
wrap: wrap2,
|
|
4311
4865
|
read,
|
|
4312
4866
|
dispose: () => {
|
|
4313
|
-
|
|
4314
|
-
|
|
4867
|
+
wrap2.removeEventListener("keydown", onKey);
|
|
4868
|
+
wrap2.removeEventListener("focusout", onFocusOut);
|
|
4315
4869
|
renderer.scroller.removeEventListener("scroll", onScroll);
|
|
4316
4870
|
dispose();
|
|
4317
|
-
|
|
4871
|
+
wrap2.remove();
|
|
4318
4872
|
}
|
|
4319
4873
|
};
|
|
4320
4874
|
if (typeof spec !== "function") {
|
|
@@ -4325,15 +4879,15 @@ var CellEditor = class {
|
|
|
4325
4879
|
return true;
|
|
4326
4880
|
}
|
|
4327
4881
|
/** Put the editor over its cell; false when the cell is not rendered. */
|
|
4328
|
-
place(
|
|
4882
|
+
place(wrap2, rowIndex, colId) {
|
|
4329
4883
|
const { renderer } = this.grid;
|
|
4330
4884
|
const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
|
|
4331
4885
|
const rootBox = renderer.root.getBoundingClientRect();
|
|
4332
4886
|
const box = cellEl?.getBoundingClientRect();
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4887
|
+
wrap2.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
|
|
4888
|
+
wrap2.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
|
|
4889
|
+
wrap2.style.width = `${box?.width || this.grid.columns.getColumn(colId)?.width}px`;
|
|
4890
|
+
wrap2.style.height = `${box?.height || renderer.opts.rowHeight}px`;
|
|
4337
4891
|
return !!cellEl;
|
|
4338
4892
|
}
|
|
4339
4893
|
/** Display index of a row object, or -1 when it is no longer shown. */
|
|
@@ -4677,6 +5231,662 @@ function attachHeader(grid) {
|
|
|
4677
5231
|
};
|
|
4678
5232
|
}
|
|
4679
5233
|
|
|
5234
|
+
// src/export/pdf.ts
|
|
5235
|
+
var PAPER_MM = {
|
|
5236
|
+
A4: [210, 297],
|
|
5237
|
+
A3: [297, 420],
|
|
5238
|
+
A5: [148, 210],
|
|
5239
|
+
B4: [250, 353],
|
|
5240
|
+
B5: [176, 250],
|
|
5241
|
+
letter: [215.9, 279.4],
|
|
5242
|
+
legal: [215.9, 355.6]
|
|
5243
|
+
};
|
|
5244
|
+
var PX_PER_MM = 96 / 25.4;
|
|
5245
|
+
var PT_PER_MM = 72 / 25.4;
|
|
5246
|
+
var FONT = "system-ui, 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans CJK KR', sans-serif";
|
|
5247
|
+
var FS = 10.5;
|
|
5248
|
+
var LINE = 15;
|
|
5249
|
+
var PAD_X = 7;
|
|
5250
|
+
var PAD_Y = 4;
|
|
5251
|
+
var MAX_LINES = 8;
|
|
5252
|
+
function base64Bytes(b64) {
|
|
5253
|
+
const bin = atob(b64);
|
|
5254
|
+
const out = new Uint8Array(bin.length);
|
|
5255
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
5256
|
+
return out;
|
|
5257
|
+
}
|
|
5258
|
+
var browserSurface = {
|
|
5259
|
+
surface(width, height) {
|
|
5260
|
+
if (typeof document === "undefined") return null;
|
|
5261
|
+
const canvas = document.createElement("canvas");
|
|
5262
|
+
canvas.width = width;
|
|
5263
|
+
canvas.height = height;
|
|
5264
|
+
let ctx = null;
|
|
5265
|
+
try {
|
|
5266
|
+
ctx = canvas.getContext("2d");
|
|
5267
|
+
} catch {
|
|
5268
|
+
ctx = null;
|
|
5269
|
+
}
|
|
5270
|
+
if (!ctx) return null;
|
|
5271
|
+
return {
|
|
5272
|
+
ctx,
|
|
5273
|
+
jpeg: (q) => base64Bytes(canvas.toDataURL("image/jpeg", q).split(",")[1] ?? "")
|
|
5274
|
+
};
|
|
5275
|
+
}
|
|
5276
|
+
};
|
|
5277
|
+
var fontOf = (weight, size = FS) => `${weight} ${size}px ${FONT}`;
|
|
5278
|
+
function wrap(ctx, text2, width) {
|
|
5279
|
+
if (!text2) return [""];
|
|
5280
|
+
const out = [];
|
|
5281
|
+
for (const para of text2.split(/\r?\n/)) {
|
|
5282
|
+
let line2 = "";
|
|
5283
|
+
const parts = para.match(/[ -鿿가--]|[^\s -鿿가--]+\s*|\s+/g) ?? [""];
|
|
5284
|
+
for (const part of parts) {
|
|
5285
|
+
const next = line2 + part;
|
|
5286
|
+
if (!line2 || ctx.measureText(next.trimEnd()).width <= width) {
|
|
5287
|
+
if (!line2 && ctx.measureText(part.trimEnd()).width > width) {
|
|
5288
|
+
for (const ch of part) {
|
|
5289
|
+
if (line2 && ctx.measureText(line2 + ch).width > width) {
|
|
5290
|
+
out.push(line2);
|
|
5291
|
+
line2 = "";
|
|
5292
|
+
}
|
|
5293
|
+
line2 += ch;
|
|
5294
|
+
}
|
|
5295
|
+
} else line2 = next;
|
|
5296
|
+
} else {
|
|
5297
|
+
out.push(line2.trimEnd());
|
|
5298
|
+
line2 = part.trimStart();
|
|
5299
|
+
}
|
|
5300
|
+
if (out.length >= MAX_LINES) break;
|
|
5301
|
+
}
|
|
5302
|
+
out.push(line2.trimEnd());
|
|
5303
|
+
if (out.length >= MAX_LINES) break;
|
|
5304
|
+
}
|
|
5305
|
+
if (out.length > MAX_LINES) out.length = MAX_LINES;
|
|
5306
|
+
return out;
|
|
5307
|
+
}
|
|
5308
|
+
function geometry(m) {
|
|
5309
|
+
const [w, h] = PAPER_MM[m.paper] ?? PAPER_MM.A4;
|
|
5310
|
+
const [pw, ph] = m.orientation === "landscape" ? [h, w] : [w, h];
|
|
5311
|
+
const pageW = pw * PX_PER_MM, pageH = ph * PX_PER_MM;
|
|
5312
|
+
const margin = 12 * PX_PER_MM, marginBottom = 14 * PX_PER_MM;
|
|
5313
|
+
const contentW = pageW - margin * 2;
|
|
5314
|
+
const fixed = m.numbered ? 40 : 0;
|
|
5315
|
+
const data = m.widths.slice(m.numbered ? 1 : 0);
|
|
5316
|
+
const sum = data.reduce((a, b) => a + b, 0) || 1;
|
|
5317
|
+
const scale = m.fit ? (contentW - fixed) / sum : Math.min(1, (contentW - fixed) / sum);
|
|
5318
|
+
const colW = [...m.numbered ? [fixed] : [], ...data.map((x2) => x2 * scale)];
|
|
5319
|
+
const colX = [];
|
|
5320
|
+
let x = margin;
|
|
5321
|
+
for (const cw of colW) {
|
|
5322
|
+
colX.push(x);
|
|
5323
|
+
x += cw;
|
|
5324
|
+
}
|
|
5325
|
+
return { pageW, pageH, margin, marginBottom, colX, colW };
|
|
5326
|
+
}
|
|
5327
|
+
function placeLine(ctx, g, cells, kind, level) {
|
|
5328
|
+
const placed = { cells: [], kind, level, y: 0, h: 0 };
|
|
5329
|
+
let col = 0, h = 22;
|
|
5330
|
+
for (const cell of cells) {
|
|
5331
|
+
const span = Math.max(1, cell.span ?? 1);
|
|
5332
|
+
const x = g.colX[col] ?? g.margin;
|
|
5333
|
+
let w = 0;
|
|
5334
|
+
for (let k = col; k < col + span && k < g.colW.length; k++) w += g.colW[k];
|
|
5335
|
+
col += span;
|
|
5336
|
+
ctx.font = fontOf(kind === "group" ? 600 : kind === "total" || cell.bold ? 700 : 400);
|
|
5337
|
+
const indent = (cell.indent ?? 0) * 14;
|
|
5338
|
+
const lines = wrap(ctx, cell.text, Math.max(8, w - PAD_X * 2 - indent - (cell.icon ? 14 : 0)));
|
|
5339
|
+
h = Math.max(h, lines.length * LINE + PAD_Y * 2);
|
|
5340
|
+
placed.cells.push({ cell, x, w, y: 0, h: 0, lines });
|
|
5341
|
+
}
|
|
5342
|
+
placed.h = h;
|
|
5343
|
+
return placed;
|
|
5344
|
+
}
|
|
5345
|
+
function placeHead(ctx, g, head) {
|
|
5346
|
+
const rows = head.map(() => ({ cells: [], kind: "head", y: 0, h: 22 }));
|
|
5347
|
+
const taken = head.map(() => g.colW.map(() => false));
|
|
5348
|
+
const items = [];
|
|
5349
|
+
ctx.font = fontOf(600);
|
|
5350
|
+
head.forEach((tr, r) => {
|
|
5351
|
+
let col = 0;
|
|
5352
|
+
for (const cell of tr) {
|
|
5353
|
+
while (col < g.colW.length && taken[r][col]) col++;
|
|
5354
|
+
const span = Math.max(1, cell.span ?? 1), rs = Math.max(1, Math.min(head.length - r, cell.rowspan ?? 1));
|
|
5355
|
+
for (let a = r; a < r + rs; a++) for (let b = col; b < col + span && b < g.colW.length; b++) taken[a][b] = true;
|
|
5356
|
+
let w = 0;
|
|
5357
|
+
for (let k = col; k < col + span && k < g.colW.length; k++) w += g.colW[k];
|
|
5358
|
+
const lines = wrap(ctx, cell.text, Math.max(8, w - PAD_X * 2));
|
|
5359
|
+
items.push({ cell, r, col, span, rs, lines, x: g.colX[col] ?? g.margin, w });
|
|
5360
|
+
col += span;
|
|
5361
|
+
}
|
|
5362
|
+
});
|
|
5363
|
+
for (const it of items) {
|
|
5364
|
+
const need = it.lines.length * LINE + PAD_Y * 2;
|
|
5365
|
+
const each = need / it.rs;
|
|
5366
|
+
for (let a = it.r; a < it.r + it.rs; a++) rows[a].h = Math.max(rows[a].h, each);
|
|
5367
|
+
}
|
|
5368
|
+
for (const it of items) {
|
|
5369
|
+
rows[it.r].cells.push({ cell: it.cell, x: it.x, w: it.w, y: 0, h: rows.slice(it.r, it.r + it.rs).reduce((s, x) => s + x.h, 0), lines: it.lines });
|
|
5370
|
+
}
|
|
5371
|
+
return rows;
|
|
5372
|
+
}
|
|
5373
|
+
function titleHeight(m) {
|
|
5374
|
+
if (!m.title && !m.subtitle && !m.printed) return 0;
|
|
5375
|
+
return (m.title ? 24 : 12) + (m.subtitle ? 17 : 0) + 18;
|
|
5376
|
+
}
|
|
5377
|
+
function paginate(ctx, m) {
|
|
5378
|
+
const g = geometry(m);
|
|
5379
|
+
const head = placeHead(ctx, g, m.head);
|
|
5380
|
+
let n = 0;
|
|
5381
|
+
const lines = m.body.map((l) => {
|
|
5382
|
+
const p = placeLine(ctx, g, l.cells, l.kind, l.level);
|
|
5383
|
+
if (l.kind === "row") p.odd = n++ % 2 === 1;
|
|
5384
|
+
return p;
|
|
5385
|
+
});
|
|
5386
|
+
const repeated = lines.slice(0, m.repeatRows);
|
|
5387
|
+
const rest = lines.slice(m.repeatRows);
|
|
5388
|
+
const foot = m.foot.map((l) => placeLine(ctx, g, l.cells, l.kind, l.level));
|
|
5389
|
+
const bottom = g.pageH - g.marginBottom;
|
|
5390
|
+
const tH = titleHeight(m);
|
|
5391
|
+
const pages = [];
|
|
5392
|
+
const queue = [...rest, ...foot];
|
|
5393
|
+
const clone = (p) => ({ ...p, cells: p.cells.map((c) => ({ ...c })) });
|
|
5394
|
+
let i = 0;
|
|
5395
|
+
do {
|
|
5396
|
+
const first = pages.length === 0;
|
|
5397
|
+
const page = { rows: [], title: first || m.repeatTitle };
|
|
5398
|
+
let y = g.margin + (page.title ? tH : 0);
|
|
5399
|
+
const put = (p) => {
|
|
5400
|
+
const c = clone(p);
|
|
5401
|
+
c.y = y;
|
|
5402
|
+
for (const cell of c.cells) {
|
|
5403
|
+
cell.y = y;
|
|
5404
|
+
if (c.kind !== "head") cell.h = c.h;
|
|
5405
|
+
}
|
|
5406
|
+
page.rows.push(c);
|
|
5407
|
+
y += c.h;
|
|
5408
|
+
};
|
|
5409
|
+
if (first || m.repeatHeader) head.forEach(put);
|
|
5410
|
+
repeated.forEach(put);
|
|
5411
|
+
const top = y;
|
|
5412
|
+
while (i < queue.length) {
|
|
5413
|
+
const p = queue[i];
|
|
5414
|
+
const need = p.kind === "group" && i + 1 < queue.length ? p.h + queue[i + 1].h : p.h;
|
|
5415
|
+
if (y + need > bottom && y > top) break;
|
|
5416
|
+
put(p);
|
|
5417
|
+
i++;
|
|
5418
|
+
}
|
|
5419
|
+
pages.push(page);
|
|
5420
|
+
} while (i < queue.length && pages.length < 5e3);
|
|
5421
|
+
return { pages, g };
|
|
5422
|
+
}
|
|
5423
|
+
function line(ctx, x1, y1, x2, y2, color, width) {
|
|
5424
|
+
ctx.strokeStyle = color;
|
|
5425
|
+
ctx.lineWidth = width;
|
|
5426
|
+
ctx.beginPath();
|
|
5427
|
+
ctx.moveTo(x1, y1);
|
|
5428
|
+
ctx.lineTo(x2, y2);
|
|
5429
|
+
ctx.stroke();
|
|
5430
|
+
}
|
|
5431
|
+
function paintRow(ctx, row, m, g) {
|
|
5432
|
+
const right = g.colX.length ? g.colX[g.colX.length - 1] + g.colW[g.colW.length - 1] : g.pageW - g.margin;
|
|
5433
|
+
const rowFill = row.kind === "head" ? "#f3f4f6" : row.kind === "group" ? row.level ? "#eef2f7" : "#e2e8f0" : row.kind === "total" ? "#f3f4f6" : m.striped && row.odd ? "#f9fafb" : "";
|
|
5434
|
+
for (const c of row.cells) {
|
|
5435
|
+
const fill = c.cell.fill || rowFill;
|
|
5436
|
+
if (fill) {
|
|
5437
|
+
ctx.fillStyle = fill;
|
|
5438
|
+
ctx.fillRect(c.x, c.y, c.w, c.h);
|
|
5439
|
+
}
|
|
5440
|
+
if (c.cell.bar) {
|
|
5441
|
+
ctx.save();
|
|
5442
|
+
ctx.globalAlpha = 0.4;
|
|
5443
|
+
ctx.fillStyle = c.cell.bar.color;
|
|
5444
|
+
ctx.fillRect(c.x + c.w * c.cell.bar.from, c.y + c.h * 0.15, c.w * (c.cell.bar.to - c.cell.bar.from), c.h * 0.7);
|
|
5445
|
+
ctx.restore();
|
|
5446
|
+
}
|
|
5447
|
+
const weight = row.kind === "head" || row.kind === "group" ? 600 : row.kind === "total" ? 700 : c.cell.bold ? 600 : 400;
|
|
5448
|
+
ctx.font = `${c.cell.italic ? "italic " : ""}${fontOf(weight)}`;
|
|
5449
|
+
ctx.fillStyle = c.cell.num ? "#9ca3af" : c.cell.color || (row.kind === "head" ? "#374151" : "#111827");
|
|
5450
|
+
ctx.textBaseline = "middle";
|
|
5451
|
+
ctx.save();
|
|
5452
|
+
ctx.beginPath();
|
|
5453
|
+
ctx.rect(c.x, c.y, c.w, c.h);
|
|
5454
|
+
ctx.clip();
|
|
5455
|
+
const indent = (c.cell.indent ?? 0) * 14;
|
|
5456
|
+
const textH = c.lines.length * LINE;
|
|
5457
|
+
let ty = row.kind === "head" ? c.y + c.h - PAD_Y - textH + LINE / 2 : c.y + PAD_Y + LINE / 2;
|
|
5458
|
+
if (row.kind !== "head" && c.h > textH + PAD_Y * 2) ty = c.y + PAD_Y + LINE / 2;
|
|
5459
|
+
for (const [k, text2] of c.lines.entries()) {
|
|
5460
|
+
let x = c.x + PAD_X + indent;
|
|
5461
|
+
const wText = ctx.measureText(text2).width + (c.cell.icon && k === 0 ? 14 : 0);
|
|
5462
|
+
const align = c.cell.group ? "center" : c.cell.align;
|
|
5463
|
+
if (align === "right") x = c.x + c.w - PAD_X - wText;
|
|
5464
|
+
else if (align === "center") x = c.x + (c.w - wText) / 2;
|
|
5465
|
+
ctx.textAlign = "left";
|
|
5466
|
+
if (c.cell.icon && k === 0) {
|
|
5467
|
+
const keep = ctx.fillStyle;
|
|
5468
|
+
ctx.fillStyle = c.cell.icon.color;
|
|
5469
|
+
ctx.fillText(c.cell.icon.char, x, ty);
|
|
5470
|
+
ctx.fillStyle = keep;
|
|
5471
|
+
x += 14;
|
|
5472
|
+
}
|
|
5473
|
+
ctx.fillText(text2, x, ty);
|
|
5474
|
+
if (c.cell.strike) line(ctx, x, ty, x + ctx.measureText(text2).width, ty, String(ctx.fillStyle), 1);
|
|
5475
|
+
ty += LINE;
|
|
5476
|
+
}
|
|
5477
|
+
ctx.restore();
|
|
5478
|
+
if (row.kind === "head" && c.x > g.margin + 0.5) line(ctx, c.x, c.y, c.x, c.y + c.h, "#e5e7eb", 1);
|
|
5479
|
+
if (row.kind === "head") line(ctx, c.x, c.y + c.h, c.x + c.w, c.y + c.h, "#9ca3af", 1.5);
|
|
5480
|
+
}
|
|
5481
|
+
if (row.kind === "group") line(ctx, g.margin, row.y, right, row.y, "#cbd5e1", 1);
|
|
5482
|
+
if (row.kind === "total") line(ctx, g.margin, row.y, right, row.y, "#111827", 1.5);
|
|
5483
|
+
else if (row.kind !== "head") line(ctx, g.margin, row.y + row.h, right, row.y + row.h, "#e5e7eb", 1);
|
|
5484
|
+
}
|
|
5485
|
+
function paintTitle(ctx, m, g) {
|
|
5486
|
+
const h = titleHeight(m);
|
|
5487
|
+
if (!h) return;
|
|
5488
|
+
const right = g.pageW - g.margin;
|
|
5489
|
+
let y = g.margin;
|
|
5490
|
+
ctx.textAlign = "left";
|
|
5491
|
+
ctx.textBaseline = "top";
|
|
5492
|
+
if (m.title) {
|
|
5493
|
+
ctx.font = fontOf(700, 17);
|
|
5494
|
+
ctx.fillStyle = "#111827";
|
|
5495
|
+
ctx.fillText(m.title, g.margin, y);
|
|
5496
|
+
y += 24;
|
|
5497
|
+
}
|
|
5498
|
+
if (m.subtitle) {
|
|
5499
|
+
ctx.font = fontOf(400, 11);
|
|
5500
|
+
ctx.fillStyle = "#4b5563";
|
|
5501
|
+
ctx.fillText(m.subtitle, g.margin, y);
|
|
5502
|
+
y += 17;
|
|
5503
|
+
}
|
|
5504
|
+
if (m.printed) {
|
|
5505
|
+
ctx.font = fontOf(400, 9.5);
|
|
5506
|
+
ctx.fillStyle = "#6b7280";
|
|
5507
|
+
const w = ctx.measureText(m.printed).width;
|
|
5508
|
+
ctx.fillText(m.printed, right - w, g.margin + h - 18 - 13);
|
|
5509
|
+
}
|
|
5510
|
+
line(ctx, g.margin, g.margin + h - 10, right, g.margin + h - 10, "#111827", 2);
|
|
5511
|
+
}
|
|
5512
|
+
function paintFooter(ctx, m, g, index, total) {
|
|
5513
|
+
const y = g.pageH - 7 * PX_PER_MM;
|
|
5514
|
+
ctx.font = fontOf(400, 8.5);
|
|
5515
|
+
ctx.fillStyle = "#6b7280";
|
|
5516
|
+
ctx.textBaseline = "middle";
|
|
5517
|
+
ctx.textAlign = "left";
|
|
5518
|
+
if (m.footer) ctx.fillText(m.footer, g.margin, y);
|
|
5519
|
+
if (m.pageNumbers) {
|
|
5520
|
+
const t = `${index + 1} / ${total}`;
|
|
5521
|
+
ctx.fillText(t, g.pageW - g.margin - ctx.measureText(t).width, y);
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
function renderPdf(m, deps = browserSurface, opts = {}) {
|
|
5525
|
+
const scale = typeof opts.scale === "number" && opts.scale > 0 ? Math.min(4, opts.scale) : 2;
|
|
5526
|
+
const quality = typeof opts.quality === "number" && opts.quality > 0 && opts.quality <= 1 ? opts.quality : 0.92;
|
|
5527
|
+
const probe = deps.surface(8, 8);
|
|
5528
|
+
if (!probe) return null;
|
|
5529
|
+
const { pages, g } = paginate(probe.ctx, m);
|
|
5530
|
+
const wPx = Math.round(g.pageW * scale), hPx = Math.round(g.pageH * scale);
|
|
5531
|
+
const images = [];
|
|
5532
|
+
pages.forEach((page, i) => {
|
|
5533
|
+
const s = deps.surface(wPx, hPx);
|
|
5534
|
+
if (!s) return;
|
|
5535
|
+
const ctx = s.ctx;
|
|
5536
|
+
ctx.setTransform(scale, 0, 0, scale, 0, 0);
|
|
5537
|
+
ctx.fillStyle = "#ffffff";
|
|
5538
|
+
ctx.fillRect(0, 0, g.pageW, g.pageH);
|
|
5539
|
+
if (page.title) paintTitle(ctx, m, g);
|
|
5540
|
+
for (const row of page.rows) paintRow(ctx, row, m, g);
|
|
5541
|
+
paintFooter(ctx, m, g, i, pages.length);
|
|
5542
|
+
images.push(s.jpeg(quality));
|
|
5543
|
+
});
|
|
5544
|
+
const [mw, mh] = PAPER_MM[m.paper] ?? PAPER_MM.A4;
|
|
5545
|
+
const [pw, ph] = m.orientation === "landscape" ? [mh, mw] : [mw, mh];
|
|
5546
|
+
return writePdf(images.map((jpeg) => ({ jpeg, width: wPx, height: hPx })), pw * PT_PER_MM, ph * PT_PER_MM, m.title);
|
|
5547
|
+
}
|
|
5548
|
+
function pdfString(s) {
|
|
5549
|
+
if (/^[\x20-\x7e]*$/.test(s)) return `(${s.replace(/[\\()]/g, (c) => `\\${c}`)})`;
|
|
5550
|
+
let hex2 = "FEFF";
|
|
5551
|
+
for (let i = 0; i < s.length; i++) hex2 += s.charCodeAt(i).toString(16).padStart(4, "0").toUpperCase();
|
|
5552
|
+
return `<${hex2}>`;
|
|
5553
|
+
}
|
|
5554
|
+
function writePdf(pages, ptW, ptH, title = "") {
|
|
5555
|
+
const enc = new TextEncoder();
|
|
5556
|
+
const parts = [];
|
|
5557
|
+
const offsets = [];
|
|
5558
|
+
let size = 0;
|
|
5559
|
+
const push = (b) => {
|
|
5560
|
+
const bytes = typeof b === "string" ? enc.encode(b) : b;
|
|
5561
|
+
parts.push(bytes);
|
|
5562
|
+
size += bytes.length;
|
|
5563
|
+
};
|
|
5564
|
+
const obj = (n2, body) => {
|
|
5565
|
+
offsets[n2] = size;
|
|
5566
|
+
push(`${n2} 0 obj
|
|
5567
|
+
`);
|
|
5568
|
+
if (typeof body === "string") push(body);
|
|
5569
|
+
else body();
|
|
5570
|
+
push("\nendobj\n");
|
|
5571
|
+
};
|
|
5572
|
+
push("%PDF-1.4\n");
|
|
5573
|
+
push(new Uint8Array([37, 226, 227, 207, 211, 10]));
|
|
5574
|
+
const n = pages.length;
|
|
5575
|
+
const pageIds = pages.map((_, i) => 4 + i * 3);
|
|
5576
|
+
const w = ptW.toFixed(2), h = ptH.toFixed(2);
|
|
5577
|
+
obj(1, "<< /Type /Catalog /Pages 2 0 R >>");
|
|
5578
|
+
obj(2, `<< /Type /Pages /Count ${n} /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] >>`);
|
|
5579
|
+
obj(3, `<< /Producer (FreeGrid)${title ? ` /Title ${pdfString(title)}` : ""} >>`);
|
|
5580
|
+
pages.forEach((p, i) => {
|
|
5581
|
+
const id = pageIds[i];
|
|
5582
|
+
const content = `q ${w} 0 0 ${h} 0 0 cm /Im0 Do Q`;
|
|
5583
|
+
obj(id, `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${w} ${h}] /Resources << /XObject << /Im0 ${id + 2} 0 R >> >> /Contents ${id + 1} 0 R >>`);
|
|
5584
|
+
obj(id + 1, `<< /Length ${content.length} >>
|
|
5585
|
+
stream
|
|
5586
|
+
${content}
|
|
5587
|
+
endstream`);
|
|
5588
|
+
obj(id + 2, () => {
|
|
5589
|
+
push(`<< /Type /XObject /Subtype /Image /Width ${p.width} /Height ${p.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${p.jpeg.length} >>
|
|
5590
|
+
stream
|
|
5591
|
+
`);
|
|
5592
|
+
push(p.jpeg);
|
|
5593
|
+
push("\nendstream");
|
|
5594
|
+
});
|
|
5595
|
+
});
|
|
5596
|
+
const count = 4 + n * 3;
|
|
5597
|
+
const xref = size;
|
|
5598
|
+
let table = `xref
|
|
5599
|
+
0 ${count}
|
|
5600
|
+
0000000000 65535 f
|
|
5601
|
+
`;
|
|
5602
|
+
for (let k = 1; k < count; k++) table += `${String(offsets[k] ?? 0).padStart(10, "0")} 00000 n
|
|
5603
|
+
`;
|
|
5604
|
+
push(`${table}trailer
|
|
5605
|
+
<< /Size ${count} /Root 1 0 R /Info 3 0 R >>
|
|
5606
|
+
startxref
|
|
5607
|
+
${xref}
|
|
5608
|
+
%%EOF
|
|
5609
|
+
`);
|
|
5610
|
+
const out = new Uint8Array(size);
|
|
5611
|
+
let at = 0;
|
|
5612
|
+
for (const part of parts) {
|
|
5613
|
+
out.set(part, at);
|
|
5614
|
+
at += part.length;
|
|
5615
|
+
}
|
|
5616
|
+
return out;
|
|
5617
|
+
}
|
|
5618
|
+
|
|
5619
|
+
// src/conditional.ts
|
|
5620
|
+
var NUMERIC = (v) => {
|
|
5621
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
5622
|
+
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
5623
|
+
if (v instanceof Date) return v.getTime();
|
|
5624
|
+
return null;
|
|
5625
|
+
};
|
|
5626
|
+
var STAT_TYPES = /* @__PURE__ */ new Set(["top", "bottom", "aboveAverage", "belowAverage", "duplicate", "unique", "dataBar", "colorScale", "iconSet"]);
|
|
5627
|
+
var ICON_SETS = {
|
|
5628
|
+
// high, middle, low
|
|
5629
|
+
arrows: [{ char: "\u25B2", color: "#16a34a", label: "up" }, { char: "\u25B6", color: "#d97706", label: "level" }, { char: "\u25BC", color: "#dc2626", label: "down" }],
|
|
5630
|
+
traffic: [{ char: "\u25CF", color: "#16a34a", label: "good" }, { char: "\u25CF", color: "#f59e0b", label: "fair" }, { char: "\u25CF", color: "#dc2626", label: "poor" }],
|
|
5631
|
+
signs: [{ char: "\u2714", color: "#16a34a", label: "good" }, { char: "!", color: "#d97706", label: "fair" }, { char: "\u2716", color: "#dc2626", label: "poor" }]
|
|
5632
|
+
};
|
|
5633
|
+
function hex(c) {
|
|
5634
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c.trim());
|
|
5635
|
+
if (!m) return null;
|
|
5636
|
+
const h = m[1].length === 3 ? [...m[1]].map((x) => x + x).join("") : m[1];
|
|
5637
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
5638
|
+
}
|
|
5639
|
+
function mixColor(a, b, t) {
|
|
5640
|
+
const x = hex(a), y = hex(b);
|
|
5641
|
+
if (!x || !y) return t < 0.5 ? a : b;
|
|
5642
|
+
const c = x.map((v, i) => Math.round(v + (y[i] - v) * Math.max(0, Math.min(1, t))));
|
|
5643
|
+
return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
5644
|
+
}
|
|
5645
|
+
function textOf(v) {
|
|
5646
|
+
return v == null ? "" : v instanceof Date ? v.toISOString() : String(v);
|
|
5647
|
+
}
|
|
5648
|
+
function valueMatches(rule, value) {
|
|
5649
|
+
const op = rule.op ?? "eq";
|
|
5650
|
+
if (op === "blank") return isBlank(value);
|
|
5651
|
+
if (op === "notBlank") return !isBlank(value);
|
|
5652
|
+
if (op === "contains" || op === "notContains" || op === "startsWith" || op === "endsWith") {
|
|
5653
|
+
const s2 = textOf(value).toLowerCase(), q = textOf(rule.value).toLowerCase();
|
|
5654
|
+
const hit = op === "startsWith" ? s2.startsWith(q) : op === "endsWith" ? s2.endsWith(q) : s2.includes(q);
|
|
5655
|
+
return op === "notContains" ? !hit : hit;
|
|
5656
|
+
}
|
|
5657
|
+
if (isBlank(value)) return op === "ne";
|
|
5658
|
+
const a = NUMERIC(value), b = NUMERIC(rule.value), c = NUMERIC(rule.valueTo);
|
|
5659
|
+
if (a !== null && b !== null) {
|
|
5660
|
+
switch (op) {
|
|
5661
|
+
case "eq":
|
|
5662
|
+
return a === b;
|
|
5663
|
+
case "ne":
|
|
5664
|
+
return a !== b;
|
|
5665
|
+
case "gt":
|
|
5666
|
+
return a > b;
|
|
5667
|
+
case "gte":
|
|
5668
|
+
return a >= b;
|
|
5669
|
+
case "lt":
|
|
5670
|
+
return a < b;
|
|
5671
|
+
case "lte":
|
|
5672
|
+
return a <= b;
|
|
5673
|
+
case "between":
|
|
5674
|
+
case "notBetween": {
|
|
5675
|
+
if (c === null) return false;
|
|
5676
|
+
const inside = a >= Math.min(b, c) && a <= Math.max(b, c);
|
|
5677
|
+
return op === "between" ? inside : !inside;
|
|
5678
|
+
}
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5681
|
+
const s = textOf(value).toLowerCase(), t = textOf(rule.value).toLowerCase(), u = textOf(rule.valueTo).toLowerCase();
|
|
5682
|
+
switch (op) {
|
|
5683
|
+
case "eq":
|
|
5684
|
+
return s === t;
|
|
5685
|
+
case "ne":
|
|
5686
|
+
return s !== t;
|
|
5687
|
+
case "gt":
|
|
5688
|
+
return s > t;
|
|
5689
|
+
case "gte":
|
|
5690
|
+
return s >= t;
|
|
5691
|
+
case "lt":
|
|
5692
|
+
return s < t;
|
|
5693
|
+
case "lte":
|
|
5694
|
+
return s <= t;
|
|
5695
|
+
case "between":
|
|
5696
|
+
return s >= t && s <= u;
|
|
5697
|
+
case "notBetween":
|
|
5698
|
+
return s < t || s > u;
|
|
5699
|
+
}
|
|
5700
|
+
return false;
|
|
5701
|
+
}
|
|
5702
|
+
var ConditionalFormatter = class {
|
|
5703
|
+
constructor(deps) {
|
|
5704
|
+
this.deps = deps;
|
|
5705
|
+
this.stats = /* @__PURE__ */ new Map();
|
|
5706
|
+
this.statsKey = [];
|
|
5707
|
+
this.rowCache = /* @__PURE__ */ new WeakMap();
|
|
5708
|
+
}
|
|
5709
|
+
/** True when any rule looks at other rows (an update to one row can restyle others). */
|
|
5710
|
+
dependsOnOtherRows() {
|
|
5711
|
+
return this.deps.rules().some((r) => r && (STAT_TYPES.has(r.type ?? "value") || r.type === "formula"));
|
|
5712
|
+
}
|
|
5713
|
+
fresh() {
|
|
5714
|
+
const key = [this.deps.rows(), this.deps.version(), this.deps.rules()];
|
|
5715
|
+
if (key.every((k, i) => k === this.statsKey[i])) return;
|
|
5716
|
+
this.statsKey = key;
|
|
5717
|
+
this.stats.clear();
|
|
5718
|
+
this.rowCache = /* @__PURE__ */ new WeakMap();
|
|
5719
|
+
}
|
|
5720
|
+
statsOf(colId) {
|
|
5721
|
+
let s = this.stats.get(colId);
|
|
5722
|
+
if (!s) {
|
|
5723
|
+
const nums = [];
|
|
5724
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5725
|
+
for (const row of this.deps.rows()) {
|
|
5726
|
+
const v = this.deps.value(colId, row);
|
|
5727
|
+
const n = NUMERIC(v);
|
|
5728
|
+
if (n !== null) nums.push(n);
|
|
5729
|
+
if (!isBlank(v)) {
|
|
5730
|
+
const k = setKey(v);
|
|
5731
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
nums.sort((a, b) => a - b);
|
|
5735
|
+
const sum = nums.reduce((a, b) => a + b, 0);
|
|
5736
|
+
s = { min: nums[0] ?? 0, max: nums[nums.length - 1] ?? 0, avg: nums.length ? sum / nums.length : 0, sorted: nums, counts };
|
|
5737
|
+
this.stats.set(colId, s);
|
|
5738
|
+
}
|
|
5739
|
+
return s;
|
|
5740
|
+
}
|
|
5741
|
+
test(rule, colId, value, row) {
|
|
5742
|
+
const type = rule.type ?? "value";
|
|
5743
|
+
switch (type) {
|
|
5744
|
+
case "value":
|
|
5745
|
+
return valueMatches(rule, value);
|
|
5746
|
+
case "formula": {
|
|
5747
|
+
if (typeof rule.formula !== "string") return false;
|
|
5748
|
+
const r = this.deps.formula(rule.formula.startsWith("=") ? rule.formula : `=${rule.formula}`, row);
|
|
5749
|
+
return r === true || typeof r === "number" && r !== 0;
|
|
5750
|
+
}
|
|
5751
|
+
case "top":
|
|
5752
|
+
case "bottom": {
|
|
5753
|
+
const n = NUMERIC(value);
|
|
5754
|
+
if (n === null) return false;
|
|
5755
|
+
const s = this.statsOf(colId);
|
|
5756
|
+
if (!s.sorted.length) return false;
|
|
5757
|
+
const k = Math.max(1, rule.percent ? Math.ceil(s.sorted.length * (rule.rank ?? 10) / 100) : Math.floor(rule.rank ?? 10));
|
|
5758
|
+
return type === "top" ? n >= s.sorted[Math.max(0, s.sorted.length - k)] : n <= s.sorted[Math.min(s.sorted.length - 1, k - 1)];
|
|
5759
|
+
}
|
|
5760
|
+
case "aboveAverage":
|
|
5761
|
+
case "belowAverage": {
|
|
5762
|
+
const n = NUMERIC(value);
|
|
5763
|
+
if (n === null) return false;
|
|
5764
|
+
const avg = this.statsOf(colId).avg;
|
|
5765
|
+
return type === "aboveAverage" ? n > avg : n < avg;
|
|
5766
|
+
}
|
|
5767
|
+
case "duplicate":
|
|
5768
|
+
case "unique": {
|
|
5769
|
+
if (isBlank(value)) return false;
|
|
5770
|
+
const c = this.statsOf(colId).counts.get(setKey(value)) ?? 0;
|
|
5771
|
+
return type === "duplicate" ? c > 1 : c === 1;
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
return false;
|
|
5775
|
+
}
|
|
5776
|
+
/** Style from rules with `row: true`, for every cell of the row. */
|
|
5777
|
+
rowStyle(row) {
|
|
5778
|
+
if (this.rowCache.has(row)) return this.rowCache.get(row);
|
|
5779
|
+
let style = null;
|
|
5780
|
+
for (const rule of this.deps.rules()) {
|
|
5781
|
+
if (!rule?.row || !Array.isArray(rule.columns) || !rule.columns.length) continue;
|
|
5782
|
+
const colId = rule.columns[0];
|
|
5783
|
+
if (this.test(rule, colId, this.deps.value(colId, row), row)) {
|
|
5784
|
+
style = { ...style ?? {}, ...rule.style };
|
|
5785
|
+
if (rule.stopIfTrue) break;
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
this.rowCache.set(row, style);
|
|
5789
|
+
return style;
|
|
5790
|
+
}
|
|
5791
|
+
/** Format of one cell; null when no rule applies. */
|
|
5792
|
+
cell(colId, row, value) {
|
|
5793
|
+
const rules = this.deps.rules();
|
|
5794
|
+
if (!rules.length) return null;
|
|
5795
|
+
this.fresh();
|
|
5796
|
+
let out = null;
|
|
5797
|
+
const rs = this.rowStyle(row);
|
|
5798
|
+
if (rs) out = { style: { ...rs } };
|
|
5799
|
+
for (const rule of rules) {
|
|
5800
|
+
if (!rule || rule.row || !Array.isArray(rule.columns) || !rule.columns.includes(colId)) continue;
|
|
5801
|
+
const type = rule.type ?? "value";
|
|
5802
|
+
if (type === "dataBar" || type === "colorScale" || type === "iconSet") {
|
|
5803
|
+
const n = NUMERIC(value);
|
|
5804
|
+
if (n === null) continue;
|
|
5805
|
+
const s = this.statsOf(colId);
|
|
5806
|
+
const lo = typeof rule.min === "number" ? rule.min : s.min;
|
|
5807
|
+
const hi = typeof rule.max === "number" ? rule.max : s.max;
|
|
5808
|
+
const t = hi === lo ? 1 : Math.max(0, Math.min(1, (n - lo) / (hi - lo)));
|
|
5809
|
+
out ?? (out = { style: {} });
|
|
5810
|
+
if (type === "dataBar") {
|
|
5811
|
+
const zero = lo < 0 && hi > 0 ? -lo / (hi - lo) : lo >= 0 ? 0 : 1;
|
|
5812
|
+
const at = hi === lo ? 1 : t;
|
|
5813
|
+
out.bar = { from: Math.min(zero, at), to: Math.max(zero, at), color: rule.color || "var(--fg-accent)" };
|
|
5814
|
+
} else if (type === "colorScale") {
|
|
5815
|
+
const cs = Array.isArray(rule.colors) && rule.colors.length >= 2 ? rule.colors : ["#f8696b", "#ffeb84", "#63be7b"];
|
|
5816
|
+
const bg = cs.length >= 3 ? t < 0.5 ? mixColor(cs[0], cs[1], t * 2) : mixColor(cs[1], cs[2], (t - 0.5) * 2) : mixColor(cs[0], cs[1], t);
|
|
5817
|
+
out.style = { ...out.style, background: bg };
|
|
5818
|
+
} else {
|
|
5819
|
+
const set = ICON_SETS[rule.icons ?? "arrows"] ?? ICON_SETS.arrows;
|
|
5820
|
+
out.icon = t >= 2 / 3 ? set[0] : t >= 1 / 3 ? set[1] : set[2];
|
|
5821
|
+
}
|
|
5822
|
+
continue;
|
|
5823
|
+
}
|
|
5824
|
+
if (this.test(rule, colId, value, row)) {
|
|
5825
|
+
out ?? (out = { style: {} });
|
|
5826
|
+
out.style = { ...out.style, ...rule.style };
|
|
5827
|
+
if (rule.stopIfTrue) break;
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
return out;
|
|
5831
|
+
}
|
|
5832
|
+
};
|
|
5833
|
+
function argbOf(c) {
|
|
5834
|
+
const x = c ? hex(c) : null;
|
|
5835
|
+
return x ? `FF${x.map((v) => v.toString(16).padStart(2, "0")).join("").toUpperCase()}` : void 0;
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5838
|
+
// src/rows/sort.ts
|
|
5839
|
+
function compareNonBlank(a, b, collator) {
|
|
5840
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
5841
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
5842
|
+
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
5843
|
+
return collator.compare(String(a), String(b));
|
|
5844
|
+
}
|
|
5845
|
+
function normalize(def, v) {
|
|
5846
|
+
if (isBlank(v)) return null;
|
|
5847
|
+
if (def.type === "number") {
|
|
5848
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
5849
|
+
return Number.isNaN(n) ? null : n;
|
|
5850
|
+
}
|
|
5851
|
+
if (def.type === "date") return toDate2(v)?.getTime() ?? null;
|
|
5852
|
+
return v;
|
|
5853
|
+
}
|
|
5854
|
+
function compareValues(a, b, collator) {
|
|
5855
|
+
const ba = isBlank(a), bb = isBlank(b);
|
|
5856
|
+
if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
|
|
5857
|
+
return compareNonBlank(a, b, collator);
|
|
5858
|
+
}
|
|
5859
|
+
function sortNodes(nodes, sort, resolve, collator) {
|
|
5860
|
+
const keys = sort.map((s) => ({ def: resolve(s.colId), mul: s.dir === "desc" ? -1 : 1 })).filter((k) => !!k.def);
|
|
5861
|
+
if (!keys.length) return nodes;
|
|
5862
|
+
const values = keys.map((k) => nodes.map((n) => {
|
|
5863
|
+
const v = getCellValue(k.def, n.data);
|
|
5864
|
+
return k.def.comparator ? v : normalize(k.def, v);
|
|
5865
|
+
}));
|
|
5866
|
+
const order = nodes.map((_, i) => i);
|
|
5867
|
+
order.sort((i, j) => {
|
|
5868
|
+
for (let k = 0; k < keys.length; k++) {
|
|
5869
|
+
const { def, mul } = keys[k];
|
|
5870
|
+
const a = values[k][i], b = values[k][j];
|
|
5871
|
+
let c;
|
|
5872
|
+
if (def.comparator) {
|
|
5873
|
+
try {
|
|
5874
|
+
c = (Number(def.comparator(a, b, nodes[i].data, nodes[j].data)) || 0) * mul;
|
|
5875
|
+
} catch {
|
|
5876
|
+
warn(`comparator of column "${def.id ?? def.field}" threw; treating values as equal`);
|
|
5877
|
+
c = 0;
|
|
5878
|
+
}
|
|
5879
|
+
} else {
|
|
5880
|
+
const ba = isBlank(a), bb = isBlank(b);
|
|
5881
|
+
c = ba || bb ? ba === bb ? 0 : ba ? 1 : -1 : compareNonBlank(a, b, collator) * mul;
|
|
5882
|
+
}
|
|
5883
|
+
if (c) return c;
|
|
5884
|
+
}
|
|
5885
|
+
return i - j;
|
|
5886
|
+
});
|
|
5887
|
+
return order.map((i) => nodes[i]);
|
|
5888
|
+
}
|
|
5889
|
+
|
|
4680
5890
|
// src/export/csv.ts
|
|
4681
5891
|
var FORMULA = /^[=+\-@\t\r]/;
|
|
4682
5892
|
function toCsv(rows, sep = ",") {
|
|
@@ -4689,8 +5899,8 @@ function toCsv(rows, sep = ",") {
|
|
|
4689
5899
|
return rows.map((r) => r.map(cell).join(sep)).join("\r\n");
|
|
4690
5900
|
}
|
|
4691
5901
|
function detectSeparator(text2) {
|
|
4692
|
-
const
|
|
4693
|
-
const count = (ch) =>
|
|
5902
|
+
const line2 = text2.slice(0, text2.search(/\r?\n|$/));
|
|
5903
|
+
const count = (ch) => line2.split(ch).length - 1;
|
|
4694
5904
|
const tabs = count(" "), semis = count(";"), commas = count(",");
|
|
4695
5905
|
if (tabs && tabs >= commas && tabs >= semis) return " ";
|
|
4696
5906
|
if (semis > commas) return ";";
|
|
@@ -4847,12 +6057,12 @@ function colName(i) {
|
|
|
4847
6057
|
return s;
|
|
4848
6058
|
}
|
|
4849
6059
|
var EPOCH = Date.UTC(1899, 11, 30);
|
|
4850
|
-
var
|
|
6060
|
+
var DAY3 = 864e5;
|
|
4851
6061
|
function toSerial(d) {
|
|
4852
|
-
return (Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()) - EPOCH) /
|
|
6062
|
+
return (Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()) - EPOCH) / DAY3;
|
|
4853
6063
|
}
|
|
4854
6064
|
function fromSerial(n) {
|
|
4855
|
-
const u = new Date(Math.round(n *
|
|
6065
|
+
const u = new Date(Math.round(n * DAY3) + EPOCH);
|
|
4856
6066
|
return new Date(u.getUTCFullYear(), u.getUTCMonth(), u.getUTCDate(), u.getUTCHours(), u.getUTCMinutes(), u.getUTCSeconds());
|
|
4857
6067
|
}
|
|
4858
6068
|
var BAD_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g;
|
|
@@ -4884,7 +6094,7 @@ function writeXlsx(sheets) {
|
|
|
4884
6094
|
];
|
|
4885
6095
|
const fillIndex = /* @__PURE__ */ new Map([["FFF2F4F7", 2]]);
|
|
4886
6096
|
const argb = (c) => c && /^[0-9a-f]{8}$/i.test(c) ? c.toUpperCase() : c && /^[0-9a-f]{6}$/i.test(c) ? `FF${c.toUpperCase()}` : "";
|
|
4887
|
-
const
|
|
6097
|
+
const fontOf2 = (bold, color) => {
|
|
4888
6098
|
const key = `${bold ? 1 : 0}|${color}`;
|
|
4889
6099
|
let i = fontIndex.get(key);
|
|
4890
6100
|
if (i === void 0) {
|
|
@@ -4918,7 +6128,7 @@ function writeXlsx(sheets) {
|
|
|
4918
6128
|
customFmts.push(fmt);
|
|
4919
6129
|
fmtIds.set(fmt, id);
|
|
4920
6130
|
}
|
|
4921
|
-
const fontId =
|
|
6131
|
+
const fontId = fontOf2(bold, color);
|
|
4922
6132
|
const fillId = fillOf(fill);
|
|
4923
6133
|
const attrs = `numFmtId="${id}" fontId="${fontId}" fillId="${fillId}" borderId="0" xfId="0"` + (id ? ' applyNumberFormat="1"' : "") + (fontId ? ' applyFont="1"' : "") + (fillId ? ' applyFill="1"' : "") + (align ? ' applyAlignment="1"' : "");
|
|
4924
6134
|
i = xfs.length;
|
|
@@ -4971,9 +6181,13 @@ function writeXlsx(sheets) {
|
|
|
4971
6181
|
},
|
|
4972
6182
|
{
|
|
4973
6183
|
name: "xl/workbook.xml",
|
|
4974
|
-
data: `${XML}<workbook xmlns="${NS}" xmlns:r="${REL_NS}"><sheets>` + names.map((n, i) => `<sheet name="${esc(n)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join("") + "</sheets>" + (sheets.some((s) => s.autoFilter && s.freezeRows) ? `<definedNames>${sheets.map((s, i) => {
|
|
6184
|
+
data: `${XML}<workbook xmlns="${NS}" xmlns:r="${REL_NS}"><sheets>` + names.map((n, i) => `<sheet name="${esc(n)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join("") + "</sheets>" + (sheets.some((s) => s.autoFilter && s.freezeRows || s.printTitleRows) ? `<definedNames>${sheets.map((s, i) => {
|
|
4975
6185
|
const w = Math.max(0, ...s.rows.map((r) => r.length));
|
|
4976
|
-
|
|
6186
|
+
const ref = `'${esc(names[i].replace(/'/g, "''"))}'`;
|
|
6187
|
+
const filter = s.autoFilter && s.freezeRows && w ? `<definedName name="_xlnm._FilterDatabase" localSheetId="${i}" hidden="1">${ref}!$A$${s.freezeRows}:$${colName(w - 1)}$${s.rows.length}</definedName>` : "";
|
|
6188
|
+
const n = Math.min(s.printTitleRows ?? 0, s.rows.length);
|
|
6189
|
+
const titles = n > 0 && Number.isInteger(n) ? `<definedName name="_xlnm.Print_Titles" localSheetId="${i}">${ref}!$1:$${n}</definedName>` : "";
|
|
6190
|
+
return filter + titles;
|
|
4977
6191
|
}).join("")}</definedNames>` : "") + "</workbook>"
|
|
4978
6192
|
},
|
|
4979
6193
|
{
|
|
@@ -5017,11 +6231,11 @@ async function readXlsx(input) {
|
|
|
5017
6231
|
const shared = parseXml(files.get("xl/sharedStrings.xml"));
|
|
5018
6232
|
const strings = shared ? tags(shared, "si").map((si) => tags(si, "t").map((t) => t.textContent ?? "").join("")) : [];
|
|
5019
6233
|
const stylesDoc = parseXml(files.get("xl/styles.xml"));
|
|
5020
|
-
const
|
|
6234
|
+
const custom2 = new Map(stylesDoc ? tags(stylesDoc, "numFmt").map((f) => [Number(f.getAttribute("numFmtId")), f.getAttribute("formatCode") ?? ""]) : []);
|
|
5021
6235
|
const cellXfs = stylesDoc ? tags(stylesDoc, "cellXfs")[0] : void 0;
|
|
5022
6236
|
const dateStyle = (cellXfs ? [...cellXfs.children].filter((x) => x.localName === "xf") : []).map((xf) => {
|
|
5023
6237
|
const id = Number(xf.getAttribute("numFmtId") ?? 0);
|
|
5024
|
-
return BUILTIN_DATE_FMTS.has(id) ||
|
|
6238
|
+
return BUILTIN_DATE_FMTS.has(id) || custom2.has(id) && isDateFormat(custom2.get(id));
|
|
5025
6239
|
});
|
|
5026
6240
|
const sheets = [];
|
|
5027
6241
|
for (const s of tags(workbook, "sheet")) {
|
|
@@ -5087,10 +6301,70 @@ var header = (def, id) => def.header ?? def.field ?? id;
|
|
|
5087
6301
|
var ALIGNS = ["left", "center", "right"];
|
|
5088
6302
|
var PLAIN_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
5089
6303
|
var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
5090
|
-
|
|
6304
|
+
var PAPERS = ["A4", "A3", "A5", "B4", "B5", "letter", "legal"];
|
|
6305
|
+
var PORTRAIT_WIDTH = 760;
|
|
6306
|
+
var cssStr = (s) => `"${s.replace(/[\\"<\n\r]/g, (c) => c === "\n" ? "\\A " : c === "\r" ? "" : `\\${c.charCodeAt(0).toString(16)} `)}"`;
|
|
6307
|
+
var hexOf = (argb) => argb ? `#${argb.slice(2)}` : "";
|
|
6308
|
+
function printLines(grid, rows, groupBy) {
|
|
6309
|
+
const out = [];
|
|
6310
|
+
let n = 0;
|
|
6311
|
+
const sort = grid.store.state.sort;
|
|
6312
|
+
const collator = grid.collator();
|
|
6313
|
+
const walk = (list, level) => {
|
|
6314
|
+
if (level >= groupBy.length) {
|
|
6315
|
+
for (const row of list) out.push({ kind: "row", row, n: ++n });
|
|
6316
|
+
return;
|
|
6317
|
+
}
|
|
6318
|
+
const colId = groupBy[level];
|
|
6319
|
+
const def = grid.columns.getNode(colId)?.def;
|
|
6320
|
+
if (!def) return walk(list, level + 1);
|
|
6321
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
6322
|
+
for (const r of list) {
|
|
6323
|
+
const v = getCellValue(def, r);
|
|
6324
|
+
const k = isBlank(v) ? "" : setKey(v);
|
|
6325
|
+
let b = buckets.get(k);
|
|
6326
|
+
if (!b) buckets.set(k, b = { value: v, rows: [] });
|
|
6327
|
+
b.rows.push(r);
|
|
6328
|
+
}
|
|
6329
|
+
const dir = sort.find((x) => x.colId === colId)?.dir === "desc" ? -1 : 1;
|
|
6330
|
+
for (const b of [...buckets.values()].sort((a, c) => compareValues(a.value, c.value, collator) * dir)) {
|
|
6331
|
+
out.push({ kind: "group", colId, value: b.value, rows: b.rows, level });
|
|
6332
|
+
walk(b.rows, level + 1);
|
|
6333
|
+
}
|
|
6334
|
+
};
|
|
6335
|
+
walk(rows, 0);
|
|
6336
|
+
return out;
|
|
6337
|
+
}
|
|
6338
|
+
var PRINT_CSS = `
|
|
6339
|
+
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
6340
|
+
body { font: 10.5px/1.45 system-ui, 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif; color: #111827; margin: 0; }
|
|
6341
|
+
header { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; margin: 0 0 10px; padding-bottom: 8px; border-bottom: 2px solid #111827; }
|
|
6342
|
+
h1 { font-size: 17px; margin: 0; letter-spacing: -.01em; }
|
|
6343
|
+
.sub { margin: 3px 0 0; color: #4b5563; font-size: 11px; }
|
|
6344
|
+
.printed { margin: 0; color: #6b7280; font-size: 9.5px; white-space: nowrap; }
|
|
6345
|
+
table { border-collapse: collapse; width: 100%; }
|
|
6346
|
+
table.fit { table-layout: fixed; }
|
|
6347
|
+
th, td { padding: 4px 7px; border-bottom: 1px solid #e5e7eb; vertical-align: top; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
|
6348
|
+
th { background: #f3f4f6; color: #374151; font-weight: 600; text-align: left; vertical-align: bottom; border-bottom: 1.5px solid #9ca3af; border-left: 1px solid #e5e7eb; }
|
|
6349
|
+
th.span { text-align: center; }
|
|
6350
|
+
th:first-child { border-left: 0; }
|
|
6351
|
+
thead { display: table-header-group; }
|
|
6352
|
+
tr { break-inside: avoid; page-break-inside: avoid; }
|
|
6353
|
+
.num { text-align: right; color: #9ca3af; }
|
|
6354
|
+
tr.group td { background: #eef2f7; font-weight: 600; border-top: 1px solid #cbd5e1; break-after: avoid; page-break-after: avoid; }
|
|
6355
|
+
tr.group.level-0 td { background: #e2e8f0; }
|
|
6356
|
+
tr.title td { border: 0; padding: 0 0 4px; background: none; }
|
|
6357
|
+
tr.total td { font-weight: 700; background: #f3f4f6; border-top: 1.5px solid #111827; border-bottom: 0; }
|
|
6358
|
+
`;
|
|
6359
|
+
var colorOk = (c) => typeof c === "string" && /^(#[0-9a-f]{3,8}|[a-z]+|rgba?\([\d\s.,%]+\)|hsla?\([\d\s.,%deg]+\))$/i.test(c.trim()) ? c.trim() : void 0;
|
|
6360
|
+
function printModel(grid, opts = {}) {
|
|
5091
6361
|
const cols = exportColumns(grid, opts.columns);
|
|
5092
|
-
const
|
|
6362
|
+
const loc = grid.store.state.locale;
|
|
6363
|
+
const code = loc.code;
|
|
5093
6364
|
const fmts = cols.map((c) => getFormatter(c.def, code));
|
|
6365
|
+
const nf = new Intl.NumberFormat(code);
|
|
6366
|
+
const numbered = opts.rowNumbers ?? !!grid.options.rowNumbers;
|
|
6367
|
+
const align = (def) => ALIGNS.includes(def.align) ? def.align : def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left";
|
|
5094
6368
|
const chains = cols.map((c) => {
|
|
5095
6369
|
const chain = [];
|
|
5096
6370
|
for (let n = c.node; n; n = n.parent) chain.unshift(n);
|
|
@@ -5098,41 +6372,192 @@ function buildPrintHtml(grid, opts = {}) {
|
|
|
5098
6372
|
});
|
|
5099
6373
|
const depth = Math.max(1, ...chains.map((ch) => ch.length));
|
|
5100
6374
|
const head = [];
|
|
5101
|
-
|
|
5102
|
-
let
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
6375
|
+
if (opts.headers !== false) {
|
|
6376
|
+
for (let level = 0; level < depth; level++) {
|
|
6377
|
+
const tr = level === 0 && numbered ? [{ text: "#", align: "right", num: true, rowspan: depth }] : [];
|
|
6378
|
+
for (let ci = 0; ci < cols.length; ci++) {
|
|
6379
|
+
const chain = chains[ci];
|
|
6380
|
+
const leaf = chain.length - 1;
|
|
6381
|
+
if (level < leaf) {
|
|
6382
|
+
const g = chain[level];
|
|
6383
|
+
if (ci > 0 && chains[ci - 1][level] === g) continue;
|
|
6384
|
+
let span = 1;
|
|
6385
|
+
while (ci + span < cols.length && chains[ci + span][level] === g) span++;
|
|
6386
|
+
tr.push({ text: header(g.def, g.id), align: span > 1 ? "center" : "left", span, group: true });
|
|
6387
|
+
} else if (level === leaf) {
|
|
6388
|
+
tr.push({ text: header(cols[ci].def, cols[ci].id), align: align(cols[ci].def), span: 1, rowspan: depth - level });
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
head.push(tr);
|
|
6392
|
+
}
|
|
5117
6393
|
}
|
|
5118
|
-
const
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
6394
|
+
const aggText = (c, i, v, agg) => {
|
|
6395
|
+
if (v == null || v === "") return "";
|
|
6396
|
+
if (agg === "count") return nf.format(v);
|
|
6397
|
+
const type = c.def.type ?? (typeof v === "number" ? "number" : v instanceof Date ? "date" : void 0);
|
|
6398
|
+
return type === c.def.type ? fmts[i](v) : getFormatter({ ...c.def, type }, code)(v);
|
|
6399
|
+
};
|
|
6400
|
+
const labelled = (kind, label, values, level = 0) => {
|
|
6401
|
+
let lead = 0;
|
|
6402
|
+
while (lead < cols.length - 1 && !values[lead]) lead++;
|
|
6403
|
+
lead = Math.max(1, lead);
|
|
6404
|
+
const cells = numbered ? [{ text: "", align: "right", num: true }] : [];
|
|
6405
|
+
cells.push({ text: label, align: "left", span: lead, indent: level });
|
|
6406
|
+
for (let i = lead; i < cols.length; i++) cells.push({ text: values[i] ?? "", align: align(cols[i].def) });
|
|
6407
|
+
return { kind, level, cells };
|
|
6408
|
+
};
|
|
6409
|
+
const rows = exportRows(grid, opts.rows);
|
|
6410
|
+
const groupBy = opts.groups !== false && !grid.rowModel.server && !grid.options.treeData ? grid.getGroupBy() : [];
|
|
6411
|
+
const probe = opts.styles !== false && cols.some((c) => c.def.cellClass) ? classStyles(grid) : null;
|
|
6412
|
+
const body = [];
|
|
6413
|
+
try {
|
|
6414
|
+
for (const line2 of printLines(grid, rows, groupBy)) {
|
|
6415
|
+
if (line2.kind === "group") {
|
|
6416
|
+
const def = grid.columns.getNode(line2.colId)?.def;
|
|
6417
|
+
const value = isBlank(line2.value) ? loc.blanks : def ? getFormatter(def, code)(line2.value) : String(line2.value);
|
|
6418
|
+
const label = `${def ? header(def, line2.colId) : line2.colId}: ${value} (${nf.format(line2.rows.length)})`;
|
|
6419
|
+
const values = cols.map((c, i) => {
|
|
6420
|
+
if (!c.def.aggFunc || groupBy.includes(c.id)) return void 0;
|
|
6421
|
+
try {
|
|
6422
|
+
return aggText(c, i, aggregate(c.def.aggFunc, line2.rows.map((r2) => getCellValue(c.def, r2)), line2.rows), c.def.aggFunc) || void 0;
|
|
6423
|
+
} catch {
|
|
6424
|
+
return void 0;
|
|
6425
|
+
}
|
|
6426
|
+
});
|
|
6427
|
+
body.push(labelled("group", label, values, line2.level));
|
|
6428
|
+
continue;
|
|
6429
|
+
}
|
|
6430
|
+
const r = line2.row;
|
|
6431
|
+
const cells = numbered ? [{ text: String(line2.n), align: "right", num: true }] : [];
|
|
6432
|
+
cols.forEach((c, i) => {
|
|
6433
|
+
const v = getCellValue(c.def, r);
|
|
6434
|
+
const text2 = isBlank(v) ? "" : fmts[i](v);
|
|
6435
|
+
const cell = { text: text2, align: align(c.def) };
|
|
6436
|
+
if (probe && c.def.cellClass) {
|
|
6437
|
+
let cls;
|
|
6438
|
+
try {
|
|
6439
|
+
cls = typeof c.def.cellClass === "function" ? c.def.cellClass({ value: v, formatted: text2, row: r, rowIndex: line2.n - 1, column: c.def }) : c.def.cellClass;
|
|
6440
|
+
} catch {
|
|
6441
|
+
cls = void 0;
|
|
6442
|
+
}
|
|
6443
|
+
if (cls) {
|
|
6444
|
+
const st = probe.of(cls);
|
|
6445
|
+
if (st.color) cell.color = hexOf(st.color);
|
|
6446
|
+
if (st.fill) cell.fill = hexOf(st.fill);
|
|
6447
|
+
if (st.bold) cell.bold = true;
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
const cf = opts.styles !== false ? grid.cellFormat(r, c.id, v) : null;
|
|
6451
|
+
if (cf) {
|
|
6452
|
+
const s = cf.style;
|
|
6453
|
+
if (colorOk(s.color)) cell.color = colorOk(s.color);
|
|
6454
|
+
if (colorOk(s.background)) cell.fill = colorOk(s.background);
|
|
6455
|
+
if (s.bold) cell.bold = true;
|
|
6456
|
+
if (s.italic) cell.italic = true;
|
|
6457
|
+
if (s.strike) cell.strike = true;
|
|
6458
|
+
if (cf.bar) cell.bar = { from: cf.bar.from, to: cf.bar.to, color: /^#[0-9a-f]{3,8}$/i.test(cf.bar.color) ? cf.bar.color : "#93c5fd" };
|
|
6459
|
+
if (cf.icon) cell.icon = { char: cf.icon.char, color: cf.icon.color };
|
|
6460
|
+
}
|
|
6461
|
+
cells.push(cell);
|
|
6462
|
+
});
|
|
6463
|
+
body.push({ kind: "row", cells });
|
|
6464
|
+
}
|
|
6465
|
+
} finally {
|
|
6466
|
+
probe?.dispose();
|
|
6467
|
+
}
|
|
6468
|
+
const foot = [];
|
|
6469
|
+
const summary = opts.summary !== false ? grid.summary() : null;
|
|
6470
|
+
if (summary) {
|
|
6471
|
+
summary.opts.rows.forEach((def, ri) => {
|
|
6472
|
+
const values = cols.map((c, i) => {
|
|
6473
|
+
const spec = def.columns?.[c.id];
|
|
6474
|
+
return spec === void 0 ? void 0 : aggText(c, i, summary.values[ri]?.[c.id], cellSpec(spec).agg) || void 0;
|
|
6475
|
+
});
|
|
6476
|
+
foot.push(labelled("total", def.label ?? "", values));
|
|
6477
|
+
});
|
|
6478
|
+
}
|
|
6479
|
+
const widths = cols.map((c) => Math.max(1, c.width));
|
|
6480
|
+
const sum = widths.reduce((a, b) => a + b, 0);
|
|
6481
|
+
const paper = PAPERS.includes(opts.paper) ? opts.paper : "A4";
|
|
6482
|
+
const orientation = opts.orientation === "portrait" || opts.orientation === "landscape" ? opts.orientation : sum + (numbered ? 40 : 0) > PORTRAIT_WIDTH ? "landscape" : "portrait";
|
|
6483
|
+
const printed = opts.printedAt !== false ? new Intl.DateTimeFormat(code, { dateStyle: "medium", timeStyle: "short" }).format(/* @__PURE__ */ new Date()) : "";
|
|
6484
|
+
const repeatRows = Number.isInteger(opts.repeatRows) && opts.repeatRows > 0 ? Math.min(opts.repeatRows, body.length) : 0;
|
|
6485
|
+
return {
|
|
6486
|
+
lang: code,
|
|
6487
|
+
title: opts.title ?? "",
|
|
6488
|
+
subtitle: opts.subtitle ?? "",
|
|
6489
|
+
printed,
|
|
6490
|
+
footer: opts.footer ?? "",
|
|
6491
|
+
paper,
|
|
6492
|
+
orientation,
|
|
6493
|
+
fit: opts.fitToWidth !== false,
|
|
6494
|
+
numbered,
|
|
6495
|
+
widths: numbered ? [40, ...widths] : widths,
|
|
6496
|
+
head,
|
|
6497
|
+
body,
|
|
6498
|
+
foot,
|
|
6499
|
+
striped: opts.striped !== false,
|
|
6500
|
+
pageNumbers: opts.pageNumbers !== false,
|
|
6501
|
+
repeatHeader: opts.repeatHeader !== false,
|
|
6502
|
+
repeatTitle: !!opts.repeatTitle,
|
|
6503
|
+
repeatRows
|
|
6504
|
+
};
|
|
6505
|
+
}
|
|
6506
|
+
function cellCss(c, head = false) {
|
|
6507
|
+
let s = head ? c.align === "left" || c.group ? "" : `text-align:${c.align}` : `text-align:${c.align}`;
|
|
6508
|
+
const add = (x) => {
|
|
6509
|
+
s += s ? `;${x}` : x;
|
|
6510
|
+
};
|
|
6511
|
+
if (c.indent) add(`padding-left:${7 + c.indent * 14}px`);
|
|
6512
|
+
if (c.color) add(`color:${c.color}`);
|
|
6513
|
+
if (c.fill) add(`background:${c.fill}`);
|
|
6514
|
+
if (c.bold) add("font-weight:600");
|
|
6515
|
+
if (c.italic) add("font-style:italic");
|
|
6516
|
+
if (c.strike) add("text-decoration:line-through");
|
|
6517
|
+
if (c.bar) {
|
|
6518
|
+
const from = (c.bar.from * 100).toFixed(1), to = (c.bar.to * 100).toFixed(1);
|
|
6519
|
+
add(`background-image:linear-gradient(to right,transparent ${from}%,${c.bar.color}66 ${from}%,${c.bar.color}66 ${to}%,transparent ${to}%)`);
|
|
6520
|
+
}
|
|
6521
|
+
return s;
|
|
6522
|
+
}
|
|
6523
|
+
function cellHtml(c, tag) {
|
|
6524
|
+
const attrs = [
|
|
6525
|
+
c.num ? ' class="num"' : c.group && (c.span ?? 1) > 1 ? ' class="span"' : "",
|
|
6526
|
+
c.span !== void 0 && (tag === "th" || c.span > 1) ? ` colspan="${c.span}"` : "",
|
|
6527
|
+
c.rowspan !== void 0 && tag === "th" ? ` rowspan="${c.rowspan}"` : ""
|
|
6528
|
+
].join("");
|
|
6529
|
+
if (c.num) return `<${tag}${attrs}>${escHtml(c.text)}</${tag}>`;
|
|
6530
|
+
const css = cellCss(c, tag === "th");
|
|
6531
|
+
const icon = c.icon ? `<span style="color:${escHtml(c.icon.color)};margin-right:5px">${escHtml(c.icon.char)}</span>` : "";
|
|
6532
|
+
return `<${tag}${attrs}${css ? ` style="${escHtml(css)}"` : ""}>${icon}${escHtml(c.text)}</${tag}>`;
|
|
6533
|
+
}
|
|
6534
|
+
var lineHtml = (l) => `<tr${l.kind === "group" ? ` class="group level-${Math.min(l.level ?? 0, 5)}"` : l.kind === "total" ? ' class="total"' : ""}>${l.cells.map((c) => cellHtml(c, "td")).join("")}</tr>`;
|
|
6535
|
+
function buildPrintHtml(grid, opts = {}) {
|
|
6536
|
+
const m = printModel(grid, opts);
|
|
6537
|
+
const sum = m.widths.slice(m.numbered ? 1 : 0).reduce((a, b) => a + b, 0);
|
|
6538
|
+
const colgroup = m.fit ? `<colgroup>${m.numbered ? '<col style="width:40px">' : ""}${m.widths.slice(m.numbered ? 1 : 0).map((w) => `<col style="width:${Math.round(w / sum * 1e3) / 10}%">`).join("")}</colgroup>` : "";
|
|
6539
|
+
const title = escHtml(m.title || (typeof document !== "undefined" ? document.title : "") || "");
|
|
6540
|
+
const heading = m.title || m.subtitle ? `<div>${m.title ? `<h1>${title}</h1>` : ""}${m.subtitle ? `<p class="sub">${escHtml(m.subtitle)}</p>` : ""}</div>` : "<div></div>";
|
|
6541
|
+
const top = m.title || m.subtitle || m.printed ? `<header>${heading}${m.printed ? `<p class="printed">${escHtml(m.printed)}</p>` : ""}</header>` : "";
|
|
6542
|
+
const small = "font: 8.5px system-ui, sans-serif; color: #6b7280;";
|
|
6543
|
+
const page = [
|
|
6544
|
+
`size: ${m.paper} ${m.orientation}; margin: 12mm 12mm 14mm;`,
|
|
6545
|
+
m.footer ? `@bottom-left { content: ${cssStr(m.footer)}; ${small} }` : "",
|
|
6546
|
+
m.pageNumbers ? `@bottom-right { content: counter(page) " / " counter(pages); ${small} }` : ""
|
|
6547
|
+
].join(" ");
|
|
6548
|
+
const stripes = m.striped ? "tbody tr:not(.group):nth-child(even) td { background: #f9fafb; }" : "";
|
|
6549
|
+
const headRows = m.head.map((tr) => `<tr>${tr.map((c) => cellHtml(c, "th")).join("")}</tr>`).join("");
|
|
6550
|
+
const repeated = m.body.slice(0, m.repeatRows).map(lineHtml).join("");
|
|
6551
|
+
const span = Math.max(1, m.widths.length);
|
|
6552
|
+
const titleRow = m.repeatTitle && top ? `<tr class="title"><td colspan="${span}">${top}</td></tr>` : "";
|
|
6553
|
+
const thead = m.repeatHeader ? `${titleRow}${headRows}${repeated}` : `${titleRow}${repeated}`;
|
|
6554
|
+
const body = (m.repeatHeader ? "" : headRows) + m.body.slice(m.repeatRows).map(lineHtml).join("");
|
|
6555
|
+
const foot = m.foot.map(lineHtml).join("");
|
|
6556
|
+
return `<!doctype html><html lang="${escHtml(m.lang)}"><head><meta charset="utf-8"><title>${title}</title><style>
|
|
6557
|
+
@page { ${page} }${PRINT_CSS}${stripes}
|
|
6558
|
+
</style></head><body>${m.repeatTitle ? "" : top}<table${m.fit ? ' class="fit"' : ""}>${colgroup}<thead>${thead}</thead><tbody>${body}</tbody>${foot ? `<tfoot>${foot}</tfoot>` : ""}</table></body></html>`;
|
|
6559
|
+
}
|
|
6560
|
+
function printHtml(html) {
|
|
5136
6561
|
if (typeof document === "undefined") return;
|
|
5137
6562
|
const frame = document.createElement("iframe");
|
|
5138
6563
|
frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0";
|
|
@@ -5172,6 +6597,22 @@ function buildCsv(grid, opts = {}) {
|
|
|
5172
6597
|
if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
|
|
5173
6598
|
return toCsv(matrix, sep);
|
|
5174
6599
|
}
|
|
6600
|
+
function buildJson(grid, opts = {}) {
|
|
6601
|
+
const cols = exportColumns(grid, opts.columns);
|
|
6602
|
+
const code = grid.store.state.locale.code;
|
|
6603
|
+
const fmts = opts.formatted ? cols.map((c) => getFormatter(c.def, code)) : null;
|
|
6604
|
+
const keys = cols.map((c) => opts.keys === "header" ? header(c.def, c.id) : c.id);
|
|
6605
|
+
const rows = exportRows(grid, opts.rows).map((r) => {
|
|
6606
|
+
const o = {};
|
|
6607
|
+
cols.forEach((c, i) => {
|
|
6608
|
+
const v = getCellValue(c.def, r);
|
|
6609
|
+
o[keys[i]] = fmts ? isBlank(v) ? "" : fmts[i](v) : v === void 0 ? null : v;
|
|
6610
|
+
});
|
|
6611
|
+
return o;
|
|
6612
|
+
});
|
|
6613
|
+
const space = typeof opts.space === "number" && opts.space >= 0 ? Math.min(10, opts.space) : 2;
|
|
6614
|
+
return JSON.stringify(rows, null, space);
|
|
6615
|
+
}
|
|
5175
6616
|
function excelFormat(def) {
|
|
5176
6617
|
const f = def.format;
|
|
5177
6618
|
if (def.type === "date") {
|
|
@@ -5194,9 +6635,9 @@ function excelFormat(def) {
|
|
|
5194
6635
|
return `${o.useGrouping === false ? "0" : "#,##0"}${decimals}`;
|
|
5195
6636
|
}
|
|
5196
6637
|
function toArgb(css) {
|
|
5197
|
-
const
|
|
5198
|
-
if (
|
|
5199
|
-
const h =
|
|
6638
|
+
const hex2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(css.trim());
|
|
6639
|
+
if (hex2) {
|
|
6640
|
+
const h = hex2[1].length === 3 ? [...hex2[1]].map((c) => c + c).join("") : hex2[1];
|
|
5200
6641
|
return `FF${h.toUpperCase()}`;
|
|
5201
6642
|
}
|
|
5202
6643
|
const m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?\s*\)$/i.exec(css.trim());
|
|
@@ -5295,7 +6736,7 @@ function buildXlsxSheet(grid, opts = {}) {
|
|
|
5295
6736
|
return Number.isFinite(n) ? { v: n, fmt } : String(v);
|
|
5296
6737
|
}
|
|
5297
6738
|
case "date": {
|
|
5298
|
-
const d =
|
|
6739
|
+
const d = toDate2(v);
|
|
5299
6740
|
return d ? { v: d, fmt } : String(v);
|
|
5300
6741
|
}
|
|
5301
6742
|
case "boolean":
|
|
@@ -5320,6 +6761,13 @@ function buildXlsxSheet(grid, opts = {}) {
|
|
|
5320
6761
|
}
|
|
5321
6762
|
if (cls) style = { ...style, ...probe.of(cls) };
|
|
5322
6763
|
}
|
|
6764
|
+
const cf = grid.cellFormat(r, c.id, v);
|
|
6765
|
+
if (cf) {
|
|
6766
|
+
const color = argbOf(cf.style.color), fill = argbOf(cf.style.background);
|
|
6767
|
+
if (color) style.color = color;
|
|
6768
|
+
if (fill) style.fill = fill;
|
|
6769
|
+
if (cf.style.bold) style.bold = true;
|
|
6770
|
+
}
|
|
5323
6771
|
if (!Object.keys(style).length) return cell;
|
|
5324
6772
|
return cell !== null && typeof cell === "object" && !(cell instanceof Date) ? { ...cell, ...style } : { v: cell, ...style };
|
|
5325
6773
|
}));
|
|
@@ -5353,6 +6801,8 @@ function buildXlsxSheet(grid, opts = {}) {
|
|
|
5353
6801
|
widths: cols.map((c) => Math.round(c.width / 7)),
|
|
5354
6802
|
merges,
|
|
5355
6803
|
freezeRows: headRows,
|
|
6804
|
+
// header rows (and repeatRows data rows) print on every page
|
|
6805
|
+
printTitleRows: opts.repeatHeader === false ? 0 : headRows + (Number.isInteger(opts.repeatRows) && opts.repeatRows > 0 ? opts.repeatRows : 0),
|
|
5356
6806
|
autoFilter: headRows > 0
|
|
5357
6807
|
};
|
|
5358
6808
|
}
|
|
@@ -5421,13 +6871,13 @@ async function importInto(grid, file, opts = {}) {
|
|
|
5421
6871
|
const sample = grid.store.state.data[0];
|
|
5422
6872
|
let skipped = 0;
|
|
5423
6873
|
const rows = [];
|
|
5424
|
-
for (const
|
|
5425
|
-
if (!
|
|
6874
|
+
for (const line2 of matrix.slice(headRows)) {
|
|
6875
|
+
if (!line2 || line2.every((v) => v === null || v === void 0 || String(v).trim() === "")) continue;
|
|
5426
6876
|
const row = {};
|
|
5427
6877
|
mapped.forEach((node, i) => {
|
|
5428
6878
|
if (!node) return;
|
|
5429
6879
|
const def = node.def;
|
|
5430
|
-
const raw = asText(
|
|
6880
|
+
const raw = asText(line2[i]);
|
|
5431
6881
|
const res = grid.acceptText(def, row, raw, sample?.[def.field]);
|
|
5432
6882
|
if (res.ok) {
|
|
5433
6883
|
if (!(raw === "" && (res.value === null || res.value === ""))) row[def.field] = res.value;
|
|
@@ -5778,58 +7228,6 @@ var GridCharts = class {
|
|
|
5778
7228
|
}
|
|
5779
7229
|
};
|
|
5780
7230
|
|
|
5781
|
-
// src/rows/sort.ts
|
|
5782
|
-
function compareNonBlank(a, b, collator) {
|
|
5783
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
5784
|
-
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
5785
|
-
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
5786
|
-
return collator.compare(String(a), String(b));
|
|
5787
|
-
}
|
|
5788
|
-
function normalize(def, v) {
|
|
5789
|
-
if (isBlank(v)) return null;
|
|
5790
|
-
if (def.type === "number") {
|
|
5791
|
-
const n = typeof v === "number" ? v : Number(v);
|
|
5792
|
-
return Number.isNaN(n) ? null : n;
|
|
5793
|
-
}
|
|
5794
|
-
if (def.type === "date") return toDate(v)?.getTime() ?? null;
|
|
5795
|
-
return v;
|
|
5796
|
-
}
|
|
5797
|
-
function compareValues(a, b, collator) {
|
|
5798
|
-
const ba = isBlank(a), bb = isBlank(b);
|
|
5799
|
-
if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
|
|
5800
|
-
return compareNonBlank(a, b, collator);
|
|
5801
|
-
}
|
|
5802
|
-
function sortNodes(nodes, sort, resolve, collator) {
|
|
5803
|
-
const keys = sort.map((s) => ({ def: resolve(s.colId), mul: s.dir === "desc" ? -1 : 1 })).filter((k) => !!k.def);
|
|
5804
|
-
if (!keys.length) return nodes;
|
|
5805
|
-
const values = keys.map((k) => nodes.map((n) => {
|
|
5806
|
-
const v = getCellValue(k.def, n.data);
|
|
5807
|
-
return k.def.comparator ? v : normalize(k.def, v);
|
|
5808
|
-
}));
|
|
5809
|
-
const order = nodes.map((_, i) => i);
|
|
5810
|
-
order.sort((i, j) => {
|
|
5811
|
-
for (let k = 0; k < keys.length; k++) {
|
|
5812
|
-
const { def, mul } = keys[k];
|
|
5813
|
-
const a = values[k][i], b = values[k][j];
|
|
5814
|
-
let c;
|
|
5815
|
-
if (def.comparator) {
|
|
5816
|
-
try {
|
|
5817
|
-
c = (Number(def.comparator(a, b, nodes[i].data, nodes[j].data)) || 0) * mul;
|
|
5818
|
-
} catch {
|
|
5819
|
-
warn(`comparator of column "${def.id ?? def.field}" threw; treating values as equal`);
|
|
5820
|
-
c = 0;
|
|
5821
|
-
}
|
|
5822
|
-
} else {
|
|
5823
|
-
const ba = isBlank(a), bb = isBlank(b);
|
|
5824
|
-
c = ba || bb ? ba === bb ? 0 : ba ? 1 : -1 : compareNonBlank(a, b, collator) * mul;
|
|
5825
|
-
}
|
|
5826
|
-
if (c) return c;
|
|
5827
|
-
}
|
|
5828
|
-
return i - j;
|
|
5829
|
-
});
|
|
5830
|
-
return order.map((i) => nodes[i]);
|
|
5831
|
-
}
|
|
5832
|
-
|
|
5833
7231
|
// src/pivot.ts
|
|
5834
7232
|
var SOURCE = /* @__PURE__ */ new WeakMap();
|
|
5835
7233
|
var aggName = (a) => typeof a === "function" ? "custom" : a;
|
|
@@ -5955,7 +7353,7 @@ function buildPivot(source, config, defOf, labels, locale) {
|
|
|
5955
7353
|
}
|
|
5956
7354
|
|
|
5957
7355
|
// src/version.ts
|
|
5958
|
-
var version = "1.0
|
|
7356
|
+
var version = "1.6.0";
|
|
5959
7357
|
|
|
5960
7358
|
// src/license.ts
|
|
5961
7359
|
var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
|
|
@@ -6143,6 +7541,7 @@ var ColumnMenu = class {
|
|
|
6143
7541
|
];
|
|
6144
7542
|
if (!def.lockVisible) items.push([loc.hideColumn, () => this.grid.setColumnsVisible([colId], false)]);
|
|
6145
7543
|
items.push([loc.chooseColumns, () => this.grid.chooser.open()], [loc.resetColumns, () => this.grid.resetColumns()]);
|
|
7544
|
+
if (this.grid.options.conditionalFormatMenu !== false) items.push([`${loc.cf.title}\u2026`, () => this.grid.formatDialog.open(colId)]);
|
|
6146
7545
|
popup.append(this.section(items));
|
|
6147
7546
|
const filter = this.filterSection(colId, () => this.close());
|
|
6148
7547
|
if (filter) popup.append(filter);
|
|
@@ -6189,11 +7588,11 @@ var ColumnMenu = class {
|
|
|
6189
7588
|
}
|
|
6190
7589
|
section(items) {
|
|
6191
7590
|
const s = el("div", "fg-menu-section");
|
|
6192
|
-
for (const [label,
|
|
7591
|
+
for (const [label, run2] of items) {
|
|
6193
7592
|
const b = el("button", "fg-menu-item", { type: "button" });
|
|
6194
7593
|
b.textContent = label;
|
|
6195
7594
|
b.addEventListener("click", () => {
|
|
6196
|
-
|
|
7595
|
+
run2();
|
|
6197
7596
|
this.close();
|
|
6198
7597
|
});
|
|
6199
7598
|
s.append(b);
|
|
@@ -7325,6 +8724,611 @@ var FilterBuilder = class {
|
|
|
7325
8724
|
}
|
|
7326
8725
|
};
|
|
7327
8726
|
|
|
8727
|
+
// src/interactions/formatDialog.ts
|
|
8728
|
+
var CF_PRESETS = [
|
|
8729
|
+
{ key: "red", style: { background: "#ffc7ce", color: "#9c0006" } },
|
|
8730
|
+
{ key: "yellow", style: { background: "#ffeb9c", color: "#9c5700" } },
|
|
8731
|
+
{ key: "green", style: { background: "#c6efce", color: "#006100" } },
|
|
8732
|
+
{ key: "blue", style: { background: "#dbeafe", color: "#1e3a8a" } },
|
|
8733
|
+
{ key: "redText", style: { color: "#dc2626" } },
|
|
8734
|
+
{ key: "bold", style: { bold: true } }
|
|
8735
|
+
];
|
|
8736
|
+
var TYPES2 = ["value", "formula", "top", "bottom", "aboveAverage", "belowAverage", "duplicate", "unique", "dataBar", "colorScale", "iconSet"];
|
|
8737
|
+
var OPS2 = ["gt", "gte", "lt", "lte", "eq", "ne", "between", "notBetween", "contains", "notContains", "startsWith", "endsWith", "blank", "notBlank"];
|
|
8738
|
+
var NO_VALUE3 = ["blank", "notBlank"];
|
|
8739
|
+
var SCALES = ["dataBar", "colorScale", "iconSet"];
|
|
8740
|
+
var FormatDialog = class {
|
|
8741
|
+
constructor(grid) {
|
|
8742
|
+
this.grid = grid;
|
|
8743
|
+
this.popup = null;
|
|
8744
|
+
this.off = [];
|
|
8745
|
+
}
|
|
8746
|
+
get isOpen() {
|
|
8747
|
+
return !!this.popup;
|
|
8748
|
+
}
|
|
8749
|
+
open(colId) {
|
|
8750
|
+
this.close();
|
|
8751
|
+
const { grid } = this;
|
|
8752
|
+
const node = grid.columns.getNode(colId);
|
|
8753
|
+
if (!node || node.children || isSystemCol(colId)) return;
|
|
8754
|
+
const loc = grid.store.state.locale;
|
|
8755
|
+
const cf = loc.cf;
|
|
8756
|
+
const label = node.def.header ?? node.def.field ?? colId;
|
|
8757
|
+
const popup = el("div", `fg-menu fg-cf-dialog fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": `${cf.title}: ${label}` });
|
|
8758
|
+
grid.applyFontTo(popup);
|
|
8759
|
+
const head = el("div", "fg-chooser-head");
|
|
8760
|
+
const title = el("span", "fg-menu-title");
|
|
8761
|
+
title.textContent = `${cf.title} \xB7 ${label}`;
|
|
8762
|
+
const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
|
|
8763
|
+
x.textContent = "\xD7";
|
|
8764
|
+
x.addEventListener("click", () => this.close());
|
|
8765
|
+
head.append(title, x);
|
|
8766
|
+
const list = el("div", "fg-cf-list");
|
|
8767
|
+
const opName = (op) => cf.ops[op] ?? loc.ops[op] ?? op;
|
|
8768
|
+
const describe = (r2) => {
|
|
8769
|
+
const type = r2.type ?? "value";
|
|
8770
|
+
const name = cf.types[type];
|
|
8771
|
+
if (type === "value") {
|
|
8772
|
+
const op = r2.op ?? "eq";
|
|
8773
|
+
if (NO_VALUE3.includes(op)) return opName(op);
|
|
8774
|
+
return `${opName(op)} ${String(r2.value ?? "")}${op === "between" || op === "notBetween" ? ` \u2013 ${String(r2.valueTo ?? "")}` : ""}`;
|
|
8775
|
+
}
|
|
8776
|
+
if (type === "formula") return `${name}: ${r2.formula ?? ""}`;
|
|
8777
|
+
if (type === "top" || type === "bottom") return `${name} ${r2.rank ?? 10}${r2.percent ? "%" : ""}`;
|
|
8778
|
+
return name;
|
|
8779
|
+
};
|
|
8780
|
+
const paintList = () => {
|
|
8781
|
+
list.replaceChildren();
|
|
8782
|
+
const all2 = grid.getConditionalFormats();
|
|
8783
|
+
const mine = all2.map((r2, i) => ({ r: r2, i })).filter(({ r: r2 }) => r2.columns.includes(colId));
|
|
8784
|
+
if (!mine.length) {
|
|
8785
|
+
const empty = el("div", "fg-cf-empty");
|
|
8786
|
+
empty.textContent = cf.none;
|
|
8787
|
+
list.append(empty);
|
|
8788
|
+
return;
|
|
8789
|
+
}
|
|
8790
|
+
for (const { r: r2, i } of mine) {
|
|
8791
|
+
const row = el("div", "fg-cf-rule");
|
|
8792
|
+
const sample = el("span", "fg-cf-sample", { "aria-hidden": "true" });
|
|
8793
|
+
sample.textContent = r2.type === "iconSet" ? "\u25B2" : r2.type === "dataBar" ? "\u25AC" : "Aa";
|
|
8794
|
+
if (r2.style?.background) sample.style.background = r2.style.background;
|
|
8795
|
+
if (r2.style?.color) sample.style.color = r2.style.color;
|
|
8796
|
+
if (r2.style?.bold) sample.style.fontWeight = "700";
|
|
8797
|
+
if (r2.type === "colorScale") sample.style.background = `linear-gradient(to right, ${(r2.colors ?? ["#f8696b", "#ffeb84", "#63be7b"]).join(",")})`;
|
|
8798
|
+
const text2 = el("span", "fg-cf-rule-text");
|
|
8799
|
+
text2.textContent = `${describe(r2)}${r2.row ? ` \xB7 ${cf.wholeRow}` : ""}`;
|
|
8800
|
+
text2.title = text2.textContent;
|
|
8801
|
+
const del = el("button", "fg-chooser-close", { type: "button", "aria-label": `${loc.close}: ${text2.textContent}` });
|
|
8802
|
+
del.textContent = "\xD7";
|
|
8803
|
+
del.addEventListener("click", () => {
|
|
8804
|
+
const next = grid.getConditionalFormats();
|
|
8805
|
+
const rule = next[i];
|
|
8806
|
+
rule.columns = rule.columns.filter((c) => c !== colId);
|
|
8807
|
+
grid.setConditionalFormats(next.filter((f) => f.columns.length));
|
|
8808
|
+
paintList();
|
|
8809
|
+
});
|
|
8810
|
+
row.append(sample, text2, del);
|
|
8811
|
+
list.append(row);
|
|
8812
|
+
}
|
|
8813
|
+
};
|
|
8814
|
+
paintList();
|
|
8815
|
+
const form = el("div", "fg-cf-form");
|
|
8816
|
+
const typeSel = el("select", "fg-filter-op fg-cf-wide fg-cf-type", { "aria-label": cf.type });
|
|
8817
|
+
for (const t of TYPES2) {
|
|
8818
|
+
const o = el("option");
|
|
8819
|
+
o.value = t;
|
|
8820
|
+
o.textContent = cf.types[t];
|
|
8821
|
+
typeSel.append(o);
|
|
8822
|
+
}
|
|
8823
|
+
const opSel = el("select", "fg-filter-op fg-cf-wide fg-cf-op", { "aria-label": loc.filter });
|
|
8824
|
+
for (const op of OPS2) {
|
|
8825
|
+
const o = el("option");
|
|
8826
|
+
o.value = op;
|
|
8827
|
+
o.textContent = opName(op);
|
|
8828
|
+
opSel.append(o);
|
|
8829
|
+
}
|
|
8830
|
+
const numeric = node.def.type === "number";
|
|
8831
|
+
const v1 = el("input", "fg-input fg-cf-value", { type: numeric ? "number" : "text", placeholder: loc.value, "aria-label": loc.value });
|
|
8832
|
+
const v2 = el("input", "fg-input fg-cf-value-to", { type: numeric ? "number" : "text", placeholder: loc.valueTo, "aria-label": loc.valueTo });
|
|
8833
|
+
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
|
+
const rank = el("input", "fg-input fg-cf-rank", { type: "number", min: "1", value: "10", "aria-label": cf.count });
|
|
8835
|
+
const pctLabel = el("label", "fg-cf-pct");
|
|
8836
|
+
const pct = el("input", "", { type: "checkbox" });
|
|
8837
|
+
pctLabel.append(pct, " %");
|
|
8838
|
+
const swatches = el("div", "fg-cf-swatches fg-cf-wide", { role: "group", "aria-label": cf.style });
|
|
8839
|
+
let preset = 0;
|
|
8840
|
+
CF_PRESETS.forEach((p, k) => {
|
|
8841
|
+
const b = el("button", "fg-cf-swatch", { type: "button", "aria-label": cf.styles[p.key], title: cf.styles[p.key], "aria-pressed": String(k === preset) });
|
|
8842
|
+
b.textContent = p.key === "bold" ? "B" : "Aa";
|
|
8843
|
+
if (p.style.background) b.style.background = p.style.background;
|
|
8844
|
+
if (p.style.color) b.style.color = p.style.color;
|
|
8845
|
+
b.addEventListener("click", () => {
|
|
8846
|
+
preset = k;
|
|
8847
|
+
for (const s of swatches.children) s.setAttribute("aria-pressed", String(s === b));
|
|
8848
|
+
});
|
|
8849
|
+
swatches.append(b);
|
|
8850
|
+
});
|
|
8851
|
+
const rowLabel = el("label", "fg-cf-wide fg-cf-row");
|
|
8852
|
+
const rowBox = el("input", "", { type: "checkbox" });
|
|
8853
|
+
rowLabel.append(rowBox, ` ${cf.wholeRow}`);
|
|
8854
|
+
form.append(typeSel, opSel, v1, v2, formula, rank, pctLabel, swatches, rowLabel);
|
|
8855
|
+
const sync = () => {
|
|
8856
|
+
const t = typeSel.value;
|
|
8857
|
+
const op = opSel.value;
|
|
8858
|
+
opSel.hidden = t !== "value";
|
|
8859
|
+
v1.hidden = t !== "value" || NO_VALUE3.includes(op);
|
|
8860
|
+
v2.hidden = t !== "value" || op !== "between" && op !== "notBetween";
|
|
8861
|
+
formula.hidden = t !== "formula";
|
|
8862
|
+
rank.hidden = pctLabel.hidden = t !== "top" && t !== "bottom";
|
|
8863
|
+
swatches.hidden = SCALES.includes(t);
|
|
8864
|
+
rowLabel.hidden = t !== "value" && t !== "formula";
|
|
8865
|
+
};
|
|
8866
|
+
typeSel.addEventListener("change", sync);
|
|
8867
|
+
opSel.addEventListener("change", sync);
|
|
8868
|
+
sync();
|
|
8869
|
+
const read = () => {
|
|
8870
|
+
const type = typeSel.value;
|
|
8871
|
+
const rule = { columns: [colId], type };
|
|
8872
|
+
const val = (input) => numeric && input.value !== "" ? Number(input.value) : input.value;
|
|
8873
|
+
if (type === "value") {
|
|
8874
|
+
rule.op = opSel.value;
|
|
8875
|
+
if (!NO_VALUE3.includes(rule.op)) {
|
|
8876
|
+
if (v1.value === "") return null;
|
|
8877
|
+
rule.value = val(v1);
|
|
8878
|
+
if (rule.op === "between" || rule.op === "notBetween") {
|
|
8879
|
+
if (v2.value === "") return null;
|
|
8880
|
+
rule.valueTo = val(v2);
|
|
8881
|
+
}
|
|
8882
|
+
}
|
|
8883
|
+
} else if (type === "formula") {
|
|
8884
|
+
const f = formula.value.trim();
|
|
8885
|
+
if (!f) return null;
|
|
8886
|
+
rule.formula = f.startsWith("=") ? f : `=${f}`;
|
|
8887
|
+
} else if (type === "top" || type === "bottom") {
|
|
8888
|
+
rule.rank = Math.max(1, Math.floor(Number(rank.value) || 10));
|
|
8889
|
+
if (pct.checked) rule.percent = true;
|
|
8890
|
+
}
|
|
8891
|
+
if (!SCALES.includes(type)) rule.style = { ...CF_PRESETS[preset].style };
|
|
8892
|
+
if (rowBox.checked && (type === "value" || type === "formula")) rule.row = true;
|
|
8893
|
+
return rule;
|
|
8894
|
+
};
|
|
8895
|
+
const actions = el("div", "fg-menu-actions");
|
|
8896
|
+
const clear = el("button", "fg-btn", { type: "button" });
|
|
8897
|
+
clear.textContent = cf.clear;
|
|
8898
|
+
clear.addEventListener("click", () => {
|
|
8899
|
+
grid.setConditionalFormats(grid.getConditionalFormats().map((r2) => ({ ...r2, columns: r2.columns.filter((c) => c !== colId) })).filter((r2) => r2.columns.length));
|
|
8900
|
+
paintList();
|
|
8901
|
+
});
|
|
8902
|
+
const add = el("button", "fg-btn fg-btn-primary", { type: "button" });
|
|
8903
|
+
add.textContent = cf.add;
|
|
8904
|
+
add.addEventListener("click", () => {
|
|
8905
|
+
const rule = read();
|
|
8906
|
+
if (!rule) {
|
|
8907
|
+
(typeSel.value === "formula" ? formula : v1).focus();
|
|
8908
|
+
return;
|
|
8909
|
+
}
|
|
8910
|
+
grid.setConditionalFormats([...grid.getConditionalFormats(), rule]);
|
|
8911
|
+
v1.value = v2.value = formula.value = "";
|
|
8912
|
+
paintList();
|
|
8913
|
+
});
|
|
8914
|
+
actions.append(clear, add);
|
|
8915
|
+
popup.append(head, list, form, actions);
|
|
8916
|
+
popupParent(grid.renderer.root).append(popup);
|
|
8917
|
+
const r = grid.renderer.root.getBoundingClientRect();
|
|
8918
|
+
const w = popup.offsetWidth || 340;
|
|
8919
|
+
popup.style.left = `${Math.max(4, Math.min(r.left + 24, (window.innerWidth || 1024) - w - 4))}px`;
|
|
8920
|
+
popup.style.top = `${Math.max(4, r.top + 24)}px`;
|
|
8921
|
+
this.popup = popup;
|
|
8922
|
+
typeSel.focus();
|
|
8923
|
+
const onDown = (e) => {
|
|
8924
|
+
if (!eventInside(e, popup)) this.close();
|
|
8925
|
+
};
|
|
8926
|
+
const onKey = (e) => {
|
|
8927
|
+
if (e.key === "Escape") this.close();
|
|
8928
|
+
};
|
|
8929
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
8930
|
+
document.addEventListener("keydown", onKey, true);
|
|
8931
|
+
this.off.push(
|
|
8932
|
+
() => document.removeEventListener("pointerdown", onDown, true),
|
|
8933
|
+
() => document.removeEventListener("keydown", onKey, true)
|
|
8934
|
+
);
|
|
8935
|
+
}
|
|
8936
|
+
close() {
|
|
8937
|
+
for (const f of this.off) f();
|
|
8938
|
+
this.off = [];
|
|
8939
|
+
this.popup?.remove();
|
|
8940
|
+
this.popup = null;
|
|
8941
|
+
}
|
|
8942
|
+
};
|
|
8943
|
+
|
|
8944
|
+
// src/assistant.ts
|
|
8945
|
+
var FILTER_TYPES = ["text", "number", "date", "set", "combined"];
|
|
8946
|
+
var CHART_TYPES = ["bar", "stackedBar", "line", "area", "pie", "donut", "combo", "scatter"];
|
|
8947
|
+
var MAX_ACTIONS = 30;
|
|
8948
|
+
function describeColumn(grid, id, def, rows) {
|
|
8949
|
+
const col = { id, header: def.header ?? def.field ?? id };
|
|
8950
|
+
if (def.type) col.type = def.type;
|
|
8951
|
+
if (def.formula) col.formula = def.formula;
|
|
8952
|
+
if (def.aggFunc && typeof def.aggFunc === "string") col.aggFunc = def.aggFunc;
|
|
8953
|
+
let blanks = 0;
|
|
8954
|
+
const counts = /* @__PURE__ */ new Map();
|
|
8955
|
+
let min = Infinity, max = -Infinity, sum = 0, nums = 0;
|
|
8956
|
+
let minText = "", maxText = "";
|
|
8957
|
+
for (const r of rows) {
|
|
8958
|
+
const v = getCellValue(def, r);
|
|
8959
|
+
if (isBlank(v)) {
|
|
8960
|
+
blanks++;
|
|
8961
|
+
continue;
|
|
8962
|
+
}
|
|
8963
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
8964
|
+
nums++;
|
|
8965
|
+
sum += v;
|
|
8966
|
+
if (v < min) min = v;
|
|
8967
|
+
if (v > max) max = v;
|
|
8968
|
+
} else if (def.type === "date") {
|
|
8969
|
+
const t = v instanceof Date ? v.toISOString().slice(0, 10) : String(v);
|
|
8970
|
+
if (!minText || t < minText) minText = t;
|
|
8971
|
+
if (!maxText || t > maxText) maxText = t;
|
|
8972
|
+
}
|
|
8973
|
+
if (counts.size < 5e3) {
|
|
8974
|
+
const k = setKey(v);
|
|
8975
|
+
const c = counts.get(k);
|
|
8976
|
+
if (c) c.n++;
|
|
8977
|
+
else counts.set(k, { value: v, n: 1 });
|
|
8978
|
+
}
|
|
8979
|
+
}
|
|
8980
|
+
col.blanks = blanks;
|
|
8981
|
+
col.distinct = counts.size;
|
|
8982
|
+
if (nums) Object.assign(col, { min, max, avg: Math.round(sum / nums * 1e3) / 1e3 });
|
|
8983
|
+
if (minText) Object.assign(col, { min: minText, max: maxText });
|
|
8984
|
+
if (!nums && counts.size) {
|
|
8985
|
+
col.top = [...counts.values()].sort((a, b) => b.n - a.n).slice(0, 12).map((c) => ({ value: String(c.value).slice(0, 80), count: c.n }));
|
|
8986
|
+
}
|
|
8987
|
+
return col;
|
|
8988
|
+
}
|
|
8989
|
+
function assistantRequest(grid, prompt, options) {
|
|
8990
|
+
const rows = grid.rowModel.dataRows().map((n) => n.data);
|
|
8991
|
+
const all2 = grid.columns.getState().map((s2) => grid.columns.getNode(s2.colId)).filter((n) => n && !n.children && !isSystemCol(n.id));
|
|
8992
|
+
const columns = all2.map((n) => ({ ...describeColumn(grid, n.id, n.def, rows), hidden: grid.columns.getState().find((s2) => s2.colId === n.id)?.hide || void 0 }));
|
|
8993
|
+
const sampleN = typeof options.sampleRows === "number" && options.sampleRows >= 0 ? Math.min(50, options.sampleRows) : 5;
|
|
8994
|
+
const sample = rows.slice(0, sampleN).map((r) => Object.fromEntries(all2.map((n) => [n.id, getCellValue(n.def, r)])));
|
|
8995
|
+
const s = grid.store.state;
|
|
8996
|
+
return {
|
|
8997
|
+
prompt: String(prompt).slice(0, 2e3),
|
|
8998
|
+
locale: s.locale.code,
|
|
8999
|
+
rowCount: rows.length,
|
|
9000
|
+
totalRows: grid.rowModel.server ? rows.length : grid.store.state.data.length,
|
|
9001
|
+
columns,
|
|
9002
|
+
sample,
|
|
9003
|
+
state: {
|
|
9004
|
+
sort: s.sort,
|
|
9005
|
+
filters: s.filters,
|
|
9006
|
+
quickFilter: s.quickFilter,
|
|
9007
|
+
advancedFilter: s.advancedFilter,
|
|
9008
|
+
groupBy: grid.getGroupBy(),
|
|
9009
|
+
pivot: grid.getPivot(),
|
|
9010
|
+
conditionalFormats: grid.getConditionalFormats()
|
|
9011
|
+
},
|
|
9012
|
+
...options.context ? { context: options.context() } : {}
|
|
9013
|
+
};
|
|
9014
|
+
}
|
|
9015
|
+
function applyAssistantActions(grid, actions) {
|
|
9016
|
+
const before = { state: grid.api.getState(), defs: grid.columnDefs(), summary: grid.summaryOptions() };
|
|
9017
|
+
const applied = [];
|
|
9018
|
+
const skipped = [];
|
|
9019
|
+
const charts = [];
|
|
9020
|
+
const list = Array.isArray(actions) ? actions.slice(0, MAX_ACTIONS) : [];
|
|
9021
|
+
const known = (id) => typeof id === "string" && !!grid.columns.getNode(id) && !grid.columns.getNode(id).children && !isSystemCol(id);
|
|
9022
|
+
const ok = (a) => applied.push(a);
|
|
9023
|
+
const skip = (a, reason) => skipped.push({ action: a, reason });
|
|
9024
|
+
for (const raw of list) {
|
|
9025
|
+
const a = raw;
|
|
9026
|
+
if (!a || typeof a !== "object" || typeof a.type !== "string") {
|
|
9027
|
+
skip(raw, "not an action");
|
|
9028
|
+
continue;
|
|
9029
|
+
}
|
|
9030
|
+
try {
|
|
9031
|
+
switch (a.type) {
|
|
9032
|
+
case "sort": {
|
|
9033
|
+
const sort = (Array.isArray(a.sort) ? a.sort : []).filter((x) => !!x && known(x.colId) && (x.dir === "asc" || x.dir === "desc"));
|
|
9034
|
+
if (Array.isArray(a.sort) && sort.length !== a.sort.length) skip(a, "unknown column in sort");
|
|
9035
|
+
grid.setSort(sort.map((x) => ({ colId: x.colId, dir: x.dir })));
|
|
9036
|
+
ok(a);
|
|
9037
|
+
break;
|
|
9038
|
+
}
|
|
9039
|
+
case "filter": {
|
|
9040
|
+
if (!known(a.colId)) {
|
|
9041
|
+
skip(a, "unknown column");
|
|
9042
|
+
break;
|
|
9043
|
+
}
|
|
9044
|
+
const m = a.model;
|
|
9045
|
+
if (m !== null && (!m || typeof m !== "object" || !FILTER_TYPES.includes(m.type))) {
|
|
9046
|
+
skip(a, "bad filter model");
|
|
9047
|
+
break;
|
|
9048
|
+
}
|
|
9049
|
+
grid.setFilter(a.colId, m);
|
|
9050
|
+
ok(a);
|
|
9051
|
+
break;
|
|
9052
|
+
}
|
|
9053
|
+
case "clearFilters":
|
|
9054
|
+
for (const id of Object.keys(grid.store.state.filters)) grid.setFilter(id, null);
|
|
9055
|
+
grid.setAdvancedFilter(null);
|
|
9056
|
+
grid.api.setQuickFilter("");
|
|
9057
|
+
ok(a);
|
|
9058
|
+
break;
|
|
9059
|
+
case "quickFilter":
|
|
9060
|
+
grid.api.setQuickFilter(typeof a.text === "string" ? a.text.slice(0, 200) : "");
|
|
9061
|
+
ok(a);
|
|
9062
|
+
break;
|
|
9063
|
+
case "advancedFilter": {
|
|
9064
|
+
const f = a.filter;
|
|
9065
|
+
const ids = [];
|
|
9066
|
+
const walk = (x) => {
|
|
9067
|
+
const o = x;
|
|
9068
|
+
if (Array.isArray(o?.conditions)) o.conditions.forEach(walk);
|
|
9069
|
+
else ids.push(o?.colId);
|
|
9070
|
+
};
|
|
9071
|
+
if (f) walk(f);
|
|
9072
|
+
if (f && (!Array.isArray(f.conditions) || !ids.every(known))) {
|
|
9073
|
+
skip(a, "unknown column in advanced filter");
|
|
9074
|
+
break;
|
|
9075
|
+
}
|
|
9076
|
+
grid.setAdvancedFilter(f ?? null);
|
|
9077
|
+
ok(a);
|
|
9078
|
+
break;
|
|
9079
|
+
}
|
|
9080
|
+
case "groupBy": {
|
|
9081
|
+
const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
|
|
9082
|
+
grid.setGroupBy(ids);
|
|
9083
|
+
if (a.expand) grid.expandAll(true);
|
|
9084
|
+
ok(a);
|
|
9085
|
+
break;
|
|
9086
|
+
}
|
|
9087
|
+
case "pivot": {
|
|
9088
|
+
const p = a.pivot;
|
|
9089
|
+
if (p && (!Array.isArray(p.rows) || !Array.isArray(p.values) || !p.rows.every(known) || !(p.columns ?? []).every(known) || !p.values.every((v) => v && known(v.colId)))) {
|
|
9090
|
+
skip(a, "unknown column in pivot");
|
|
9091
|
+
break;
|
|
9092
|
+
}
|
|
9093
|
+
grid.setPivot(p ?? null);
|
|
9094
|
+
ok(a);
|
|
9095
|
+
break;
|
|
9096
|
+
}
|
|
9097
|
+
case "showColumns":
|
|
9098
|
+
case "hideColumns": {
|
|
9099
|
+
const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
|
|
9100
|
+
if (ids.length) grid.setColumnsVisible(ids, a.type === "showColumns");
|
|
9101
|
+
ok(a);
|
|
9102
|
+
break;
|
|
9103
|
+
}
|
|
9104
|
+
case "conditionalFormat": {
|
|
9105
|
+
const f = a.format;
|
|
9106
|
+
if (!f || !Array.isArray(f.columns) || !f.columns.length || !f.columns.every(known)) {
|
|
9107
|
+
skip(a, "unknown column in format");
|
|
9108
|
+
break;
|
|
9109
|
+
}
|
|
9110
|
+
grid.setConditionalFormats([...grid.getConditionalFormats(), f]);
|
|
9111
|
+
ok(a);
|
|
9112
|
+
break;
|
|
9113
|
+
}
|
|
9114
|
+
case "clearConditionalFormats":
|
|
9115
|
+
grid.setConditionalFormats([]);
|
|
9116
|
+
ok(a);
|
|
9117
|
+
break;
|
|
9118
|
+
case "addColumn": {
|
|
9119
|
+
const c = a.column;
|
|
9120
|
+
if (!c || typeof c.id !== "string" || !/^[\w.-]{1,40}$/.test(c.id) || grid.columns.getNode(c.id)) {
|
|
9121
|
+
skip(a, "bad or taken column id");
|
|
9122
|
+
break;
|
|
9123
|
+
}
|
|
9124
|
+
const f = typeof c.formula === "string" ? c.formula.startsWith("=") ? c.formula : `=${c.formula}` : "";
|
|
9125
|
+
const refs = f ? formulaRefs(f) : null;
|
|
9126
|
+
if (!refs) {
|
|
9127
|
+
skip(a, "formula does not parse");
|
|
9128
|
+
break;
|
|
9129
|
+
}
|
|
9130
|
+
const def = { id: c.id, header: typeof c.header === "string" ? c.header.slice(0, 80) : c.id, formula: f };
|
|
9131
|
+
if (c.type === "number" || c.type === "date" || c.type === "text") def.type = c.type;
|
|
9132
|
+
grid.api.setColumns([...grid.columnDefs(), def]);
|
|
9133
|
+
ok(a);
|
|
9134
|
+
break;
|
|
9135
|
+
}
|
|
9136
|
+
case "summary": {
|
|
9137
|
+
const s = a.summary;
|
|
9138
|
+
if (s && (!Array.isArray(s.rows) || !s.rows.every((r) => r && Object.keys(r.columns ?? {}).every(known)))) {
|
|
9139
|
+
skip(a, "unknown column in summary");
|
|
9140
|
+
break;
|
|
9141
|
+
}
|
|
9142
|
+
grid.setSummary(s ?? null);
|
|
9143
|
+
ok(a);
|
|
9144
|
+
break;
|
|
9145
|
+
}
|
|
9146
|
+
case "chart": {
|
|
9147
|
+
const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
|
|
9148
|
+
const displayed = grid.columns.displayed().map((c) => c.id);
|
|
9149
|
+
const idx = ids.map((id) => displayed.indexOf(id)).filter((i) => i >= 0);
|
|
9150
|
+
const count = grid.rowModel.getRowCount();
|
|
9151
|
+
if (!idx.length || !count) {
|
|
9152
|
+
skip(a, "nothing to chart");
|
|
9153
|
+
break;
|
|
9154
|
+
}
|
|
9155
|
+
const range = { start: { rowIndex: 0, colId: displayed[Math.min(...idx)] }, end: { rowIndex: Math.min(count, 200) - 1, colId: displayed[Math.max(...idx)] } };
|
|
9156
|
+
charts.push(grid.api.createChart({ type: CHART_TYPES.includes(a.chartType) ? a.chartType : "bar", range, title: typeof a.title === "string" ? a.title.slice(0, 80) : void 0 }));
|
|
9157
|
+
ok(a);
|
|
9158
|
+
break;
|
|
9159
|
+
}
|
|
9160
|
+
case "message":
|
|
9161
|
+
ok(a);
|
|
9162
|
+
break;
|
|
9163
|
+
default:
|
|
9164
|
+
skip(a, "unknown action");
|
|
9165
|
+
}
|
|
9166
|
+
} catch {
|
|
9167
|
+
skip(a, "failed");
|
|
9168
|
+
}
|
|
9169
|
+
}
|
|
9170
|
+
const undo = () => {
|
|
9171
|
+
for (const c of charts) c.destroy();
|
|
9172
|
+
if (grid.columnDefs().length !== before.defs.length || grid.columnDefs().some((d, i) => d !== before.defs[i])) grid.api.setColumns(before.defs);
|
|
9173
|
+
grid.setSummary(before.summary);
|
|
9174
|
+
grid.api.setState(before.state);
|
|
9175
|
+
if (!before.state.conditionalFormats) grid.setConditionalFormats([]);
|
|
9176
|
+
};
|
|
9177
|
+
return { applied, skipped, undo };
|
|
9178
|
+
}
|
|
9179
|
+
async function askAssistant(grid, prompt, options, signal) {
|
|
9180
|
+
if (!options?.url) throw new Error("assistant.url is not set");
|
|
9181
|
+
const headers = typeof options.headers === "function" ? options.headers() : options.headers ?? {};
|
|
9182
|
+
const res = await fetch(options.url, {
|
|
9183
|
+
method: "POST",
|
|
9184
|
+
credentials: options.credentials ?? "same-origin",
|
|
9185
|
+
headers: { "Content-Type": "application/json", Accept: "application/json", ...headers },
|
|
9186
|
+
body: JSON.stringify(assistantRequest(grid, prompt, options)),
|
|
9187
|
+
signal
|
|
9188
|
+
});
|
|
9189
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from the assistant`);
|
|
9190
|
+
const json = await res.json();
|
|
9191
|
+
const result = applyAssistantActions(grid, Array.isArray(json?.actions) ? json.actions : []);
|
|
9192
|
+
const say = typeof json?.message === "string" ? json.message : result.applied.filter((a) => a.type === "message").map((a) => a.text ?? "").join("\n");
|
|
9193
|
+
return { ...result, message: say.slice(0, 4e3) };
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9196
|
+
// src/interactions/assistantPanel.ts
|
|
9197
|
+
var AssistantPanel = class {
|
|
9198
|
+
constructor(grid) {
|
|
9199
|
+
this.grid = grid;
|
|
9200
|
+
this.popup = null;
|
|
9201
|
+
this.log = null;
|
|
9202
|
+
this.off = [];
|
|
9203
|
+
this.busy = null;
|
|
9204
|
+
}
|
|
9205
|
+
get isOpen() {
|
|
9206
|
+
return !!this.popup;
|
|
9207
|
+
}
|
|
9208
|
+
open() {
|
|
9209
|
+
if (this.popup) {
|
|
9210
|
+
this.popup.querySelector(".fg-ai-input")?.focus();
|
|
9211
|
+
return;
|
|
9212
|
+
}
|
|
9213
|
+
const { grid } = this;
|
|
9214
|
+
const loc = grid.store.state.locale;
|
|
9215
|
+
const opts = grid.options.assistant;
|
|
9216
|
+
const popup = el("div", `fg-menu fg-ai-panel fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": loc.ai.title });
|
|
9217
|
+
grid.applyFontTo(popup);
|
|
9218
|
+
const head = el("div", "fg-chooser-head");
|
|
9219
|
+
const title = el("span", "fg-menu-title");
|
|
9220
|
+
title.textContent = loc.ai.title;
|
|
9221
|
+
const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
|
|
9222
|
+
x.textContent = "\xD7";
|
|
9223
|
+
x.addEventListener("click", () => this.close());
|
|
9224
|
+
head.append(title, x);
|
|
9225
|
+
const log = el("div", "fg-ai-log", { role: "log", "aria-live": "polite" });
|
|
9226
|
+
this.log = log;
|
|
9227
|
+
const form = el("form", "fg-ai-form");
|
|
9228
|
+
const input = el("textarea", "fg-input fg-ai-input", { rows: "2", placeholder: loc.ai.placeholder, "aria-label": loc.ai.placeholder, maxlength: "2000" });
|
|
9229
|
+
const send2 = el("button", "fg-btn fg-btn-primary fg-ai-send", { type: "submit" });
|
|
9230
|
+
send2.textContent = loc.ai.send;
|
|
9231
|
+
form.append(input, send2);
|
|
9232
|
+
const tips = el("div", "fg-ai-tips");
|
|
9233
|
+
for (const s of (opts?.suggestions ?? []).slice(0, 6)) {
|
|
9234
|
+
const b = el("button", "fg-ai-tip", { type: "button" });
|
|
9235
|
+
b.textContent = s;
|
|
9236
|
+
b.addEventListener("click", () => {
|
|
9237
|
+
input.value = s;
|
|
9238
|
+
input.focus();
|
|
9239
|
+
});
|
|
9240
|
+
tips.append(b);
|
|
9241
|
+
}
|
|
9242
|
+
input.addEventListener("keydown", (e) => {
|
|
9243
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
9244
|
+
e.preventDefault();
|
|
9245
|
+
form.requestSubmit?.() ?? form.dispatchEvent(new Event("submit", { cancelable: true }));
|
|
9246
|
+
}
|
|
9247
|
+
});
|
|
9248
|
+
form.addEventListener("submit", (e) => {
|
|
9249
|
+
e.preventDefault();
|
|
9250
|
+
const prompt = input.value.trim();
|
|
9251
|
+
if (!prompt || this.busy) return;
|
|
9252
|
+
input.value = "";
|
|
9253
|
+
void this.ask(prompt, send2);
|
|
9254
|
+
});
|
|
9255
|
+
popup.append(head, log, form);
|
|
9256
|
+
if (tips.childElementCount) popup.append(tips);
|
|
9257
|
+
popupParent(grid.renderer.root).append(popup);
|
|
9258
|
+
const r = grid.renderer.root.getBoundingClientRect();
|
|
9259
|
+
const w = popup.offsetWidth || 360;
|
|
9260
|
+
popup.style.left = `${Math.max(4, Math.min(r.right - w - 16, (window.innerWidth || 1024) - w - 4))}px`;
|
|
9261
|
+
popup.style.top = `${Math.max(4, r.top + 16)}px`;
|
|
9262
|
+
this.popup = popup;
|
|
9263
|
+
input.focus();
|
|
9264
|
+
const onDown = (e) => {
|
|
9265
|
+
if (!eventInside(e, popup) && !this.busy) this.close();
|
|
9266
|
+
};
|
|
9267
|
+
const onKey = (e) => {
|
|
9268
|
+
if (e.key === "Escape") this.close();
|
|
9269
|
+
};
|
|
9270
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
9271
|
+
document.addEventListener("keydown", onKey, true);
|
|
9272
|
+
this.off.push(
|
|
9273
|
+
() => document.removeEventListener("pointerdown", onDown, true),
|
|
9274
|
+
() => document.removeEventListener("keydown", onKey, true)
|
|
9275
|
+
);
|
|
9276
|
+
}
|
|
9277
|
+
say(who, text2) {
|
|
9278
|
+
const msg = el("div", `fg-ai-msg fg-ai-${who}`);
|
|
9279
|
+
msg.textContent = text2;
|
|
9280
|
+
this.log?.append(msg);
|
|
9281
|
+
if (this.log) this.log.scrollTop = this.log.scrollHeight;
|
|
9282
|
+
return msg;
|
|
9283
|
+
}
|
|
9284
|
+
async ask(prompt, send2) {
|
|
9285
|
+
const loc = this.grid.store.state.locale;
|
|
9286
|
+
this.say("user", prompt);
|
|
9287
|
+
const wait = this.say("ai", loc.ai.thinking);
|
|
9288
|
+
wait.classList.add("fg-ai-wait");
|
|
9289
|
+
this.busy = new AbortController();
|
|
9290
|
+
send2.disabled = true;
|
|
9291
|
+
try {
|
|
9292
|
+
const res = await askAssistant(this.grid, prompt, this.grid.options.assistant, this.busy.signal);
|
|
9293
|
+
wait.classList.remove("fg-ai-wait");
|
|
9294
|
+
const changed = res.applied.filter((a) => a.type !== "message").length;
|
|
9295
|
+
wait.textContent = res.message || (changed ? loc.ai.done : "");
|
|
9296
|
+
if (res.skipped.length) {
|
|
9297
|
+
const note = el("div", "fg-ai-note");
|
|
9298
|
+
note.textContent = `${loc.ai.skipped}: ${res.skipped.map((s) => s.reason).join(", ")}`;
|
|
9299
|
+
wait.append(note);
|
|
9300
|
+
}
|
|
9301
|
+
if (changed) {
|
|
9302
|
+
const undo = el("button", "fg-btn fg-ai-undo", { type: "button" });
|
|
9303
|
+
undo.textContent = loc.ai.undo;
|
|
9304
|
+
undo.addEventListener("click", () => {
|
|
9305
|
+
res.undo();
|
|
9306
|
+
undo.disabled = true;
|
|
9307
|
+
undo.textContent = loc.ai.undone;
|
|
9308
|
+
});
|
|
9309
|
+
wait.append(undo);
|
|
9310
|
+
}
|
|
9311
|
+
this.grid.events.emit("assistantAnswer", { prompt, message: res.message, applied: res.applied, skipped: res.skipped });
|
|
9312
|
+
} catch (e) {
|
|
9313
|
+
if (e?.name === "AbortError") return;
|
|
9314
|
+
wait.remove();
|
|
9315
|
+
this.say("error", loc.ai.failed);
|
|
9316
|
+
} finally {
|
|
9317
|
+
this.busy = null;
|
|
9318
|
+
send2.disabled = false;
|
|
9319
|
+
}
|
|
9320
|
+
}
|
|
9321
|
+
close() {
|
|
9322
|
+
this.busy?.abort();
|
|
9323
|
+
this.busy = null;
|
|
9324
|
+
for (const f of this.off) f();
|
|
9325
|
+
this.off = [];
|
|
9326
|
+
this.popup?.remove();
|
|
9327
|
+
this.popup = null;
|
|
9328
|
+
this.log = null;
|
|
9329
|
+
}
|
|
9330
|
+
};
|
|
9331
|
+
|
|
7328
9332
|
// src/grid.ts
|
|
7329
9333
|
var autoRowHeight = (size) => Math.round(size * 2.75);
|
|
7330
9334
|
var autoHeaderHeight = (size) => Math.round(size * 3.1);
|
|
@@ -7369,11 +9373,34 @@ var Grid = class {
|
|
|
7369
9373
|
/** 0-based page and rows per page (pagination only). */
|
|
7370
9374
|
this.page = 0;
|
|
7371
9375
|
this.pageSize = DEFAULT_PAGE_SIZE;
|
|
9376
|
+
/** What formulas see: every source row, and computed columns by id. */
|
|
9377
|
+
this.formulaScope = {
|
|
9378
|
+
rows: () => this.store.state.data,
|
|
9379
|
+
version: () => this.editVersion,
|
|
9380
|
+
computed: (name) => {
|
|
9381
|
+
const def = this.columns.getNode(name)?.def;
|
|
9382
|
+
return def && typeof def.formula === "string" && def.formula.startsWith("=") ? def.formula : void 0;
|
|
9383
|
+
}
|
|
9384
|
+
};
|
|
9385
|
+
this.cfRules = [];
|
|
9386
|
+
this.formats = new ConditionalFormatter({
|
|
9387
|
+
rules: () => this.cfRules,
|
|
9388
|
+
rows: () => this.rowModel.dataRows().map((n) => n.data),
|
|
9389
|
+
value: (colId, row) => {
|
|
9390
|
+
const def = this.columns.getNode(colId)?.def;
|
|
9391
|
+
return def ? getCellValue(def, row) : void 0;
|
|
9392
|
+
},
|
|
9393
|
+
version: () => this.editVersion,
|
|
9394
|
+
formula: (f, row) => evaluateFormula(f, row, /* @__PURE__ */ new Set(), 0, /* @__PURE__ */ new Map(), this.formulaScope)
|
|
9395
|
+
});
|
|
9396
|
+
this.formatDialog = new FormatDialog(this);
|
|
9397
|
+
this.assistantPanel = new AssistantPanel(this);
|
|
7372
9398
|
// ── pagination and server rows ──────────────────────────────
|
|
7373
9399
|
/** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
|
|
7374
9400
|
/** showOverlay: an overlay the app asked for, shown whatever the row count. */
|
|
7375
9401
|
this.forcedOverlay = null;
|
|
7376
9402
|
this.filterBuilder = new FilterBuilder(this);
|
|
9403
|
+
/** `${rowId}\u0000${colId}` → when the flash ends and which way a number moved. */
|
|
7377
9404
|
this.flashing = /* @__PURE__ */ new Map();
|
|
7378
9405
|
this.flashTimer = 0;
|
|
7379
9406
|
this.txQueue = [];
|
|
@@ -7413,6 +9440,7 @@ var Grid = class {
|
|
|
7413
9440
|
});
|
|
7414
9441
|
this.sourceDefs = Array.isArray(options.columns) ? options.columns : [];
|
|
7415
9442
|
this.columns = new ColumnModel(this.withSelect(this.sourceDefs));
|
|
9443
|
+
this.bindFormulas();
|
|
7416
9444
|
const pg = options.pagination;
|
|
7417
9445
|
if (pg && typeof pg === "object" && pg.pageSize !== void 0) {
|
|
7418
9446
|
if (Number.isInteger(pg.pageSize) && pg.pageSize > 0) this.pageSize = pg.pageSize;
|
|
@@ -7434,6 +9462,7 @@ var Grid = class {
|
|
|
7434
9462
|
collator: () => this.collator()
|
|
7435
9463
|
});
|
|
7436
9464
|
this.groupBy = this.validGroupBy(options.groupBy ?? []);
|
|
9465
|
+
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] }));
|
|
7437
9466
|
this.defaultExpanded = options.groupDefaultExpanded ?? 0;
|
|
7438
9467
|
if (options.treeData && !options.getRowId) warn("treeData needs getRowId so parent ids can be matched");
|
|
7439
9468
|
this.updateRows();
|
|
@@ -7449,6 +9478,7 @@ var Grid = class {
|
|
|
7449
9478
|
});
|
|
7450
9479
|
this.density = this.densityOf(options.density);
|
|
7451
9480
|
this.renderer.applyTheme();
|
|
9481
|
+
if (options.flashDuration) this.renderer.root.style.setProperty("--fg-flash-duration", `${this.flashMs()}ms`);
|
|
7452
9482
|
this.renderer.applyFont(this.font.size, this.font.family);
|
|
7453
9483
|
this.menu = new ColumnMenu(this);
|
|
7454
9484
|
this.chooser = new ColumnChooserDialog(this);
|
|
@@ -7475,6 +9505,8 @@ var Grid = class {
|
|
|
7475
9505
|
() => this.groupPanel?.destroy(),
|
|
7476
9506
|
() => this.charts.destroy(),
|
|
7477
9507
|
() => this.filterBuilder.close(),
|
|
9508
|
+
() => this.formatDialog.close(),
|
|
9509
|
+
() => this.assistantPanel.close(),
|
|
7478
9510
|
attachTooltips(this)
|
|
7479
9511
|
);
|
|
7480
9512
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -7490,6 +9522,45 @@ var Grid = class {
|
|
|
7490
9522
|
this.api = this.createApi();
|
|
7491
9523
|
liveGrids.set(this.renderer.root, this);
|
|
7492
9524
|
}
|
|
9525
|
+
/** The column definitions as given (setColumns), without the system columns. */
|
|
9526
|
+
columnDefs() {
|
|
9527
|
+
return [...this.sourceDefs];
|
|
9528
|
+
}
|
|
9529
|
+
summaryOptions() {
|
|
9530
|
+
return this.summaryOpts ? JSON.parse(JSON.stringify(this.summaryOpts)) : null;
|
|
9531
|
+
}
|
|
9532
|
+
setConditionalFormats(list) {
|
|
9533
|
+
if (!Array.isArray(list)) {
|
|
9534
|
+
warn("setConditionalFormats: formats must be an array");
|
|
9535
|
+
return;
|
|
9536
|
+
}
|
|
9537
|
+
this.cfRules = list.filter((r) => r && typeof r === "object" && Array.isArray(r.columns)).map((r) => ({ ...r, columns: [...r.columns] }));
|
|
9538
|
+
this.renderer.invalidate({ rows: true });
|
|
9539
|
+
this.events.emit("conditionalFormatsChange", { formats: this.getConditionalFormats() });
|
|
9540
|
+
}
|
|
9541
|
+
getConditionalFormats() {
|
|
9542
|
+
return JSON.parse(JSON.stringify(this.cfRules));
|
|
9543
|
+
}
|
|
9544
|
+
/** Conditional format of one cell, or null. */
|
|
9545
|
+
cellFormat(row, colId, value) {
|
|
9546
|
+
return this.cfRules.length ? this.formats.cell(colId, row, value) : null;
|
|
9547
|
+
}
|
|
9548
|
+
bindFormulas() {
|
|
9549
|
+
const defs = [];
|
|
9550
|
+
const walk = (list) => {
|
|
9551
|
+
for (const d of list) {
|
|
9552
|
+
if (!d || typeof d !== "object") continue;
|
|
9553
|
+
defs.push(d);
|
|
9554
|
+
if (Array.isArray(d.children)) walk(d.children);
|
|
9555
|
+
}
|
|
9556
|
+
};
|
|
9557
|
+
walk(this.sourceDefs);
|
|
9558
|
+
for (const st of this.columns.getState()) {
|
|
9559
|
+
const d = this.columns.getNode(st.colId)?.def;
|
|
9560
|
+
if (d) defs.push(d);
|
|
9561
|
+
}
|
|
9562
|
+
setFormulaScope(defs, this.formulaScope);
|
|
9563
|
+
}
|
|
7493
9564
|
collator() {
|
|
7494
9565
|
const code = this.store.state.locale.code;
|
|
7495
9566
|
if (this.collatorCache?.code !== code) {
|
|
@@ -7627,17 +9698,35 @@ var Grid = class {
|
|
|
7627
9698
|
isFlashing(rowId, colId) {
|
|
7628
9699
|
return this.flashing.size > 0 && this.flashing.has(`${rowId}\0${colId}`);
|
|
7629
9700
|
}
|
|
9701
|
+
/** Classes of a flashing cell ('' when it is not flashing). */
|
|
9702
|
+
flashClass(rowId, colId) {
|
|
9703
|
+
if (!this.flashing.size) return "";
|
|
9704
|
+
const f = this.flashing.get(`${rowId}\0${colId}`);
|
|
9705
|
+
return f ? f.dir ? ` fg-cell-flash fg-cell-flash-${f.dir}` : " fg-cell-flash" : "";
|
|
9706
|
+
}
|
|
9707
|
+
flashMs() {
|
|
9708
|
+
const d = this.options.flashDuration;
|
|
9709
|
+
return typeof d === "number" && d > 0 ? d : 1e3;
|
|
9710
|
+
}
|
|
7630
9711
|
flash(keys) {
|
|
7631
|
-
const
|
|
7632
|
-
|
|
9712
|
+
const ms = this.flashMs();
|
|
9713
|
+
const until = Date.now() + ms;
|
|
9714
|
+
for (const [k, dir] of keys) this.flashing.set(k, { until, dir });
|
|
7633
9715
|
if (this.flashTimer) return;
|
|
7634
9716
|
const tick = () => {
|
|
7635
9717
|
const now = Date.now();
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
9718
|
+
const ended = /* @__PURE__ */ new Set();
|
|
9719
|
+
let next = Infinity;
|
|
9720
|
+
for (const [k, f] of this.flashing) {
|
|
9721
|
+
if (f.until <= now) {
|
|
9722
|
+
this.flashing.delete(k);
|
|
9723
|
+
ended.add(k.slice(0, k.indexOf("\0")));
|
|
9724
|
+
} else next = Math.min(next, f.until);
|
|
9725
|
+
}
|
|
9726
|
+
if (ended.size) this.renderer.invalidateRowIds(ended);
|
|
9727
|
+
this.flashTimer = this.flashing.size && !this.destroyed ? window.setTimeout(tick, Math.max(16, next - now)) : 0;
|
|
7639
9728
|
};
|
|
7640
|
-
this.flashTimer = window.setTimeout(tick,
|
|
9729
|
+
this.flashTimer = window.setTimeout(tick, ms);
|
|
7641
9730
|
}
|
|
7642
9731
|
applyTransaction(tx) {
|
|
7643
9732
|
if (!tx || typeof tx !== "object") return { added: [], updated: [], removed: [], notFound: [] };
|
|
@@ -7652,32 +9741,55 @@ var Grid = class {
|
|
|
7652
9741
|
const f = this.columns.getNode(st.colId)?.def.field;
|
|
7653
9742
|
if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
|
|
7654
9743
|
}
|
|
7655
|
-
const flashKeys =
|
|
9744
|
+
const flashKeys = /* @__PURE__ */ new Map();
|
|
9745
|
+
const changed = { fields: /* @__PURE__ */ new Set(), ids: /* @__PURE__ */ new Set() };
|
|
7656
9746
|
let next = this.store.state.data;
|
|
7657
9747
|
const present = new Set(next);
|
|
7658
9748
|
const byId = new Map(next.map((r) => [this.rowModel.idOf(r), r]));
|
|
7659
9749
|
for (const tx of list) {
|
|
7660
|
-
if (tx && typeof tx === "object") next = this.txStep(tx, next, { present, byId }, result, flashKeys, colsByField);
|
|
9750
|
+
if (tx && typeof tx === "object") next = this.txStep(tx, next, { present, byId }, result, flashKeys, colsByField, changed);
|
|
7661
9751
|
}
|
|
7662
9752
|
if (!result.added.length && !result.updated.length && !result.removed.length) {
|
|
7663
9753
|
if (result.notFound.length) this.events.emit("transactionApplied", result);
|
|
7664
9754
|
return result;
|
|
7665
9755
|
}
|
|
7666
9756
|
this.editVersion++;
|
|
7667
|
-
this.
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
this.
|
|
7671
|
-
|
|
7672
|
-
|
|
9757
|
+
if (next === this.store.state.data && !this.viewReads(changed.fields)) {
|
|
9758
|
+
if (this.options.autoRowHeight || this.options.getRowHeight) this.renderer.invalidateHeights();
|
|
9759
|
+
this.refreshSummary();
|
|
9760
|
+
this.finder.refresh();
|
|
9761
|
+
if (this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan || c.def.formula || c.def.formulas) || this.formats.dependsOnOtherRows()) {
|
|
9762
|
+
this.renderer.invalidate({ rows: true });
|
|
9763
|
+
} else this.renderer.invalidateRowIds(changed.ids);
|
|
9764
|
+
} else {
|
|
9765
|
+
this.renderer.invalidateHeights();
|
|
9766
|
+
this.keepRange = true;
|
|
9767
|
+
try {
|
|
9768
|
+
this.replaceData(next === this.store.state.data ? [...next] : next);
|
|
9769
|
+
} finally {
|
|
9770
|
+
this.keepRange = false;
|
|
9771
|
+
}
|
|
7673
9772
|
}
|
|
7674
9773
|
this.editor.sync();
|
|
7675
|
-
if (flashKeys.
|
|
9774
|
+
if (flashKeys.size && this.options.flashCells !== false) this.flash(flashKeys);
|
|
7676
9775
|
this.events.emit("transactionApplied", result);
|
|
7677
9776
|
return result;
|
|
7678
9777
|
}
|
|
7679
9778
|
/** One transaction against `data` (remove → update → add); returns the new array. */
|
|
7680
|
-
|
|
9779
|
+
/** Whether rows changed in `fields` could filter, sort, group or move differently. */
|
|
9780
|
+
viewReads(fields) {
|
|
9781
|
+
if (!fields.size) return false;
|
|
9782
|
+
const st = this.store.state;
|
|
9783
|
+
if (this.pivotCfg || this.options.treeData || st.quickFilter || st.advancedFilter || this.groupBy.length) return true;
|
|
9784
|
+
const ids = [...st.sort.map((x) => x.colId), ...Object.keys(st.filters)];
|
|
9785
|
+
return ids.some((id) => {
|
|
9786
|
+
const def = this.columns.getNode(id)?.def;
|
|
9787
|
+
if (!def) return false;
|
|
9788
|
+
if (def.valueGetter || def.formulas || def.formula) return true;
|
|
9789
|
+
return def.field != null && fields.has(def.field);
|
|
9790
|
+
});
|
|
9791
|
+
}
|
|
9792
|
+
txStep(tx, data, index, result, flashKeys, colsByField, changed) {
|
|
7681
9793
|
const getId = this.options.getRowId;
|
|
7682
9794
|
const { present, byId } = index;
|
|
7683
9795
|
const keyOf = (r) => {
|
|
@@ -7702,6 +9814,20 @@ var Grid = class {
|
|
|
7702
9814
|
this.tracker.originals.delete(row);
|
|
7703
9815
|
} else if (!row) result.notFound.push(r);
|
|
7704
9816
|
}
|
|
9817
|
+
const update = (r, k, row) => {
|
|
9818
|
+
const orig = this.tracker.originals.get(row);
|
|
9819
|
+
for (const [field, value] of Object.entries(r)) {
|
|
9820
|
+
if (orig) orig[field] = value;
|
|
9821
|
+
const old = row[field];
|
|
9822
|
+
if (Object.is(old, value)) continue;
|
|
9823
|
+
row[field] = value;
|
|
9824
|
+
changed.fields.add(field);
|
|
9825
|
+
changed.ids.add(k);
|
|
9826
|
+
const dir = typeof old === "number" && typeof value === "number" ? value > old ? "up" : "down" : "";
|
|
9827
|
+
for (const colId of colsByField.get(field) ?? []) flashKeys.set(`${k}\0${colId}`, dir);
|
|
9828
|
+
}
|
|
9829
|
+
result.updated.push(row);
|
|
9830
|
+
};
|
|
7705
9831
|
for (const r of tx.update ?? []) {
|
|
7706
9832
|
const k = keyOf(r);
|
|
7707
9833
|
const row = k === null ? void 0 : byId.get(k);
|
|
@@ -7709,16 +9835,16 @@ var Grid = class {
|
|
|
7709
9835
|
result.notFound.push(r);
|
|
7710
9836
|
continue;
|
|
7711
9837
|
}
|
|
7712
|
-
|
|
7713
|
-
for (const [field, value] of Object.entries(r)) {
|
|
7714
|
-
if (orig) orig[field] = value;
|
|
7715
|
-
if (Object.is(row[field], value)) continue;
|
|
7716
|
-
row[field] = value;
|
|
7717
|
-
for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
|
|
7718
|
-
}
|
|
7719
|
-
result.updated.push(row);
|
|
9838
|
+
update(r, k, row);
|
|
7720
9839
|
}
|
|
7721
|
-
const
|
|
9840
|
+
const fresh = Array.isArray(tx.add) ? [...tx.add] : [];
|
|
9841
|
+
for (const r of Array.isArray(tx.upsert) ? tx.upsert : []) {
|
|
9842
|
+
const k = keyOf(r);
|
|
9843
|
+
const row = k === null ? void 0 : byId.get(k);
|
|
9844
|
+
if (row && !gone.has(row)) update(r, k, row);
|
|
9845
|
+
else if (r && typeof r === "object") fresh.push(r);
|
|
9846
|
+
}
|
|
9847
|
+
const added = cleanRows(fresh);
|
|
7722
9848
|
result.added.push(...added);
|
|
7723
9849
|
for (const r of added) {
|
|
7724
9850
|
present.add(r);
|
|
@@ -7734,13 +9860,16 @@ var Grid = class {
|
|
|
7734
9860
|
if (!tx || typeof tx !== "object") return;
|
|
7735
9861
|
this.txQueue.push(tx);
|
|
7736
9862
|
if (this.txTimer) return;
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
9863
|
+
const wait = this.options.asyncTransactionWait;
|
|
9864
|
+
this.txTimer = window.setTimeout(() => this.flushAsyncTransactions(), typeof wait === "number" && wait >= 0 ? wait : 50);
|
|
9865
|
+
}
|
|
9866
|
+
flushAsyncTransactions() {
|
|
9867
|
+
clearTimeout(this.txTimer);
|
|
9868
|
+
this.txTimer = 0;
|
|
9869
|
+
const queue = this.txQueue;
|
|
9870
|
+
this.txQueue = [];
|
|
9871
|
+
if (this.destroyed || !queue.length) return;
|
|
9872
|
+
this.applyTransactions(queue);
|
|
7744
9873
|
}
|
|
7745
9874
|
setAdvancedFilter(filter) {
|
|
7746
9875
|
const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
|
|
@@ -7769,6 +9898,7 @@ var Grid = class {
|
|
|
7769
9898
|
expanded: Object.fromEntries(expanded),
|
|
7770
9899
|
selected: [...s.selected]
|
|
7771
9900
|
};
|
|
9901
|
+
if (this.cfRules.length) state.conditionalFormats = this.getConditionalFormats();
|
|
7772
9902
|
if (this.options.pagination) {
|
|
7773
9903
|
state.page = this.page;
|
|
7774
9904
|
state.pageSize = this.pageSize;
|
|
@@ -7806,6 +9936,7 @@ var Grid = class {
|
|
|
7806
9936
|
}
|
|
7807
9937
|
if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
|
|
7808
9938
|
if (want("page") && typeof state.page === "number") this.setPage(state.page);
|
|
9939
|
+
if (want("conditionalFormats") && Array.isArray(state.conditionalFormats)) this.setConditionalFormats(state.conditionalFormats);
|
|
7809
9940
|
if (want("selected") && Array.isArray(state.selected)) {
|
|
7810
9941
|
const ids = new Set(this.rowModel.allNodes().map((n) => n.id));
|
|
7811
9942
|
this.setSelected(new Set(state.selected.map(String).filter((id) => ids.has(id))));
|
|
@@ -9055,6 +11186,7 @@ var Grid = class {
|
|
|
9055
11186
|
return;
|
|
9056
11187
|
}
|
|
9057
11188
|
this.columns.setDefs(this.withSelect(defs));
|
|
11189
|
+
this.bindFormulas();
|
|
9058
11190
|
const known = (id) => !!this.columns.getNode(id);
|
|
9059
11191
|
const groupBy = this.groupBy.filter(known);
|
|
9060
11192
|
if (groupBy.length !== this.groupBy.length) {
|
|
@@ -9132,6 +11264,12 @@ var Grid = class {
|
|
|
9132
11264
|
getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
|
|
9133
11265
|
print: guard((o) => printHtml(buildPrintHtml(this, o))),
|
|
9134
11266
|
getPrintHtml: guard((o) => buildPrintHtml(this, o)),
|
|
11267
|
+
getPdf: guard((o) => renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality })),
|
|
11268
|
+
exportPdf: guard((name = "export.pdf", o) => {
|
|
11269
|
+
const bytes = renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality });
|
|
11270
|
+
if (bytes) download(name, [bytes], "application/pdf");
|
|
11271
|
+
else warn("exportPdf: this browser cannot draw on a canvas");
|
|
11272
|
+
}),
|
|
9135
11273
|
setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
|
|
9136
11274
|
setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
|
|
9137
11275
|
getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
|
|
@@ -9139,6 +11277,12 @@ var Grid = class {
|
|
|
9139
11277
|
isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
|
|
9140
11278
|
applyTransaction: guard((tx) => this.applyTransaction(tx)),
|
|
9141
11279
|
applyTransactionAsync: guard((tx) => this.applyTransactionAsync(tx)),
|
|
11280
|
+
flushAsyncTransactions: guard(() => this.flushAsyncTransactions()),
|
|
11281
|
+
setConditionalFormats: guard((f) => this.setConditionalFormats(f)),
|
|
11282
|
+
getConditionalFormats: guard(() => this.getConditionalFormats()),
|
|
11283
|
+
openConditionalFormats: guard((colId) => this.formatDialog.open(colId)),
|
|
11284
|
+
askAssistant: guard((prompt) => askAssistant(this, prompt, this.options.assistant)),
|
|
11285
|
+
openAssistant: guard(() => this.assistantPanel.open()),
|
|
9142
11286
|
getState: guard(() => this.getState()),
|
|
9143
11287
|
setState: guard((s, o) => this.setState(s, o)),
|
|
9144
11288
|
getScrollPosition: guard(() => this.renderer.getScroll()),
|
|
@@ -9205,6 +11349,8 @@ var Grid = class {
|
|
|
9205
11349
|
getPivot: guard(() => this.getPivot()),
|
|
9206
11350
|
createChart: guard((o) => this.charts.create(o)),
|
|
9207
11351
|
getCsv: guard((o) => buildCsv(this, o)),
|
|
11352
|
+
getJson: guard((o) => buildJson(this, o)),
|
|
11353
|
+
exportJson: guard((name = "export.json", o) => download(name, [buildJson(this, o)], "application/json;charset=utf-8")),
|
|
9208
11354
|
exportCsv: guard((name = "export.csv", o) => download(name, ["\uFEFF", buildCsv(this, o)], "text/csv;charset=utf-8")),
|
|
9209
11355
|
getXlsx: guard((o) => buildXlsx(this, o)),
|
|
9210
11356
|
exportXlsx: guard((name = "export.xlsx", o) => download(name, [buildXlsx(this, o)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")),
|
|
@@ -9302,13 +11448,13 @@ function saveChanges(grid, url, options = {}) {
|
|
|
9302
11448
|
if (!grid.hasChanges()) return Promise.resolve(null);
|
|
9303
11449
|
const { created, updated, deleted } = grid.getModifiedRows();
|
|
9304
11450
|
const rows = new Map([...created, ...updated].map((r) => [r, { ...r }]));
|
|
9305
|
-
const
|
|
11451
|
+
const run2 = (async () => {
|
|
9306
11452
|
const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
|
|
9307
11453
|
grid.commitChanges({ rows, deleted });
|
|
9308
11454
|
return json;
|
|
9309
11455
|
})().finally(() => saving.delete(grid));
|
|
9310
|
-
saving.set(grid,
|
|
9311
|
-
return
|
|
11456
|
+
saving.set(grid, run2);
|
|
11457
|
+
return run2;
|
|
9312
11458
|
}
|
|
9313
11459
|
async function loadData(grid, url, options = {}) {
|
|
9314
11460
|
const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });
|
|
@@ -9320,15 +11466,186 @@ function csrfHeaders(doc = document) {
|
|
|
9320
11466
|
const header2 = doc.querySelector('meta[name="_csrf_header"]')?.content || "X-CSRF-TOKEN";
|
|
9321
11467
|
return token ? { [header2]: token } : {};
|
|
9322
11468
|
}
|
|
11469
|
+
|
|
11470
|
+
// src/live.ts
|
|
11471
|
+
var TX_KEYS = ["add", "update", "remove", "upsert"];
|
|
11472
|
+
function messageToTransaction(message) {
|
|
11473
|
+
let m = message;
|
|
11474
|
+
if (typeof m === "string") {
|
|
11475
|
+
if (!m.trim()) return null;
|
|
11476
|
+
m = JSON.parse(m);
|
|
11477
|
+
}
|
|
11478
|
+
if (Array.isArray(m)) return { upsert: m.filter((r) => r && typeof r === "object") };
|
|
11479
|
+
if (!m || typeof m !== "object") return null;
|
|
11480
|
+
const o = m;
|
|
11481
|
+
if (TX_KEYS.some((k) => Array.isArray(o[k]))) return o;
|
|
11482
|
+
if (Array.isArray(o.rows)) return { rows: o.rows };
|
|
11483
|
+
return null;
|
|
11484
|
+
}
|
|
11485
|
+
function connectLive(api, options) {
|
|
11486
|
+
const opts = options ?? {};
|
|
11487
|
+
let status = "connecting";
|
|
11488
|
+
let closed = false;
|
|
11489
|
+
let paused = false;
|
|
11490
|
+
let held = [];
|
|
11491
|
+
let socket = null;
|
|
11492
|
+
let timer = 0;
|
|
11493
|
+
let attempt = 0;
|
|
11494
|
+
const setStatus = (s) => {
|
|
11495
|
+
if (status === s) return;
|
|
11496
|
+
status = s;
|
|
11497
|
+
try {
|
|
11498
|
+
opts.onStatus?.(s);
|
|
11499
|
+
} catch {
|
|
11500
|
+
}
|
|
11501
|
+
};
|
|
11502
|
+
const apply = (tx) => {
|
|
11503
|
+
if ("rows" in tx && Array.isArray(tx.rows) && !TX_KEYS.some((k) => k in tx)) {
|
|
11504
|
+
api.flushAsyncTransactions();
|
|
11505
|
+
api.setData(tx.rows);
|
|
11506
|
+
} else if (opts.immediate) api.applyTransaction(tx);
|
|
11507
|
+
else api.applyTransactionAsync(tx);
|
|
11508
|
+
};
|
|
11509
|
+
const handle = (message) => {
|
|
11510
|
+
if (closed) return;
|
|
11511
|
+
let tx;
|
|
11512
|
+
try {
|
|
11513
|
+
tx = opts.parse ? opts.parse(message) : messageToTransaction(message);
|
|
11514
|
+
if (Array.isArray(tx)) tx = { upsert: tx };
|
|
11515
|
+
} catch (e) {
|
|
11516
|
+
if (opts.onError) opts.onError(e);
|
|
11517
|
+
else warn("connectLive: a message could not be read");
|
|
11518
|
+
return;
|
|
11519
|
+
}
|
|
11520
|
+
if (!tx || typeof tx !== "object") return;
|
|
11521
|
+
if (paused) held.push(tx);
|
|
11522
|
+
else apply(tx);
|
|
11523
|
+
};
|
|
11524
|
+
const retry = () => {
|
|
11525
|
+
if (closed || opts.reconnect === false) {
|
|
11526
|
+
setStatus("closed");
|
|
11527
|
+
return;
|
|
11528
|
+
}
|
|
11529
|
+
const max = typeof opts.maxDelay === "number" && opts.maxDelay > 0 ? opts.maxDelay : 3e4;
|
|
11530
|
+
const delay = Math.min(max, 500 * 2 ** Math.min(attempt++, 10)) * (0.8 + Math.random() * 0.4);
|
|
11531
|
+
setStatus("connecting");
|
|
11532
|
+
timer = window.setTimeout(open, delay);
|
|
11533
|
+
};
|
|
11534
|
+
const open = () => {
|
|
11535
|
+
if (closed) return;
|
|
11536
|
+
timer = 0;
|
|
11537
|
+
if (opts.poll) {
|
|
11538
|
+
void pollOnce();
|
|
11539
|
+
return;
|
|
11540
|
+
}
|
|
11541
|
+
const url = opts.url;
|
|
11542
|
+
if (!url) {
|
|
11543
|
+
warn("connectLive: give a url or a poll function");
|
|
11544
|
+
setStatus("error");
|
|
11545
|
+
return;
|
|
11546
|
+
}
|
|
11547
|
+
setStatus(paused ? "paused" : "connecting");
|
|
11548
|
+
try {
|
|
11549
|
+
if (/^wss?:/i.test(url)) {
|
|
11550
|
+
if (typeof WebSocket === "undefined") throw new Error("no WebSocket");
|
|
11551
|
+
const ws = new WebSocket(url, opts.protocols);
|
|
11552
|
+
socket = ws;
|
|
11553
|
+
ws.onopen = () => {
|
|
11554
|
+
attempt = 0;
|
|
11555
|
+
setStatus(paused ? "paused" : "open");
|
|
11556
|
+
};
|
|
11557
|
+
ws.onmessage = (e) => handle(e.data);
|
|
11558
|
+
ws.onerror = () => setStatus("error");
|
|
11559
|
+
ws.onclose = () => {
|
|
11560
|
+
if (socket === ws) socket = null;
|
|
11561
|
+
if (!closed) retry();
|
|
11562
|
+
};
|
|
11563
|
+
} else {
|
|
11564
|
+
if (typeof EventSource === "undefined") throw new Error("no EventSource");
|
|
11565
|
+
const es = new EventSource(url, { withCredentials: !!opts.withCredentials });
|
|
11566
|
+
socket = es;
|
|
11567
|
+
es.onopen = () => {
|
|
11568
|
+
attempt = 0;
|
|
11569
|
+
setStatus(paused ? "paused" : "open");
|
|
11570
|
+
};
|
|
11571
|
+
es.addEventListener(opts.event || "message", (e) => handle(e.data));
|
|
11572
|
+
es.onerror = () => {
|
|
11573
|
+
if (es.readyState === 2) {
|
|
11574
|
+
if (socket === es) socket = null;
|
|
11575
|
+
if (!closed) retry();
|
|
11576
|
+
} else setStatus("connecting");
|
|
11577
|
+
};
|
|
11578
|
+
}
|
|
11579
|
+
} catch {
|
|
11580
|
+
setStatus("error");
|
|
11581
|
+
retry();
|
|
11582
|
+
}
|
|
11583
|
+
};
|
|
11584
|
+
const pollOnce = async () => {
|
|
11585
|
+
if (closed) return;
|
|
11586
|
+
try {
|
|
11587
|
+
const result = await opts.poll();
|
|
11588
|
+
if (closed) return;
|
|
11589
|
+
attempt = 0;
|
|
11590
|
+
setStatus(paused ? "paused" : "open");
|
|
11591
|
+
handle(result);
|
|
11592
|
+
const every = typeof opts.interval === "number" && opts.interval > 0 ? opts.interval : 2e3;
|
|
11593
|
+
timer = window.setTimeout(pollOnce, every);
|
|
11594
|
+
} catch {
|
|
11595
|
+
if (closed) return;
|
|
11596
|
+
setStatus("error");
|
|
11597
|
+
retry();
|
|
11598
|
+
}
|
|
11599
|
+
};
|
|
11600
|
+
open();
|
|
11601
|
+
return {
|
|
11602
|
+
get status() {
|
|
11603
|
+
return status;
|
|
11604
|
+
},
|
|
11605
|
+
pause() {
|
|
11606
|
+
if (closed || paused) return;
|
|
11607
|
+
paused = true;
|
|
11608
|
+
setStatus("paused");
|
|
11609
|
+
},
|
|
11610
|
+
resume() {
|
|
11611
|
+
if (closed || !paused) return;
|
|
11612
|
+
paused = false;
|
|
11613
|
+
const list = held;
|
|
11614
|
+
held = [];
|
|
11615
|
+
for (const tx of list) apply(tx);
|
|
11616
|
+
setStatus(socket || opts.poll ? "open" : "connecting");
|
|
11617
|
+
},
|
|
11618
|
+
close() {
|
|
11619
|
+
if (closed) return;
|
|
11620
|
+
closed = true;
|
|
11621
|
+
clearTimeout(timer);
|
|
11622
|
+
socket?.close();
|
|
11623
|
+
socket = null;
|
|
11624
|
+
held = [];
|
|
11625
|
+
setStatus("closed");
|
|
11626
|
+
},
|
|
11627
|
+
send(data) {
|
|
11628
|
+
const ws = socket;
|
|
11629
|
+
if (!ws || typeof ws.send !== "function" || ws.readyState !== 1) return false;
|
|
11630
|
+
ws.send(data);
|
|
11631
|
+
return true;
|
|
11632
|
+
}
|
|
11633
|
+
};
|
|
11634
|
+
}
|
|
9323
11635
|
export {
|
|
9324
11636
|
buildWorkbook,
|
|
11637
|
+
connectLive,
|
|
9325
11638
|
createGrid,
|
|
9326
11639
|
csrfHeaders,
|
|
9327
11640
|
en_default as en,
|
|
11641
|
+
evaluateFormula,
|
|
9328
11642
|
exportWorkbook,
|
|
11643
|
+
formulaFunctions,
|
|
9329
11644
|
loadData,
|
|
11645
|
+
messageToTransaction,
|
|
9330
11646
|
parseCsv,
|
|
9331
11647
|
readXlsx,
|
|
11648
|
+
registerFormulaFunction,
|
|
9332
11649
|
registerLocale,
|
|
9333
11650
|
renderChart,
|
|
9334
11651
|
resolveLocale,
|