@freegridjs/core 1.1.0 → 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 +33 -12
- package/dist/cdn/freegrid.min.js +33 -12
- package/dist/index.d.ts +71 -4
- package/dist/index.js +2417 -239
- package/dist/locales/de.d.ts +1 -1
- package/dist/locales/de.js +16 -0
- package/dist/locales/en.d.ts +1 -1
- package/dist/locales/en.js +16 -0
- package/dist/locales/es.d.ts +1 -1
- package/dist/locales/es.js +16 -0
- package/dist/locales/fr.d.ts +1 -1
- package/dist/locales/fr.js +16 -0
- package/dist/locales/id.d.ts +1 -1
- package/dist/locales/id.js +16 -0
- package/dist/locales/it.d.ts +1 -1
- package/dist/locales/it.js +16 -0
- package/dist/locales/ja.d.ts +1 -1
- package/dist/locales/ja.js +16 -0
- package/dist/locales/ko.d.ts +1 -1
- package/dist/locales/ko.js +16 -0
- package/dist/locales/pt-BR.d.ts +1 -1
- package/dist/locales/pt-BR.js +16 -0
- package/dist/locales/ru.d.ts +1 -1
- package/dist/locales/ru.js +16 -0
- package/dist/locales/th.d.ts +1 -1
- package/dist/locales/th.js +16 -0
- package/dist/locales/vi.d.ts +1 -1
- package/dist/locales/vi.js +16 -0
- package/dist/locales/zh-CN.d.ts +1 -1
- package/dist/locales/zh-CN.js +16 -0
- package/dist/locales/zh-TW.d.ts +1 -1
- package/dist/locales/zh-TW.js +16 -0
- package/dist/styles.css +51 -2
- package/dist/{types-BDvXVuCe.d.ts → types-DPOWDM8R.d.ts} +255 -1
- 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})`;
|
|
@@ -2599,6 +3131,22 @@ var en = {
|
|
|
2599
3131
|
chartCombo: "Bar and line chart",
|
|
2600
3132
|
chartScatter: "Scatter chart",
|
|
2601
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
|
+
},
|
|
2602
3150
|
ops: {
|
|
2603
3151
|
contains: "Contains",
|
|
2604
3152
|
equals: "Equals",
|
|
@@ -2703,9 +3251,9 @@ function nextFocus(key, mods, p, s) {
|
|
|
2703
3251
|
case "End":
|
|
2704
3252
|
return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
|
|
2705
3253
|
case "Tab": {
|
|
2706
|
-
const
|
|
2707
|
-
if (
|
|
2708
|
-
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 };
|
|
2709
3257
|
}
|
|
2710
3258
|
default:
|
|
2711
3259
|
return null;
|
|
@@ -2925,7 +3473,7 @@ function attachBody(grid) {
|
|
|
2925
3473
|
const box = under?.getBoundingClientRect();
|
|
2926
3474
|
let target = null;
|
|
2927
3475
|
let position = "after";
|
|
2928
|
-
let
|
|
3476
|
+
let line2;
|
|
2929
3477
|
if (under && box) {
|
|
2930
3478
|
const over = into.rowModel.getRow(Number(under.dataset.row));
|
|
2931
3479
|
if (!over || over.kind === "group") return;
|
|
@@ -2934,17 +3482,17 @@ function attachBody(grid) {
|
|
|
2934
3482
|
if (over.kind === "detail") position = "after";
|
|
2935
3483
|
else if (into.options.treeData) position = y < 0.25 ? "before" : y > 0.75 ? "after" : "inside";
|
|
2936
3484
|
else position = y < 0.5 ? "before" : "after";
|
|
2937
|
-
|
|
3485
|
+
line2 = position === "before" ? box.top : box.bottom;
|
|
2938
3486
|
} else {
|
|
2939
3487
|
if (!point.closest(".fg-body")) return;
|
|
2940
3488
|
const rows = intoRoot.querySelectorAll(".fg-body-pane-center .fg-row[data-row]");
|
|
2941
|
-
|
|
3489
|
+
line2 = rows.length ? Math.max(...[...rows].map((r) => r.getBoundingClientRect().bottom)) : intoRoot.querySelector(".fg-body").getBoundingClientRect().top;
|
|
2942
3490
|
}
|
|
2943
3491
|
if (!into.canDropRow(row, target, position, grid)) return;
|
|
2944
3492
|
drop = { into, target, position };
|
|
2945
3493
|
const top = intoRoot.getBoundingClientRect().top;
|
|
2946
3494
|
marker.classList.toggle("fg-row-drop-inside", position === "inside");
|
|
2947
|
-
marker.style.top = `${position === "inside" ? box.top - top :
|
|
3495
|
+
marker.style.top = `${position === "inside" ? box.top - top : line2 - top - 1}px`;
|
|
2948
3496
|
marker.style.height = position === "inside" ? `${box.height}px` : "";
|
|
2949
3497
|
intoRoot.append(marker);
|
|
2950
3498
|
};
|
|
@@ -3528,6 +4076,7 @@ var SideBar = class {
|
|
|
3528
4076
|
this.grid.renderer.invalidate({ header: true, rows: true });
|
|
3529
4077
|
}
|
|
3530
4078
|
syncTabs() {
|
|
4079
|
+
this.el.classList.toggle("fg-sidebar-open", this.isOpen);
|
|
3531
4080
|
for (const [p, tab] of this.tabs) {
|
|
3532
4081
|
const on = this.isOpen && p === this.current;
|
|
3533
4082
|
tab.setAttribute("aria-expanded", String(on));
|
|
@@ -3641,7 +4190,10 @@ var ContextMenu = class {
|
|
|
3641
4190
|
items.push(
|
|
3642
4191
|
"separator",
|
|
3643
4192
|
{ label: loc.exportCsv, action: () => grid.api.exportCsv() },
|
|
3644
|
-
{ 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() }
|
|
3645
4197
|
);
|
|
3646
4198
|
if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
|
|
3647
4199
|
items.push("separator", { label: loc.print, action: () => grid.api.print() });
|
|
@@ -3667,11 +4219,11 @@ var ContextMenu = class {
|
|
|
3667
4219
|
value: node && col && !isSystemCol(colId) ? getCellValue(col.def, node.data) : void 0,
|
|
3668
4220
|
defaultItems: defaults
|
|
3669
4221
|
};
|
|
3670
|
-
const
|
|
4222
|
+
const custom2 = grid.options.contextMenu;
|
|
3671
4223
|
let items = defaults;
|
|
3672
|
-
if (typeof
|
|
4224
|
+
if (typeof custom2 === "function") {
|
|
3673
4225
|
try {
|
|
3674
|
-
items =
|
|
4226
|
+
items = custom2(params);
|
|
3675
4227
|
} catch {
|
|
3676
4228
|
items = defaults;
|
|
3677
4229
|
}
|
|
@@ -3931,10 +4483,10 @@ var Finder = class {
|
|
|
3931
4483
|
const bar = el("div", "fg-findbar", { role: "search" });
|
|
3932
4484
|
const input = el("input", "fg-input", { type: "search", placeholder: loc.findPlaceholder, "aria-label": loc.findPlaceholder });
|
|
3933
4485
|
this.countEl = el("span", "fg-findbar-count", { "aria-live": "polite" });
|
|
3934
|
-
const btn = (label, text2,
|
|
4486
|
+
const btn = (label, text2, run2) => {
|
|
3935
4487
|
const b = el("button", "fg-findbar-btn", { type: "button", "aria-label": label, title: label });
|
|
3936
4488
|
b.textContent = text2;
|
|
3937
|
-
b.addEventListener("click",
|
|
4489
|
+
b.addEventListener("click", run2);
|
|
3938
4490
|
return b;
|
|
3939
4491
|
};
|
|
3940
4492
|
bar.append(
|
|
@@ -3970,7 +4522,7 @@ var Finder = class {
|
|
|
3970
4522
|
};
|
|
3971
4523
|
|
|
3972
4524
|
// src/fill.ts
|
|
3973
|
-
var
|
|
4525
|
+
var DAY2 = 864e5;
|
|
3974
4526
|
function fillSeries(source, count) {
|
|
3975
4527
|
if (!source.length || count <= 0) return [];
|
|
3976
4528
|
const repeat = () => Array.from({ length: count }, (_, i) => source[i % source.length]);
|
|
@@ -3982,9 +4534,9 @@ function fillSeries(source, count) {
|
|
|
3982
4534
|
const last2 = nums[nums.length - 1];
|
|
3983
4535
|
return Array.from({ length: count }, (_, i) => Math.round((last2 + step * (i + 1)) * 1e12) / 1e12);
|
|
3984
4536
|
}
|
|
3985
|
-
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);
|
|
3986
4538
|
if (dates.every(Boolean) && source.length >= 2) {
|
|
3987
|
-
const times = dates.map((d) => Math.round(d.getTime() /
|
|
4539
|
+
const times = dates.map((d) => Math.round(d.getTime() / DAY2));
|
|
3988
4540
|
const step = times[1] - times[0];
|
|
3989
4541
|
if (times.every((t, i) => i === 0 || t - times[i - 1] === step)) {
|
|
3990
4542
|
const lastDate = dates[dates.length - 1];
|
|
@@ -4119,7 +4671,7 @@ function parseDate(text2, previous) {
|
|
|
4119
4671
|
let m = /^(\d{4})\s*[-./년]\s*(\d{1,2})\s*[-./월]\s*(\d{1,2})\s*[.일]?$/.exec(s);
|
|
4120
4672
|
if (m) date = validDate(+m[1], +m[2], +m[3]);
|
|
4121
4673
|
else if (m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s)) date = validDate(+m[3], +m[1], +m[2]);
|
|
4122
|
-
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);
|
|
4123
4675
|
if (!date) return { ok: false };
|
|
4124
4676
|
return { ok: true, value: previous instanceof Date ? date : toDateKey2(date) };
|
|
4125
4677
|
}
|
|
@@ -4143,16 +4695,16 @@ function parseValue(def, text2, previous) {
|
|
|
4143
4695
|
|
|
4144
4696
|
// src/editing/editor.ts
|
|
4145
4697
|
var normalizeOptions = (opts) => opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o), disabled: false });
|
|
4146
|
-
var
|
|
4698
|
+
var pad3 = (n) => String(n).padStart(2, "0");
|
|
4147
4699
|
function localDateTime(v) {
|
|
4148
4700
|
const d = v instanceof Date ? v : typeof v === "string" && v ? new Date(v) : null;
|
|
4149
4701
|
if (!d || Number.isNaN(d.getTime())) return "";
|
|
4150
|
-
return `${d.getFullYear()}-${
|
|
4702
|
+
return `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}T${pad3(d.getHours())}:${pad3(d.getMinutes())}`;
|
|
4151
4703
|
}
|
|
4152
4704
|
function timeOf(v) {
|
|
4153
|
-
if (v instanceof Date) return `${
|
|
4705
|
+
if (v instanceof Date) return `${pad3(v.getHours())}:${pad3(v.getMinutes())}`;
|
|
4154
4706
|
const m = typeof v === "string" ? /(\d{1,2}):(\d{2})/.exec(v) : null;
|
|
4155
|
-
return m ? `${
|
|
4707
|
+
return m ? `${pad3(Number(m[1]))}:${m[2]}` : "";
|
|
4156
4708
|
}
|
|
4157
4709
|
var CellEditor = class {
|
|
4158
4710
|
constructor(grid) {
|
|
@@ -4227,20 +4779,20 @@ var CellEditor = class {
|
|
|
4227
4779
|
grid.setFocus(rowIndex, colId);
|
|
4228
4780
|
renderer.ensureVisible(rowIndex, colId);
|
|
4229
4781
|
renderer.render();
|
|
4230
|
-
const
|
|
4231
|
-
this.place(
|
|
4782
|
+
const wrap2 = el("div", "fg-editor");
|
|
4783
|
+
this.place(wrap2, rowIndex, colId);
|
|
4232
4784
|
let control;
|
|
4233
4785
|
let read;
|
|
4234
4786
|
let dispose = () => {
|
|
4235
4787
|
};
|
|
4236
4788
|
const spec = def.editor;
|
|
4237
4789
|
if (typeof spec === "function") {
|
|
4238
|
-
const
|
|
4239
|
-
control =
|
|
4240
|
-
read = () => ({ value:
|
|
4241
|
-
dispose = () =>
|
|
4242
|
-
|
|
4243
|
-
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?.());
|
|
4244
4796
|
} else if (spec?.type === "select") {
|
|
4245
4797
|
const options = normalizeOptions(typeof spec.options === "function" ? spec.options(row) : spec.options);
|
|
4246
4798
|
const select = el("select", "fg-editor-control");
|
|
@@ -4254,7 +4806,7 @@ var CellEditor = class {
|
|
|
4254
4806
|
select.selectedIndex = Math.max(0, options.findIndex((o) => Object.is(o.value, value)));
|
|
4255
4807
|
control = select;
|
|
4256
4808
|
read = () => ({ value: options[select.selectedIndex]?.value ?? null });
|
|
4257
|
-
|
|
4809
|
+
wrap2.append(select);
|
|
4258
4810
|
} else if (spec?.type === "time" || spec?.type === "datetime" || !spec && def.type === "date" && def.format === "datetime") {
|
|
4259
4811
|
const isTime = spec?.type === "time";
|
|
4260
4812
|
const input = el("input", "fg-editor-control", { type: isTime ? "time" : "datetime-local" });
|
|
@@ -4265,7 +4817,7 @@ var CellEditor = class {
|
|
|
4265
4817
|
if (isTime) return { value: input.value };
|
|
4266
4818
|
return { value: value instanceof Date ? new Date(input.value) : input.value };
|
|
4267
4819
|
};
|
|
4268
|
-
|
|
4820
|
+
wrap2.append(input);
|
|
4269
4821
|
} else {
|
|
4270
4822
|
const textarea = spec?.type === "textarea";
|
|
4271
4823
|
const input = textarea ? el("textarea", "fg-editor-control") : el("input", "fg-editor-control");
|
|
@@ -4277,11 +4829,11 @@ var CellEditor = class {
|
|
|
4277
4829
|
input.value = initialText ?? start;
|
|
4278
4830
|
control = input;
|
|
4279
4831
|
read = () => ({ text: input.value });
|
|
4280
|
-
|
|
4832
|
+
wrap2.append(input);
|
|
4281
4833
|
}
|
|
4282
4834
|
control.setAttribute("aria-label", def.header ?? def.field ?? colId);
|
|
4283
4835
|
const msg = el("div", "fg-editor-msg", { role: "alert" });
|
|
4284
|
-
|
|
4836
|
+
wrap2.append(msg);
|
|
4285
4837
|
const onKey = (e) => {
|
|
4286
4838
|
e.stopPropagation();
|
|
4287
4839
|
if (e.key === "Escape") {
|
|
@@ -4297,26 +4849,26 @@ var CellEditor = class {
|
|
|
4297
4849
|
}
|
|
4298
4850
|
};
|
|
4299
4851
|
const onFocusOut = (e) => {
|
|
4300
|
-
if (this.cur?.wrap ===
|
|
4852
|
+
if (this.cur?.wrap === wrap2 && !wrap2.contains(e.relatedTarget)) this.commit(null);
|
|
4301
4853
|
};
|
|
4302
4854
|
const onScroll = () => this.commit(null);
|
|
4303
|
-
|
|
4304
|
-
|
|
4855
|
+
wrap2.addEventListener("keydown", onKey);
|
|
4856
|
+
wrap2.addEventListener("focusout", onFocusOut);
|
|
4305
4857
|
renderer.scroller.addEventListener("scroll", onScroll, { passive: true });
|
|
4306
|
-
renderer.root.append(
|
|
4858
|
+
renderer.root.append(wrap2);
|
|
4307
4859
|
this.cur = {
|
|
4308
4860
|
rowIndex,
|
|
4309
4861
|
colId,
|
|
4310
4862
|
row,
|
|
4311
4863
|
def,
|
|
4312
|
-
wrap,
|
|
4864
|
+
wrap: wrap2,
|
|
4313
4865
|
read,
|
|
4314
4866
|
dispose: () => {
|
|
4315
|
-
|
|
4316
|
-
|
|
4867
|
+
wrap2.removeEventListener("keydown", onKey);
|
|
4868
|
+
wrap2.removeEventListener("focusout", onFocusOut);
|
|
4317
4869
|
renderer.scroller.removeEventListener("scroll", onScroll);
|
|
4318
4870
|
dispose();
|
|
4319
|
-
|
|
4871
|
+
wrap2.remove();
|
|
4320
4872
|
}
|
|
4321
4873
|
};
|
|
4322
4874
|
if (typeof spec !== "function") {
|
|
@@ -4327,15 +4879,15 @@ var CellEditor = class {
|
|
|
4327
4879
|
return true;
|
|
4328
4880
|
}
|
|
4329
4881
|
/** Put the editor over its cell; false when the cell is not rendered. */
|
|
4330
|
-
place(
|
|
4882
|
+
place(wrap2, rowIndex, colId) {
|
|
4331
4883
|
const { renderer } = this.grid;
|
|
4332
4884
|
const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
|
|
4333
4885
|
const rootBox = renderer.root.getBoundingClientRect();
|
|
4334
4886
|
const box = cellEl?.getBoundingClientRect();
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
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`;
|
|
4339
4891
|
return !!cellEl;
|
|
4340
4892
|
}
|
|
4341
4893
|
/** Display index of a row object, or -1 when it is no longer shown. */
|
|
@@ -4679,6 +5231,610 @@ function attachHeader(grid) {
|
|
|
4679
5231
|
};
|
|
4680
5232
|
}
|
|
4681
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
|
+
|
|
4682
5838
|
// src/rows/sort.ts
|
|
4683
5839
|
function compareNonBlank(a, b, collator) {
|
|
4684
5840
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
@@ -4692,7 +5848,7 @@ function normalize(def, v) {
|
|
|
4692
5848
|
const n = typeof v === "number" ? v : Number(v);
|
|
4693
5849
|
return Number.isNaN(n) ? null : n;
|
|
4694
5850
|
}
|
|
4695
|
-
if (def.type === "date") return
|
|
5851
|
+
if (def.type === "date") return toDate2(v)?.getTime() ?? null;
|
|
4696
5852
|
return v;
|
|
4697
5853
|
}
|
|
4698
5854
|
function compareValues(a, b, collator) {
|
|
@@ -4743,8 +5899,8 @@ function toCsv(rows, sep = ",") {
|
|
|
4743
5899
|
return rows.map((r) => r.map(cell).join(sep)).join("\r\n");
|
|
4744
5900
|
}
|
|
4745
5901
|
function detectSeparator(text2) {
|
|
4746
|
-
const
|
|
4747
|
-
const count = (ch) =>
|
|
5902
|
+
const line2 = text2.slice(0, text2.search(/\r?\n|$/));
|
|
5903
|
+
const count = (ch) => line2.split(ch).length - 1;
|
|
4748
5904
|
const tabs = count(" "), semis = count(";"), commas = count(",");
|
|
4749
5905
|
if (tabs && tabs >= commas && tabs >= semis) return " ";
|
|
4750
5906
|
if (semis > commas) return ";";
|
|
@@ -4901,12 +6057,12 @@ function colName(i) {
|
|
|
4901
6057
|
return s;
|
|
4902
6058
|
}
|
|
4903
6059
|
var EPOCH = Date.UTC(1899, 11, 30);
|
|
4904
|
-
var
|
|
6060
|
+
var DAY3 = 864e5;
|
|
4905
6061
|
function toSerial(d) {
|
|
4906
|
-
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;
|
|
4907
6063
|
}
|
|
4908
6064
|
function fromSerial(n) {
|
|
4909
|
-
const u = new Date(Math.round(n *
|
|
6065
|
+
const u = new Date(Math.round(n * DAY3) + EPOCH);
|
|
4910
6066
|
return new Date(u.getUTCFullYear(), u.getUTCMonth(), u.getUTCDate(), u.getUTCHours(), u.getUTCMinutes(), u.getUTCSeconds());
|
|
4911
6067
|
}
|
|
4912
6068
|
var BAD_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g;
|
|
@@ -4938,7 +6094,7 @@ function writeXlsx(sheets) {
|
|
|
4938
6094
|
];
|
|
4939
6095
|
const fillIndex = /* @__PURE__ */ new Map([["FFF2F4F7", 2]]);
|
|
4940
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()}` : "";
|
|
4941
|
-
const
|
|
6097
|
+
const fontOf2 = (bold, color) => {
|
|
4942
6098
|
const key = `${bold ? 1 : 0}|${color}`;
|
|
4943
6099
|
let i = fontIndex.get(key);
|
|
4944
6100
|
if (i === void 0) {
|
|
@@ -4972,7 +6128,7 @@ function writeXlsx(sheets) {
|
|
|
4972
6128
|
customFmts.push(fmt);
|
|
4973
6129
|
fmtIds.set(fmt, id);
|
|
4974
6130
|
}
|
|
4975
|
-
const fontId =
|
|
6131
|
+
const fontId = fontOf2(bold, color);
|
|
4976
6132
|
const fillId = fillOf(fill);
|
|
4977
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"' : "");
|
|
4978
6134
|
i = xfs.length;
|
|
@@ -5025,9 +6181,13 @@ function writeXlsx(sheets) {
|
|
|
5025
6181
|
},
|
|
5026
6182
|
{
|
|
5027
6183
|
name: "xl/workbook.xml",
|
|
5028
|
-
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) => {
|
|
5029
6185
|
const w = Math.max(0, ...s.rows.map((r) => r.length));
|
|
5030
|
-
|
|
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;
|
|
5031
6191
|
}).join("")}</definedNames>` : "") + "</workbook>"
|
|
5032
6192
|
},
|
|
5033
6193
|
{
|
|
@@ -5071,11 +6231,11 @@ async function readXlsx(input) {
|
|
|
5071
6231
|
const shared = parseXml(files.get("xl/sharedStrings.xml"));
|
|
5072
6232
|
const strings = shared ? tags(shared, "si").map((si) => tags(si, "t").map((t) => t.textContent ?? "").join("")) : [];
|
|
5073
6233
|
const stylesDoc = parseXml(files.get("xl/styles.xml"));
|
|
5074
|
-
const
|
|
6234
|
+
const custom2 = new Map(stylesDoc ? tags(stylesDoc, "numFmt").map((f) => [Number(f.getAttribute("numFmtId")), f.getAttribute("formatCode") ?? ""]) : []);
|
|
5075
6235
|
const cellXfs = stylesDoc ? tags(stylesDoc, "cellXfs")[0] : void 0;
|
|
5076
6236
|
const dateStyle = (cellXfs ? [...cellXfs.children].filter((x) => x.localName === "xf") : []).map((xf) => {
|
|
5077
6237
|
const id = Number(xf.getAttribute("numFmtId") ?? 0);
|
|
5078
|
-
return BUILTIN_DATE_FMTS.has(id) ||
|
|
6238
|
+
return BUILTIN_DATE_FMTS.has(id) || custom2.has(id) && isDateFormat(custom2.get(id));
|
|
5079
6239
|
});
|
|
5080
6240
|
const sheets = [];
|
|
5081
6241
|
for (const s of tags(workbook, "sheet")) {
|
|
@@ -5193,9 +6353,11 @@ tr { break-inside: avoid; page-break-inside: avoid; }
|
|
|
5193
6353
|
.num { text-align: right; color: #9ca3af; }
|
|
5194
6354
|
tr.group td { background: #eef2f7; font-weight: 600; border-top: 1px solid #cbd5e1; break-after: avoid; page-break-after: avoid; }
|
|
5195
6355
|
tr.group.level-0 td { background: #e2e8f0; }
|
|
6356
|
+
tr.title td { border: 0; padding: 0 0 4px; background: none; }
|
|
5196
6357
|
tr.total td { font-weight: 700; background: #f3f4f6; border-top: 1.5px solid #111827; border-bottom: 0; }
|
|
5197
6358
|
`;
|
|
5198
|
-
|
|
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 = {}) {
|
|
5199
6361
|
const cols = exportColumns(grid, opts.columns);
|
|
5200
6362
|
const loc = grid.store.state.locale;
|
|
5201
6363
|
const code = loc.code;
|
|
@@ -5210,23 +6372,24 @@ function buildPrintHtml(grid, opts = {}) {
|
|
|
5210
6372
|
});
|
|
5211
6373
|
const depth = Math.max(1, ...chains.map((ch) => ch.length));
|
|
5212
6374
|
const head = [];
|
|
5213
|
-
|
|
5214
|
-
let
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
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
|
+
}
|
|
5230
6393
|
}
|
|
5231
6394
|
const aggText = (c, i, v, agg) => {
|
|
5232
6395
|
if (v == null || v === "") return "";
|
|
@@ -5234,64 +6397,75 @@ function buildPrintHtml(grid, opts = {}) {
|
|
|
5234
6397
|
const type = c.def.type ?? (typeof v === "number" ? "number" : v instanceof Date ? "date" : void 0);
|
|
5235
6398
|
return type === c.def.type ? fmts[i](v) : getFormatter({ ...c.def, type }, code)(v);
|
|
5236
6399
|
};
|
|
5237
|
-
const labelled = (
|
|
6400
|
+
const labelled = (kind, label, values, level = 0) => {
|
|
5238
6401
|
let lead = 0;
|
|
5239
6402
|
while (lead < cols.length - 1 && !values[lead]) lead++;
|
|
5240
6403
|
lead = Math.max(1, lead);
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
for (let i = lead; i < cols.length; i++)
|
|
5244
|
-
return
|
|
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 };
|
|
5245
6408
|
};
|
|
5246
6409
|
const rows = exportRows(grid, opts.rows);
|
|
5247
6410
|
const groupBy = opts.groups !== false && !grid.rowModel.server && !grid.options.treeData ? grid.getGroupBy() : [];
|
|
5248
6411
|
const probe = opts.styles !== false && cols.some((c) => c.def.cellClass) ? classStyles(grid) : null;
|
|
5249
|
-
|
|
6412
|
+
const body = [];
|
|
5250
6413
|
try {
|
|
5251
|
-
for (const
|
|
5252
|
-
if (
|
|
5253
|
-
const def = grid.columns.getNode(
|
|
5254
|
-
const value = isBlank(
|
|
5255
|
-
const label = `${def ? header(def,
|
|
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)})`;
|
|
5256
6419
|
const values = cols.map((c, i) => {
|
|
5257
6420
|
if (!c.def.aggFunc || groupBy.includes(c.id)) return void 0;
|
|
5258
6421
|
try {
|
|
5259
|
-
return aggText(c, i, aggregate(c.def.aggFunc,
|
|
6422
|
+
return aggText(c, i, aggregate(c.def.aggFunc, line2.rows.map((r2) => getCellValue(c.def, r2)), line2.rows), c.def.aggFunc) || void 0;
|
|
5260
6423
|
} catch {
|
|
5261
6424
|
return void 0;
|
|
5262
6425
|
}
|
|
5263
6426
|
});
|
|
5264
|
-
body
|
|
6427
|
+
body.push(labelled("group", label, values, line2.level));
|
|
5265
6428
|
continue;
|
|
5266
6429
|
}
|
|
5267
|
-
const r =
|
|
5268
|
-
|
|
6430
|
+
const r = line2.row;
|
|
6431
|
+
const cells = numbered ? [{ text: String(line2.n), align: "right", num: true }] : [];
|
|
5269
6432
|
cols.forEach((c, i) => {
|
|
5270
6433
|
const v = getCellValue(c.def, r);
|
|
5271
6434
|
const text2 = isBlank(v) ? "" : fmts[i](v);
|
|
5272
|
-
|
|
6435
|
+
const cell = { text: text2, align: align(c.def) };
|
|
5273
6436
|
if (probe && c.def.cellClass) {
|
|
5274
6437
|
let cls;
|
|
5275
6438
|
try {
|
|
5276
|
-
cls = typeof c.def.cellClass === "function" ? c.def.cellClass({ value: v, formatted: text2, row: r, rowIndex:
|
|
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;
|
|
5277
6440
|
} catch {
|
|
5278
6441
|
cls = void 0;
|
|
5279
6442
|
}
|
|
5280
6443
|
if (cls) {
|
|
5281
6444
|
const st = probe.of(cls);
|
|
5282
|
-
if (st.color)
|
|
5283
|
-
if (st.fill)
|
|
5284
|
-
if (st.bold)
|
|
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;
|
|
5285
6448
|
}
|
|
5286
6449
|
}
|
|
5287
|
-
|
|
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);
|
|
5288
6462
|
});
|
|
5289
|
-
body
|
|
6463
|
+
body.push({ kind: "row", cells });
|
|
5290
6464
|
}
|
|
5291
6465
|
} finally {
|
|
5292
6466
|
probe?.dispose();
|
|
5293
6467
|
}
|
|
5294
|
-
|
|
6468
|
+
const foot = [];
|
|
5295
6469
|
const summary = opts.summary !== false ? grid.summary() : null;
|
|
5296
6470
|
if (summary) {
|
|
5297
6471
|
summary.opts.rows.forEach((def, ri) => {
|
|
@@ -5299,29 +6473,89 @@ function buildPrintHtml(grid, opts = {}) {
|
|
|
5299
6473
|
const spec = def.columns?.[c.id];
|
|
5300
6474
|
return spec === void 0 ? void 0 : aggText(c, i, summary.values[ri]?.[c.id], cellSpec(spec).agg) || void 0;
|
|
5301
6475
|
});
|
|
5302
|
-
foot
|
|
6476
|
+
foot.push(labelled("total", def.label ?? "", values));
|
|
5303
6477
|
});
|
|
5304
6478
|
}
|
|
5305
6479
|
const widths = cols.map((c) => Math.max(1, c.width));
|
|
5306
6480
|
const sum = widths.reduce((a, b) => a + b, 0);
|
|
5307
6481
|
const paper = PAPERS.includes(opts.paper) ? opts.paper : "A4";
|
|
5308
6482
|
const orientation = opts.orientation === "portrait" || opts.orientation === "landscape" ? opts.orientation : sum + (numbered ? 40 : 0) > PORTRAIT_WIDTH ? "landscape" : "portrait";
|
|
5309
|
-
const fit = opts.fitToWidth !== false;
|
|
5310
|
-
const colgroup = fit ? `<colgroup>${numbered ? '<col style="width:40px">' : ""}${widths.map((w) => `<col style="width:${Math.round(w / sum * 1e3) / 10}%">`).join("")}</colgroup>` : "";
|
|
5311
|
-
const title = escHtml(opts.title ?? document.title ?? "");
|
|
5312
6483
|
const printed = opts.printedAt !== false ? new Intl.DateTimeFormat(code, { dateStyle: "medium", timeStyle: "short" }).format(/* @__PURE__ */ new Date()) : "";
|
|
5313
|
-
const
|
|
5314
|
-
|
|
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>` : "";
|
|
5315
6542
|
const small = "font: 8.5px system-ui, sans-serif; color: #6b7280;";
|
|
5316
6543
|
const page = [
|
|
5317
|
-
`size: ${paper} ${orientation}; margin: 12mm 12mm 14mm;`,
|
|
5318
|
-
|
|
5319
|
-
|
|
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} }` : ""
|
|
5320
6547
|
].join(" ");
|
|
5321
|
-
const stripes =
|
|
5322
|
-
|
|
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>
|
|
5323
6557
|
@page { ${page} }${PRINT_CSS}${stripes}
|
|
5324
|
-
</style></head><body>${top}<table${fit ? ' class="fit"' : ""}>${colgroup}<thead>${
|
|
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>`;
|
|
5325
6559
|
}
|
|
5326
6560
|
function printHtml(html) {
|
|
5327
6561
|
if (typeof document === "undefined") return;
|
|
@@ -5363,6 +6597,22 @@ function buildCsv(grid, opts = {}) {
|
|
|
5363
6597
|
if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
|
|
5364
6598
|
return toCsv(matrix, sep);
|
|
5365
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
|
+
}
|
|
5366
6616
|
function excelFormat(def) {
|
|
5367
6617
|
const f = def.format;
|
|
5368
6618
|
if (def.type === "date") {
|
|
@@ -5385,9 +6635,9 @@ function excelFormat(def) {
|
|
|
5385
6635
|
return `${o.useGrouping === false ? "0" : "#,##0"}${decimals}`;
|
|
5386
6636
|
}
|
|
5387
6637
|
function toArgb(css) {
|
|
5388
|
-
const
|
|
5389
|
-
if (
|
|
5390
|
-
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];
|
|
5391
6641
|
return `FF${h.toUpperCase()}`;
|
|
5392
6642
|
}
|
|
5393
6643
|
const m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?\s*\)$/i.exec(css.trim());
|
|
@@ -5486,7 +6736,7 @@ function buildXlsxSheet(grid, opts = {}) {
|
|
|
5486
6736
|
return Number.isFinite(n) ? { v: n, fmt } : String(v);
|
|
5487
6737
|
}
|
|
5488
6738
|
case "date": {
|
|
5489
|
-
const d =
|
|
6739
|
+
const d = toDate2(v);
|
|
5490
6740
|
return d ? { v: d, fmt } : String(v);
|
|
5491
6741
|
}
|
|
5492
6742
|
case "boolean":
|
|
@@ -5511,6 +6761,13 @@ function buildXlsxSheet(grid, opts = {}) {
|
|
|
5511
6761
|
}
|
|
5512
6762
|
if (cls) style = { ...style, ...probe.of(cls) };
|
|
5513
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
|
+
}
|
|
5514
6771
|
if (!Object.keys(style).length) return cell;
|
|
5515
6772
|
return cell !== null && typeof cell === "object" && !(cell instanceof Date) ? { ...cell, ...style } : { v: cell, ...style };
|
|
5516
6773
|
}));
|
|
@@ -5544,6 +6801,8 @@ function buildXlsxSheet(grid, opts = {}) {
|
|
|
5544
6801
|
widths: cols.map((c) => Math.round(c.width / 7)),
|
|
5545
6802
|
merges,
|
|
5546
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),
|
|
5547
6806
|
autoFilter: headRows > 0
|
|
5548
6807
|
};
|
|
5549
6808
|
}
|
|
@@ -5612,13 +6871,13 @@ async function importInto(grid, file, opts = {}) {
|
|
|
5612
6871
|
const sample = grid.store.state.data[0];
|
|
5613
6872
|
let skipped = 0;
|
|
5614
6873
|
const rows = [];
|
|
5615
|
-
for (const
|
|
5616
|
-
if (!
|
|
6874
|
+
for (const line2 of matrix.slice(headRows)) {
|
|
6875
|
+
if (!line2 || line2.every((v) => v === null || v === void 0 || String(v).trim() === "")) continue;
|
|
5617
6876
|
const row = {};
|
|
5618
6877
|
mapped.forEach((node, i) => {
|
|
5619
6878
|
if (!node) return;
|
|
5620
6879
|
const def = node.def;
|
|
5621
|
-
const raw = asText(
|
|
6880
|
+
const raw = asText(line2[i]);
|
|
5622
6881
|
const res = grid.acceptText(def, row, raw, sample?.[def.field]);
|
|
5623
6882
|
if (res.ok) {
|
|
5624
6883
|
if (!(raw === "" && (res.value === null || res.value === ""))) row[def.field] = res.value;
|
|
@@ -6094,7 +7353,7 @@ function buildPivot(source, config, defOf, labels, locale) {
|
|
|
6094
7353
|
}
|
|
6095
7354
|
|
|
6096
7355
|
// src/version.ts
|
|
6097
|
-
var version = "1.
|
|
7356
|
+
var version = "1.6.0";
|
|
6098
7357
|
|
|
6099
7358
|
// src/license.ts
|
|
6100
7359
|
var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
|
|
@@ -6282,6 +7541,7 @@ var ColumnMenu = class {
|
|
|
6282
7541
|
];
|
|
6283
7542
|
if (!def.lockVisible) items.push([loc.hideColumn, () => this.grid.setColumnsVisible([colId], false)]);
|
|
6284
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)]);
|
|
6285
7545
|
popup.append(this.section(items));
|
|
6286
7546
|
const filter = this.filterSection(colId, () => this.close());
|
|
6287
7547
|
if (filter) popup.append(filter);
|
|
@@ -6328,11 +7588,11 @@ var ColumnMenu = class {
|
|
|
6328
7588
|
}
|
|
6329
7589
|
section(items) {
|
|
6330
7590
|
const s = el("div", "fg-menu-section");
|
|
6331
|
-
for (const [label,
|
|
7591
|
+
for (const [label, run2] of items) {
|
|
6332
7592
|
const b = el("button", "fg-menu-item", { type: "button" });
|
|
6333
7593
|
b.textContent = label;
|
|
6334
7594
|
b.addEventListener("click", () => {
|
|
6335
|
-
|
|
7595
|
+
run2();
|
|
6336
7596
|
this.close();
|
|
6337
7597
|
});
|
|
6338
7598
|
s.append(b);
|
|
@@ -7464,6 +8724,611 @@ var FilterBuilder = class {
|
|
|
7464
8724
|
}
|
|
7465
8725
|
};
|
|
7466
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
|
+
|
|
7467
9332
|
// src/grid.ts
|
|
7468
9333
|
var autoRowHeight = (size) => Math.round(size * 2.75);
|
|
7469
9334
|
var autoHeaderHeight = (size) => Math.round(size * 3.1);
|
|
@@ -7508,11 +9373,34 @@ var Grid = class {
|
|
|
7508
9373
|
/** 0-based page and rows per page (pagination only). */
|
|
7509
9374
|
this.page = 0;
|
|
7510
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);
|
|
7511
9398
|
// ── pagination and server rows ──────────────────────────────
|
|
7512
9399
|
/** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
|
|
7513
9400
|
/** showOverlay: an overlay the app asked for, shown whatever the row count. */
|
|
7514
9401
|
this.forcedOverlay = null;
|
|
7515
9402
|
this.filterBuilder = new FilterBuilder(this);
|
|
9403
|
+
/** `${rowId}\u0000${colId}` → when the flash ends and which way a number moved. */
|
|
7516
9404
|
this.flashing = /* @__PURE__ */ new Map();
|
|
7517
9405
|
this.flashTimer = 0;
|
|
7518
9406
|
this.txQueue = [];
|
|
@@ -7552,6 +9440,7 @@ var Grid = class {
|
|
|
7552
9440
|
});
|
|
7553
9441
|
this.sourceDefs = Array.isArray(options.columns) ? options.columns : [];
|
|
7554
9442
|
this.columns = new ColumnModel(this.withSelect(this.sourceDefs));
|
|
9443
|
+
this.bindFormulas();
|
|
7555
9444
|
const pg = options.pagination;
|
|
7556
9445
|
if (pg && typeof pg === "object" && pg.pageSize !== void 0) {
|
|
7557
9446
|
if (Number.isInteger(pg.pageSize) && pg.pageSize > 0) this.pageSize = pg.pageSize;
|
|
@@ -7573,6 +9462,7 @@ var Grid = class {
|
|
|
7573
9462
|
collator: () => this.collator()
|
|
7574
9463
|
});
|
|
7575
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] }));
|
|
7576
9466
|
this.defaultExpanded = options.groupDefaultExpanded ?? 0;
|
|
7577
9467
|
if (options.treeData && !options.getRowId) warn("treeData needs getRowId so parent ids can be matched");
|
|
7578
9468
|
this.updateRows();
|
|
@@ -7588,6 +9478,7 @@ var Grid = class {
|
|
|
7588
9478
|
});
|
|
7589
9479
|
this.density = this.densityOf(options.density);
|
|
7590
9480
|
this.renderer.applyTheme();
|
|
9481
|
+
if (options.flashDuration) this.renderer.root.style.setProperty("--fg-flash-duration", `${this.flashMs()}ms`);
|
|
7591
9482
|
this.renderer.applyFont(this.font.size, this.font.family);
|
|
7592
9483
|
this.menu = new ColumnMenu(this);
|
|
7593
9484
|
this.chooser = new ColumnChooserDialog(this);
|
|
@@ -7614,6 +9505,8 @@ var Grid = class {
|
|
|
7614
9505
|
() => this.groupPanel?.destroy(),
|
|
7615
9506
|
() => this.charts.destroy(),
|
|
7616
9507
|
() => this.filterBuilder.close(),
|
|
9508
|
+
() => this.formatDialog.close(),
|
|
9509
|
+
() => this.assistantPanel.close(),
|
|
7617
9510
|
attachTooltips(this)
|
|
7618
9511
|
);
|
|
7619
9512
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -7629,6 +9522,45 @@ var Grid = class {
|
|
|
7629
9522
|
this.api = this.createApi();
|
|
7630
9523
|
liveGrids.set(this.renderer.root, this);
|
|
7631
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
|
+
}
|
|
7632
9564
|
collator() {
|
|
7633
9565
|
const code = this.store.state.locale.code;
|
|
7634
9566
|
if (this.collatorCache?.code !== code) {
|
|
@@ -7766,17 +9698,35 @@ var Grid = class {
|
|
|
7766
9698
|
isFlashing(rowId, colId) {
|
|
7767
9699
|
return this.flashing.size > 0 && this.flashing.has(`${rowId}\0${colId}`);
|
|
7768
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
|
+
}
|
|
7769
9711
|
flash(keys) {
|
|
7770
|
-
const
|
|
7771
|
-
|
|
9712
|
+
const ms = this.flashMs();
|
|
9713
|
+
const until = Date.now() + ms;
|
|
9714
|
+
for (const [k, dir] of keys) this.flashing.set(k, { until, dir });
|
|
7772
9715
|
if (this.flashTimer) return;
|
|
7773
9716
|
const tick = () => {
|
|
7774
9717
|
const now = Date.now();
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
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;
|
|
7778
9728
|
};
|
|
7779
|
-
this.flashTimer = window.setTimeout(tick,
|
|
9729
|
+
this.flashTimer = window.setTimeout(tick, ms);
|
|
7780
9730
|
}
|
|
7781
9731
|
applyTransaction(tx) {
|
|
7782
9732
|
if (!tx || typeof tx !== "object") return { added: [], updated: [], removed: [], notFound: [] };
|
|
@@ -7791,32 +9741,55 @@ var Grid = class {
|
|
|
7791
9741
|
const f = this.columns.getNode(st.colId)?.def.field;
|
|
7792
9742
|
if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
|
|
7793
9743
|
}
|
|
7794
|
-
const flashKeys =
|
|
9744
|
+
const flashKeys = /* @__PURE__ */ new Map();
|
|
9745
|
+
const changed = { fields: /* @__PURE__ */ new Set(), ids: /* @__PURE__ */ new Set() };
|
|
7795
9746
|
let next = this.store.state.data;
|
|
7796
9747
|
const present = new Set(next);
|
|
7797
9748
|
const byId = new Map(next.map((r) => [this.rowModel.idOf(r), r]));
|
|
7798
9749
|
for (const tx of list) {
|
|
7799
|
-
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);
|
|
7800
9751
|
}
|
|
7801
9752
|
if (!result.added.length && !result.updated.length && !result.removed.length) {
|
|
7802
9753
|
if (result.notFound.length) this.events.emit("transactionApplied", result);
|
|
7803
9754
|
return result;
|
|
7804
9755
|
}
|
|
7805
9756
|
this.editVersion++;
|
|
7806
|
-
this.
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
this.
|
|
7810
|
-
|
|
7811
|
-
|
|
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
|
+
}
|
|
7812
9772
|
}
|
|
7813
9773
|
this.editor.sync();
|
|
7814
|
-
if (flashKeys.
|
|
9774
|
+
if (flashKeys.size && this.options.flashCells !== false) this.flash(flashKeys);
|
|
7815
9775
|
this.events.emit("transactionApplied", result);
|
|
7816
9776
|
return result;
|
|
7817
9777
|
}
|
|
7818
9778
|
/** One transaction against `data` (remove → update → add); returns the new array. */
|
|
7819
|
-
|
|
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) {
|
|
7820
9793
|
const getId = this.options.getRowId;
|
|
7821
9794
|
const { present, byId } = index;
|
|
7822
9795
|
const keyOf = (r) => {
|
|
@@ -7841,6 +9814,20 @@ var Grid = class {
|
|
|
7841
9814
|
this.tracker.originals.delete(row);
|
|
7842
9815
|
} else if (!row) result.notFound.push(r);
|
|
7843
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
|
+
};
|
|
7844
9831
|
for (const r of tx.update ?? []) {
|
|
7845
9832
|
const k = keyOf(r);
|
|
7846
9833
|
const row = k === null ? void 0 : byId.get(k);
|
|
@@ -7848,16 +9835,16 @@ var Grid = class {
|
|
|
7848
9835
|
result.notFound.push(r);
|
|
7849
9836
|
continue;
|
|
7850
9837
|
}
|
|
7851
|
-
|
|
7852
|
-
for (const [field, value] of Object.entries(r)) {
|
|
7853
|
-
if (orig) orig[field] = value;
|
|
7854
|
-
if (Object.is(row[field], value)) continue;
|
|
7855
|
-
row[field] = value;
|
|
7856
|
-
for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
|
|
7857
|
-
}
|
|
7858
|
-
result.updated.push(row);
|
|
9838
|
+
update(r, k, row);
|
|
7859
9839
|
}
|
|
7860
|
-
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);
|
|
7861
9848
|
result.added.push(...added);
|
|
7862
9849
|
for (const r of added) {
|
|
7863
9850
|
present.add(r);
|
|
@@ -7873,13 +9860,16 @@ var Grid = class {
|
|
|
7873
9860
|
if (!tx || typeof tx !== "object") return;
|
|
7874
9861
|
this.txQueue.push(tx);
|
|
7875
9862
|
if (this.txTimer) return;
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
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);
|
|
7883
9873
|
}
|
|
7884
9874
|
setAdvancedFilter(filter) {
|
|
7885
9875
|
const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
|
|
@@ -7908,6 +9898,7 @@ var Grid = class {
|
|
|
7908
9898
|
expanded: Object.fromEntries(expanded),
|
|
7909
9899
|
selected: [...s.selected]
|
|
7910
9900
|
};
|
|
9901
|
+
if (this.cfRules.length) state.conditionalFormats = this.getConditionalFormats();
|
|
7911
9902
|
if (this.options.pagination) {
|
|
7912
9903
|
state.page = this.page;
|
|
7913
9904
|
state.pageSize = this.pageSize;
|
|
@@ -7945,6 +9936,7 @@ var Grid = class {
|
|
|
7945
9936
|
}
|
|
7946
9937
|
if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
|
|
7947
9938
|
if (want("page") && typeof state.page === "number") this.setPage(state.page);
|
|
9939
|
+
if (want("conditionalFormats") && Array.isArray(state.conditionalFormats)) this.setConditionalFormats(state.conditionalFormats);
|
|
7948
9940
|
if (want("selected") && Array.isArray(state.selected)) {
|
|
7949
9941
|
const ids = new Set(this.rowModel.allNodes().map((n) => n.id));
|
|
7950
9942
|
this.setSelected(new Set(state.selected.map(String).filter((id) => ids.has(id))));
|
|
@@ -9194,6 +11186,7 @@ var Grid = class {
|
|
|
9194
11186
|
return;
|
|
9195
11187
|
}
|
|
9196
11188
|
this.columns.setDefs(this.withSelect(defs));
|
|
11189
|
+
this.bindFormulas();
|
|
9197
11190
|
const known = (id) => !!this.columns.getNode(id);
|
|
9198
11191
|
const groupBy = this.groupBy.filter(known);
|
|
9199
11192
|
if (groupBy.length !== this.groupBy.length) {
|
|
@@ -9271,6 +11264,12 @@ var Grid = class {
|
|
|
9271
11264
|
getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
|
|
9272
11265
|
print: guard((o) => printHtml(buildPrintHtml(this, o))),
|
|
9273
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
|
+
}),
|
|
9274
11273
|
setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
|
|
9275
11274
|
setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
|
|
9276
11275
|
getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
|
|
@@ -9278,6 +11277,12 @@ var Grid = class {
|
|
|
9278
11277
|
isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
|
|
9279
11278
|
applyTransaction: guard((tx) => this.applyTransaction(tx)),
|
|
9280
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()),
|
|
9281
11286
|
getState: guard(() => this.getState()),
|
|
9282
11287
|
setState: guard((s, o) => this.setState(s, o)),
|
|
9283
11288
|
getScrollPosition: guard(() => this.renderer.getScroll()),
|
|
@@ -9344,6 +11349,8 @@ var Grid = class {
|
|
|
9344
11349
|
getPivot: guard(() => this.getPivot()),
|
|
9345
11350
|
createChart: guard((o) => this.charts.create(o)),
|
|
9346
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")),
|
|
9347
11354
|
exportCsv: guard((name = "export.csv", o) => download(name, ["\uFEFF", buildCsv(this, o)], "text/csv;charset=utf-8")),
|
|
9348
11355
|
getXlsx: guard((o) => buildXlsx(this, o)),
|
|
9349
11356
|
exportXlsx: guard((name = "export.xlsx", o) => download(name, [buildXlsx(this, o)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")),
|
|
@@ -9441,13 +11448,13 @@ function saveChanges(grid, url, options = {}) {
|
|
|
9441
11448
|
if (!grid.hasChanges()) return Promise.resolve(null);
|
|
9442
11449
|
const { created, updated, deleted } = grid.getModifiedRows();
|
|
9443
11450
|
const rows = new Map([...created, ...updated].map((r) => [r, { ...r }]));
|
|
9444
|
-
const
|
|
11451
|
+
const run2 = (async () => {
|
|
9445
11452
|
const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
|
|
9446
11453
|
grid.commitChanges({ rows, deleted });
|
|
9447
11454
|
return json;
|
|
9448
11455
|
})().finally(() => saving.delete(grid));
|
|
9449
|
-
saving.set(grid,
|
|
9450
|
-
return
|
|
11456
|
+
saving.set(grid, run2);
|
|
11457
|
+
return run2;
|
|
9451
11458
|
}
|
|
9452
11459
|
async function loadData(grid, url, options = {}) {
|
|
9453
11460
|
const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });
|
|
@@ -9459,15 +11466,186 @@ function csrfHeaders(doc = document) {
|
|
|
9459
11466
|
const header2 = doc.querySelector('meta[name="_csrf_header"]')?.content || "X-CSRF-TOKEN";
|
|
9460
11467
|
return token ? { [header2]: token } : {};
|
|
9461
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
|
+
}
|
|
9462
11635
|
export {
|
|
9463
11636
|
buildWorkbook,
|
|
11637
|
+
connectLive,
|
|
9464
11638
|
createGrid,
|
|
9465
11639
|
csrfHeaders,
|
|
9466
11640
|
en_default as en,
|
|
11641
|
+
evaluateFormula,
|
|
9467
11642
|
exportWorkbook,
|
|
11643
|
+
formulaFunctions,
|
|
9468
11644
|
loadData,
|
|
11645
|
+
messageToTransaction,
|
|
9469
11646
|
parseCsv,
|
|
9470
11647
|
readXlsx,
|
|
11648
|
+
registerFormulaFunction,
|
|
9471
11649
|
registerLocale,
|
|
9472
11650
|
renderChart,
|
|
9473
11651
|
resolveLocale,
|