@freegridjs/core 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1296,6 +1296,7 @@ function columnFormulaValue(def, row) {
1296
1296
  if (scope) return valueIn(scope, cacheOf(scope), row, def.field);
1297
1297
  return run(row[def.field], { row, scope, seen: /* @__PURE__ */ new Set([def.field]), depth: 0, memo: /* @__PURE__ */ new Map() });
1298
1298
  }
1299
+ var formulasOff = (def) => !!scopes.get(def)?.off?.();
1299
1300
  var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
1300
1301
  function formulaRefs(formula) {
1301
1302
  let n;
@@ -1327,10 +1328,10 @@ function getCellValue(def, row) {
1327
1328
  return void 0;
1328
1329
  }
1329
1330
  }
1330
- if (typeof def.formula === "string" && isFormula(def.formula)) return columnFormulaValue(def, row);
1331
+ if (typeof def.formula === "string" && isFormula(def.formula) && !formulasOff(def)) return columnFormulaValue(def, row);
1331
1332
  if (def.field == null) return void 0;
1332
1333
  const raw = row[def.field];
1333
- return def.formulas && isFormula(raw) ? columnFormulaValue(def, row) : raw;
1334
+ return def.formulas && isFormula(raw) && !formulasOff(def) ? columnFormulaValue(def, row) : raw;
1334
1335
  }
1335
1336
 
1336
1337
  // src/interactions/filterRow.ts
@@ -3416,6 +3417,8 @@ function nextFocus(key, mods, p, s) {
3416
3417
 
3417
3418
  // src/registry.ts
3418
3419
  var liveGrids = /* @__PURE__ */ new Map();
3420
+ var apiGrids = /* @__PURE__ */ new WeakMap();
3421
+ var gridOfApi = (api) => api ? apiGrids.get(api) : void 0;
3419
3422
  var gridOfRoot = (root) => root ? liveGrids.get(root) : void 0;
3420
3423
 
3421
3424
  // src/interactions/body.ts
@@ -3450,6 +3453,7 @@ function attachBody(grid) {
3450
3453
  const keyOf = (e) => mirrored(grid, e);
3451
3454
  let lastPointer = "mouse";
3452
3455
  let pressTimer = 0;
3456
+ let endGesture = null;
3453
3457
  let suppressClick = false;
3454
3458
  const toEvent = (e) => {
3455
3459
  const hit = renderer.cellAt(e.target);
@@ -3492,7 +3496,7 @@ function attachBody(grid) {
3492
3496
  const data = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
3493
3497
  if (data.length) {
3494
3498
  grid.setFocus(ev.rowIndex, data[0].id);
3495
- grid.setRange({ start: { rowIndex: ev.rowIndex, colId: data[0].id }, end: { rowIndex: ev.rowIndex, colId: data[data.length - 1].id } });
3499
+ if (grid.rangesOn()) grid.setRange({ start: { rowIndex: ev.rowIndex, colId: data[0].id }, end: { rowIndex: ev.rowIndex, colId: data[data.length - 1].id } });
3496
3500
  }
3497
3501
  return;
3498
3502
  }
@@ -3501,9 +3505,9 @@ function attachBody(grid) {
3501
3505
  const prev = grid.store.state.focus;
3502
3506
  const onBox = ev.colId === SELECT_COL_ID;
3503
3507
  if (dragged) {
3504
- } else if (e.shiftKey && prev && !onBox && !isSystemCol(prev.colId) && grid.options.rangeSelection !== false) {
3508
+ } else if (e.shiftKey && prev && !onBox && !isSystemCol(prev.colId) && grid.rangesOn()) {
3505
3509
  grid.setRange({ start: prev, end: { rowIndex: ev.rowIndex, colId: ev.colId } }, true);
3506
- } else if ((e.ctrlKey || e.metaKey) && !onBox && !isSystemCol(ev.colId) && grid.options.rangeSelection !== false) {
3510
+ } else if ((e.ctrlKey || e.metaKey) && !onBox && !isSystemCol(ev.colId) && grid.rangesOn()) {
3507
3511
  const at = { rowIndex: ev.rowIndex, colId: ev.colId };
3508
3512
  grid.addRange({ start: at, end: at });
3509
3513
  grid.setFocus(ev.rowIndex, ev.colId);
@@ -3569,7 +3573,7 @@ function attachBody(grid) {
3569
3573
  grid.selectAllCells();
3570
3574
  return true;
3571
3575
  case "find":
3572
- if (grid.options.findBar === false) return false;
3576
+ if (grid.options.findBar === false || !grid.paid) return false;
3573
3577
  grid.finder.openBar();
3574
3578
  return true;
3575
3579
  case "paste":
@@ -3672,6 +3676,7 @@ function attachBody(grid) {
3672
3676
  window.removeEventListener("pointermove", move);
3673
3677
  window.removeEventListener("pointerup", cancel);
3674
3678
  window.removeEventListener("pointercancel", cancel);
3679
+ if (endGesture === cancel) endGesture = null;
3675
3680
  };
3676
3681
  const move = (ev) => {
3677
3682
  if (Math.hypot(ev.clientX - x0, ev.clientY - y0) > 8) cancel();
@@ -3679,7 +3684,7 @@ function attachBody(grid) {
3679
3684
  clearTimeout(pressTimer);
3680
3685
  pressTimer = window.setTimeout(() => {
3681
3686
  cancel();
3682
- if (grid.options.contextMenu === false) return;
3687
+ if (!grid.contextMenu.enabled()) return;
3683
3688
  suppressClick = true;
3684
3689
  if (!isSystemCol(hit.colId)) {
3685
3690
  grid.setFocus(hit.rowIndex, hit.colId);
@@ -3687,6 +3692,8 @@ function attachBody(grid) {
3687
3692
  }
3688
3693
  grid.contextMenu.open(x0, y0, hit.rowIndex, hit.colId);
3689
3694
  }, 550);
3695
+ endGesture?.();
3696
+ endGesture = cancel;
3690
3697
  window.addEventListener("pointermove", move);
3691
3698
  window.addEventListener("pointerup", cancel);
3692
3699
  window.addEventListener("pointercancel", cancel);
@@ -3698,7 +3705,7 @@ function attachBody(grid) {
3698
3705
  return;
3699
3706
  }
3700
3707
  if (e.button === 0 && onHandleDown(e)) return;
3701
- if (grid.options.rangeSelection === false) return;
3708
+ if (!grid.rangesOn()) return;
3702
3709
  if (e.button !== 0 || e.shiftKey || e.target.closest("input, a, button")) return;
3703
3710
  const start = renderer.cellAt(e.target);
3704
3711
  if (!start || isSystemCol(start.colId)) return;
@@ -3720,11 +3727,16 @@ function attachBody(grid) {
3720
3727
  const up = () => {
3721
3728
  window.removeEventListener("pointermove", move);
3722
3729
  window.removeEventListener("pointerup", up);
3730
+ window.removeEventListener("pointercancel", up);
3723
3731
  root.classList.remove("fg-range-dragging");
3724
3732
  if (dragging) suppressClick = true;
3733
+ if (endGesture === up) endGesture = null;
3725
3734
  };
3735
+ endGesture?.();
3736
+ endGesture = up;
3726
3737
  window.addEventListener("pointermove", move);
3727
3738
  window.addEventListener("pointerup", up);
3739
+ window.addEventListener("pointercancel", up);
3728
3740
  };
3729
3741
  const onKeyDown = (e) => {
3730
3742
  if (e.target !== root && e.target !== renderer.gridEl) return;
@@ -3746,7 +3758,7 @@ function attachBody(grid) {
3746
3758
  grid.setRange(null);
3747
3759
  return;
3748
3760
  }
3749
- if (ctrl && e.key.toLowerCase() === "a" && grid.options.rangeSelection !== false) {
3761
+ if (ctrl && e.key.toLowerCase() === "a" && grid.rangesOn()) {
3750
3762
  e.preventDefault();
3751
3763
  grid.selectAllCells();
3752
3764
  return;
@@ -3763,7 +3775,7 @@ function attachBody(grid) {
3763
3775
  }
3764
3776
  const size = { rows: count, cols: cols.length, pageRows: renderer.pageRows() };
3765
3777
  const firstData = cols.findIndex((c) => !isSystemCol(c.id));
3766
- const ranges = grid.options.rangeSelection !== false;
3778
+ const ranges = grid.rangesOn();
3767
3779
  if (ranges && e.shiftKey && e.key !== "Tab" && NAV_KEYS.has(e.key)) {
3768
3780
  const anchor = f ?? { rowIndex: 0, colId: cols[firstData].id };
3769
3781
  const range = grid.store.state.range;
@@ -3823,6 +3835,8 @@ function attachBody(grid) {
3823
3835
  root.addEventListener("paste", onPaste);
3824
3836
  root.addEventListener("cut", onCut);
3825
3837
  return () => {
3838
+ clearTimeout(pressTimer);
3839
+ endGesture?.();
3826
3840
  body.removeEventListener("click", onClick);
3827
3841
  body.removeEventListener("dblclick", onDblClick);
3828
3842
  body.removeEventListener("pointerdown", onPointerDown);
@@ -4264,6 +4278,10 @@ var ContextMenu = class {
4264
4278
  this.close();
4265
4279
  this.detach();
4266
4280
  }
4281
+ /** The cell menu is on: not switched off and the paid feature allowed (closed: the browser's menu shows). */
4282
+ enabled() {
4283
+ return this.grid.options.contextMenu !== false && this.grid.allow("contextMenu");
4284
+ }
4267
4285
  onContextMenu(e) {
4268
4286
  const { grid } = this;
4269
4287
  if (grid.options.contextMenu === false) return;
@@ -4278,7 +4296,7 @@ var ContextMenu = class {
4278
4296
  return;
4279
4297
  }
4280
4298
  const hit = grid.renderer.cellAt(target);
4281
- if (!hit) return;
4299
+ if (!hit || !this.enabled()) return;
4282
4300
  e.preventDefault();
4283
4301
  grid.renderer.gridEl.focus({ preventScroll: true });
4284
4302
  if (!isSystemCol(hit.colId) && !grid.rangeContains(hit.rowIndex, hit.colId)) {
@@ -4290,7 +4308,7 @@ var ContextMenu = class {
4290
4308
  /** Keyboard: Shift+F10 or the ContextMenu key. */
4291
4309
  openAtFocus() {
4292
4310
  const f = this.grid.store.state.focus;
4293
- if (!f || this.grid.options.contextMenu === false) return;
4311
+ if (!f || !this.enabled()) return;
4294
4312
  const r = this.grid.renderer.root.getBoundingClientRect();
4295
4313
  const cell = this.grid.renderer.root.querySelector(`.fg-row[data-row="${f.rowIndex}"] .fg-cell[data-col="${CSS.escape(f.colId)}"]`);
4296
4314
  const box = cell?.getBoundingClientRect() ?? r;
@@ -4333,7 +4351,7 @@ var ContextMenu = class {
4333
4351
  if (grid.options.rowSelection === "multiple") items.push({ label: loc.selectAllRows, action: () => grid.api.selectAll() });
4334
4352
  items.push({ label: loc.chooseColumns, action: () => grid.chooser.open() });
4335
4353
  const b = grid.rangeBounds();
4336
- if (b && (b.r2 > b.r1 || b.c2 > b.c1)) {
4354
+ if (b && (b.r2 > b.r1 || b.c2 > b.c1) && grid.paid) {
4337
4355
  items.push(
4338
4356
  "separator",
4339
4357
  { label: loc.chartBar, action: () => grid.api.createChart({ type: "bar" }) },
@@ -4344,12 +4362,12 @@ var ContextMenu = class {
4344
4362
  items.push(
4345
4363
  "separator",
4346
4364
  { label: loc.exportCsv, action: () => grid.api.exportCsv() },
4347
- { label: loc.exportExcel, action: () => grid.api.exportXlsx() },
4365
+ ...grid.paid ? [{ label: loc.exportExcel, action: () => grid.api.exportXlsx() }] : [],
4348
4366
  { label: loc.exportJson, action: () => grid.api.exportJson() },
4349
- ...grid.options.assistant?.url ? [{ label: `${loc.ai.title}\u2026`, action: () => grid.api.openAssistant() }] : [],
4350
- { label: loc.exportPdf, action: () => grid.api.exportPdf() }
4367
+ ...grid.options.assistant?.url && grid.paid ? [{ label: `${loc.ai.title}\u2026`, action: () => grid.api.openAssistant() }] : [],
4368
+ ...grid.paid ? [{ label: loc.exportPdf, action: () => grid.api.exportPdf() }] : []
4351
4369
  );
4352
- if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
4370
+ if (grid.options.advancedFilter !== false && !grid.options.pivot && grid.paid) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
4353
4371
  items.push("separator", { label: loc.print, action: () => grid.api.print() });
4354
4372
  return items;
4355
4373
  }
@@ -4492,8 +4510,7 @@ var FillHandle = class {
4492
4510
  this.handle.addEventListener("pointerdown", this.onDown);
4493
4511
  }
4494
4512
  enabled() {
4495
- const o = this.grid.options;
4496
- return o.fillHandle !== false && o.rangeSelection !== false;
4513
+ return this.grid.options.fillHandle !== false && this.grid.rangesOn();
4497
4514
  }
4498
4515
  /** Current source block: the range, or the focused cell. */
4499
4516
  source() {
@@ -4670,7 +4687,7 @@ var Finder = class {
4670
4687
  this.countEl.textContent = !this.query ? "" : this.matches.length ? `${this.index + 1}/${this.matches.length}` : loc.noMatches;
4671
4688
  }
4672
4689
  openBar() {
4673
- if (this.grid.options.findBar === false) return;
4690
+ if (this.grid.options.findBar === false || !this.grid.paid) return;
4674
4691
  if (this.bar) {
4675
4692
  this.bar.querySelector("input").focus();
4676
4693
  return;
@@ -7603,12 +7620,36 @@ function buildPivot(source, config, defOf, labels, locale) {
7603
7620
  return { columns: [...rowColumns, ...valueColumns], rows, groupBy: cfg.rows.slice(0, -1) };
7604
7621
  }
7605
7622
 
7623
+ // src/features.ts
7624
+ var PAID_FEATURES = {
7625
+ grouping: { en: "Row grouping and aggregation", ko: "\uD589 \uADF8\uB8F9\xB7\uC9D1\uACC4" },
7626
+ pivot: { en: "Pivot", ko: "\uD53C\uBC97" },
7627
+ charts: { en: "Charts", ko: "\uCC28\uD2B8" },
7628
+ range: { en: "Cell range selection", ko: "\uC140 \uBC94\uC704 \uC120\uD0DD" },
7629
+ clipboard: { en: "Range copy and paste", ko: "\uBC94\uC704 \uBCF5\uC0AC\xB7\uBD99\uC5EC\uB123\uAE30" },
7630
+ fill: { en: "Fill handle", ko: "\uCC44\uC6B0\uAE30 \uD578\uB4E4" },
7631
+ excel: { en: "Excel export", ko: "\uC5D1\uC140 \uB0B4\uBCF4\uB0B4\uAE30" },
7632
+ pdf: { en: "PDF export", ko: "PDF \uB0B4\uBCF4\uB0B4\uAE30" },
7633
+ serverGroups: { en: "Server-side groups", ko: "\uC11C\uBC84 \uADF8\uB8F9" },
7634
+ advancedFilter: { en: "Advanced filter", ko: "\uACE0\uAE09 \uD544\uD130" },
7635
+ sideBar: { en: "Side bar panels", ko: "\uC0AC\uC774\uB4DC\uBC14 \uD328\uB110" },
7636
+ contextMenu: { en: "Context menu", ko: "\uCEE8\uD14D\uC2A4\uD2B8 \uBA54\uB274" },
7637
+ summary: { en: "Summary rows", ko: "\uD569\uACC4 \uD589" },
7638
+ formulas: { en: "Formulas", ko: "\uC218\uC2DD" },
7639
+ find: { en: "Find", ko: "\uCC3E\uAE30" },
7640
+ conditional: { en: "Conditional formatting", ko: "\uC870\uAC74\uBD80 \uC11C\uC2DD" },
7641
+ assistant: { en: "AI assistant", ko: "AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8" },
7642
+ merge: { en: "Merged cells", ko: "\uC140 \uBCD1\uD569" },
7643
+ workbook: { en: "Workbook", ko: "\uC6CC\uD06C\uBD81" }
7644
+ };
7645
+
7606
7646
  // src/version.ts
7607
- var version = "1.8.0";
7647
+ var version = "1.9.1";
7608
7648
 
7609
7649
  // src/license.ts
7610
7650
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
7611
7651
  var PRICING_URL = "https://freegrid.app/pricing/";
7652
+ var PERSONAL_URL = "https://freegrid.app/license/personal/";
7612
7653
  var USAGE_URL = "https://freegrid.app/api/usage";
7613
7654
  var STATUS_URL = "https://freegrid.app/api/domain-status";
7614
7655
  var majorOf = (v) => Number.parseInt(String(v).split(".")[0], 10);
@@ -7618,17 +7659,21 @@ var b64urlDecode = (s) => {
7618
7659
  };
7619
7660
  function isDevHost(host) {
7620
7661
  const h = host.toLowerCase().replace(/^\[|\]$/g, "");
7621
- return !h || h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0" || h.endsWith(".localhost") || h.endsWith(".test") || h.endsWith(".local") || /^(10|192\.168|172\.(1[6-9]|2\d|3[01]))\./.test(h);
7662
+ if (!h || h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0" || h.endsWith(".localhost") || h.endsWith(".test") || h.endsWith(".local")) return true;
7663
+ const ip = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
7664
+ if (!ip) return false;
7665
+ const [a, b, c, d] = ip.slice(1).map(Number);
7666
+ if ([a, b, c, d].some((n) => n > 255)) return false;
7667
+ return a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31;
7622
7668
  }
7623
7669
  function domainMatches(host, domains) {
7624
7670
  const h = host.toLowerCase().replace(/\.$/, "");
7625
7671
  return domains.some((d) => {
7672
+ if (typeof d !== "string") return false;
7626
7673
  const raw = d.toLowerCase().replace(/\.$/, "");
7627
- const wildcard = raw.startsWith("*.");
7628
- const dom = wildcard ? raw.slice(2) : raw;
7674
+ const dom = raw.startsWith("*.") ? raw.slice(2) : raw;
7629
7675
  if (!dom) return false;
7630
- if (h === dom || h === `www.${dom}`) return true;
7631
- return wildcard && h.endsWith(`.${dom}`);
7676
+ return h === dom || h.endsWith(`.${dom}`);
7632
7677
  });
7633
7678
  }
7634
7679
  var isDay = (s) => {
@@ -7638,19 +7683,32 @@ var isDay = (s) => {
7638
7683
  return d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
7639
7684
  };
7640
7685
  var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
7641
- async function verifyLicense(key, host, opts = {}) {
7686
+ function checkTerms(key, host, opts = {}) {
7642
7687
  const parts = typeof key === "string" ? key.trim().split(".") : [];
7643
- if (parts.length !== 3 || parts[0] !== "FG1") return { ok: false, reason: "format" };
7688
+ if (parts.length !== 3 || parts[0] !== "FG1") return { fail: { ok: false, reason: "format" } };
7644
7689
  let info;
7645
7690
  try {
7646
7691
  info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
7647
- if (!Array.isArray(info.domains) || !isDay(info.expires)) return { ok: false, reason: "format" };
7648
- if (info.major !== void 0 && !(Number.isInteger(info.major) && info.major > 0)) return { ok: false, reason: "format" };
7692
+ if (!info || !Array.isArray(info.domains) || !info.domains.every((d) => typeof d === "string") || !isDay(info.expires)) return { fail: { ok: false, reason: "format" } };
7693
+ if (info.major !== void 0 && !(Number.isInteger(info.major) && info.major > 0)) return { fail: { ok: false, reason: "format" } };
7694
+ if (info.plan !== void 0 && info.plan !== "company" && info.plan !== "personal") return { fail: { ok: false, reason: "format" } };
7649
7695
  } catch {
7650
- return { ok: false, reason: "format" };
7696
+ return { fail: { ok: false, reason: "format" } };
7651
7697
  }
7652
- const subtle = typeof crypto !== "undefined" ? crypto.subtle : void 0;
7653
- if (!subtle) return { ok: false, reason: "unsupported", info };
7698
+ let fail = null;
7699
+ if (today(opts.now ?? /* @__PURE__ */ new Date()) > info.expires) fail = { ok: false, reason: "expired", info };
7700
+ else if (!domainMatches(host, info.domains)) fail = { ok: false, reason: "domain", info };
7701
+ else if (info.major !== void 0 && majorOf(opts.version ?? version) > info.major) fail = { ok: false, reason: "version", info };
7702
+ return { parts, info, fail };
7703
+ }
7704
+ var hasSubtle = () => typeof crypto !== "undefined" && !!crypto.subtle;
7705
+ async function verifyLicense(key, host, opts = {}) {
7706
+ const checked = checkTerms(key, host, opts);
7707
+ if (!("info" in checked)) return checked.fail;
7708
+ const { parts, info } = checked;
7709
+ const terms = () => checked.fail;
7710
+ if (!hasSubtle()) return terms() ?? { ok: false, reason: "unsupported", info };
7711
+ const subtle = crypto.subtle;
7654
7712
  try {
7655
7713
  const pub = await subtle.importKey("jwk", opts.publicKey ?? PUBLIC_KEY, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
7656
7714
  const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
@@ -7659,20 +7717,22 @@ async function verifyLicense(key, host, opts = {}) {
7659
7717
  } catch {
7660
7718
  return { ok: false, reason: "signature", info };
7661
7719
  }
7662
- if (today(opts.now ?? /* @__PURE__ */ new Date()) > info.expires) return { ok: false, reason: "expired", info };
7663
- if (!domainMatches(host, info.domains)) return { ok: false, reason: "domain", info };
7664
- if (info.major !== void 0 && majorOf(opts.version ?? version) > info.major) return { ok: false, reason: "version", info };
7665
- return { ok: true, info };
7720
+ return terms() ?? { ok: true, info };
7666
7721
  }
7667
7722
  var globalKey;
7668
7723
  var statuses = /* @__PURE__ */ new Map();
7669
- var badgeCheck = null;
7724
+ var statusChecks = /* @__PURE__ */ new Map();
7725
+ var gateNotices = /* @__PURE__ */ new Set();
7726
+ var blockedWarned = /* @__PURE__ */ new Set();
7670
7727
  function setLicenseKey(key) {
7671
7728
  globalKey = key;
7672
7729
  }
7673
7730
  var NOTICE = {
7674
- missing: `FreeGrid is free for personal use. Company use needs a domain license (\u20A9200,000 per domain per year): ${PRICING_URL}
7675
- FreeGrid\uB294 \uAC1C\uC778\uC5D0\uAC8C \uBB34\uB8CC\uC785\uB2C8\uB2E4. \uD68C\uC0AC\uC5D0\uC11C \uC4F0\uB824\uBA74 \uB3C4\uBA54\uC778 \uB77C\uC774\uC120\uC2A4(\uB3C4\uBA54\uC778\uB2F9 \uC5F0 20\uB9CC \uC6D0)\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4: ${PRICING_URL}`,
7731
+ missing: `FreeGrid: every feature is free for 30 days on a domain. After that, paid features need a free personal key (${PERSONAL_URL}) or a company key: ${PRICING_URL}
7732
+ FreeGrid: \uB3C4\uBA54\uC778\uB9C8\uB2E4 30\uC77C \uB3D9\uC548 \uBAA8\uB4E0 \uAE30\uB2A5\uC744 \uBB34\uB8CC\uB85C \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uADF8 \uB4A4 \uC720\uB8CC \uAE30\uB2A5\uC5D0\uB294 \uBB34\uB8CC \uAC1C\uC778 \uD0A4(${PERSONAL_URL}) \uB610\uB294 \uD68C\uC0AC \uD0A4\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4: ${PRICING_URL}`,
7733
+ trial: (ends) => `FreeGrid: the 30-day trial of paid features on this domain ends ${ends}. / \uC774 \uB3C4\uBA54\uC778\uC758 \uC720\uB8CC \uAE30\uB2A5 30\uC77C \uCCB4\uD5D8\uC740 ${ends}\uC5D0 \uB05D\uB0A9\uB2C8\uB2E4.`,
7734
+ ended: `FreeGrid: the 30-day trial on this domain has ended, so paid features are now off. Get a free personal key (${PERSONAL_URL}) or a company key: ${PRICING_URL}
7735
+ FreeGrid: \uC774 \uB3C4\uBA54\uC778\uC758 30\uC77C \uCCB4\uD5D8\uC774 \uB05D\uB098 \uC720\uB8CC \uAE30\uB2A5\uC774 \uAEBC\uC84C\uC2B5\uB2C8\uB2E4. \uBB34\uB8CC \uAC1C\uC778 \uD0A4(${PERSONAL_URL}) \uB610\uB294 \uD68C\uC0AC \uD0A4\uB97C \uBC1B\uC73C\uC138\uC694: ${PRICING_URL}`,
7676
7736
  format: "FreeGrid: the license key is not in the right format. / \uB77C\uC774\uC120\uC2A4 \uD0A4 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
7677
7737
  signature: "FreeGrid: the license key could not be verified. / \uB77C\uC774\uC120\uC2A4 \uD0A4\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
7678
7738
  expired: "FreeGrid: the license key has expired. / \uB77C\uC774\uC120\uC2A4 \uAE30\uAC04\uC774 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4.",
@@ -7695,52 +7755,145 @@ function reportUsage(info, host = typeof location !== "undefined" ? location.hos
7695
7755
  }
7696
7756
  function checkLicense(key = globalKey, host = typeof location !== "undefined" ? location.hostname : "") {
7697
7757
  if (isDevHost(host)) return Promise.resolve(true);
7698
- const k = key ?? globalKey;
7758
+ return keyState(key ?? globalKey, host).then((s) => s === "ok" || s === "unsupported");
7759
+ }
7760
+ function keyState(k, host) {
7699
7761
  const id = `${k ?? ""}@${host}`;
7700
7762
  let status = statuses.get(id);
7701
7763
  if (status) return status;
7702
7764
  if (!k) {
7703
7765
  console.info(NOTICE.missing);
7704
7766
  reportUsage({ version, licensed: false }, host);
7705
- status = Promise.resolve(false);
7767
+ status = Promise.resolve("none");
7706
7768
  } else {
7707
7769
  status = verifyLicense(k, host).then((res) => {
7708
7770
  reportUsage({ version, licensed: res.ok, company: res.ok ? res.info.company : void 0 }, host);
7709
- if (res.ok || res.reason === "unsupported") return res.ok || res.reason === "unsupported";
7771
+ if (res.ok) return "ok";
7772
+ if (res.reason === "unsupported") return "unsupported";
7710
7773
  const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.major ? `${res.info.major}.x` : res.info.expires})` : "";
7711
7774
  console.warn(`${NOTICE[res.reason]}${extra}
7712
7775
  ${PRICING_URL}`);
7713
- return false;
7714
- }, () => false);
7776
+ return "bad";
7777
+ }, () => "bad");
7715
7778
  }
7716
7779
  statuses.set(id, status);
7717
7780
  return status;
7718
7781
  }
7719
- function badgeRequested(host = typeof location !== "undefined" ? location.hostname : "") {
7720
- if (isDevHost(host) || typeof fetch !== "function") return Promise.resolve(false);
7721
- if (badgeCheck) return badgeCheck;
7782
+ var gateHost;
7783
+ var currentHost = () => gateHost ?? (typeof location !== "undefined" ? location.hostname : "");
7784
+ var parseStatus = (d) => {
7785
+ if (!d || typeof d !== "object") return null;
7786
+ const { badge, trial } = d;
7787
+ const valid = !!trial && typeof trial === "object" && isDay(trial.started) && isDay(trial.ends) && typeof trial.active === "boolean";
7788
+ return { badge: badge === true, trial: valid ? { started: trial.started, ends: trial.ends, active: trial.active } : null };
7789
+ };
7790
+ function fetchDomainStatus(host = currentHost()) {
7791
+ if (isDevHost(host) || typeof fetch !== "function") return Promise.resolve(null);
7792
+ const pending = statusChecks.get(host);
7793
+ if (pending) return pending;
7722
7794
  const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7723
7795
  let store = null;
7724
7796
  try {
7725
7797
  store = typeof localStorage !== "undefined" ? localStorage : null;
7726
- const cached = JSON.parse(store?.getItem("freegrid:badge") ?? "null");
7727
- if (cached && cached.host === host && cached.day === day) return badgeCheck = Promise.resolve(cached.show);
7798
+ const cached = JSON.parse(store?.getItem("freegrid:status") ?? "null");
7799
+ const status = cached && cached.host === host && cached.day === day ? parseStatus(cached.status) : null;
7800
+ if (status) {
7801
+ const hit = Promise.resolve(status);
7802
+ statusChecks.set(host, hit);
7803
+ return hit;
7804
+ }
7728
7805
  } catch {
7729
7806
  }
7730
- badgeCheck = fetch(STATUS_URL, { mode: "cors", credentials: "omit" }).then((r) => r.ok ? r.json() : { badge: false }).then((d) => {
7731
- const show = d.badge === true;
7732
- try {
7733
- store?.setItem("freegrid:badge", JSON.stringify({ host, day, show }));
7734
- } catch {
7807
+ const check = fetch(STATUS_URL, { mode: "cors", credentials: "omit" }).then((r) => r.ok ? r.json() : null).then((d) => {
7808
+ const status = parseStatus(d);
7809
+ if (status) {
7810
+ try {
7811
+ store?.setItem("freegrid:status", JSON.stringify({ host, day, status }));
7812
+ } catch {
7813
+ }
7735
7814
  }
7736
- return show;
7737
- }, () => false);
7738
- return badgeCheck;
7815
+ return status;
7816
+ }).catch(() => null);
7817
+ statusChecks.set(host, check);
7818
+ return check;
7819
+ }
7820
+ function badgeRequested(host = currentHost()) {
7821
+ return fetchDomainStatus(host).then((s) => s?.badge === true);
7739
7822
  }
7740
- async function needsFreeBadge(key, host = typeof location !== "undefined" ? location.hostname : "") {
7823
+ async function needsFreeBadge(key, host = currentHost()) {
7824
+ await Promise.resolve();
7741
7825
  if (await checkLicense(key, host)) return false;
7742
7826
  return badgeRequested(host);
7743
7827
  }
7828
+ var GATE_KEY = "freegrid:gate";
7829
+ var MAX_VERIFIED_KEYS = 5;
7830
+ var keyIdOf = (key) => {
7831
+ if (!key) return "";
7832
+ const parts = key.trim().split(".");
7833
+ return parts[parts.length - 1].slice(-16);
7834
+ };
7835
+ var readGate = (host) => {
7836
+ const c = JSON.parse(localStorage.getItem(GATE_KEY) ?? "null");
7837
+ if (!c || typeof c !== "object" || c.host !== host || typeof c.trialEnded !== "boolean" || !Array.isArray(c.verifiedKeys) || !c.verifiedKeys.every((k) => typeof k === "string")) return null;
7838
+ return { host, trialEnded: c.trialEnded, ...typeof c.trialEnds === "string" ? { trialEnds: c.trialEnds } : {}, verifiedKeys: c.verifiedKeys };
7839
+ };
7840
+ function paidAllowed(key, host = currentHost()) {
7841
+ if (isDevHost(host)) return true;
7842
+ const k = key ?? globalKey;
7843
+ if (k && !hasSubtle()) {
7844
+ const checked = checkTerms(k, host);
7845
+ if ("info" in checked && !checked.fail) return true;
7846
+ }
7847
+ try {
7848
+ const cache2 = readGate(host);
7849
+ if (!cache2) return true;
7850
+ const id = keyIdOf(k);
7851
+ if (id && cache2.verifiedKeys.includes(id)) return true;
7852
+ return !cache2.trialEnded;
7853
+ } catch {
7854
+ return true;
7855
+ }
7856
+ }
7857
+ var updateGate = (host, patch) => {
7858
+ try {
7859
+ let cur = null;
7860
+ try {
7861
+ cur = readGate(host);
7862
+ } catch {
7863
+ }
7864
+ localStorage.setItem(GATE_KEY, JSON.stringify(patch(cur ?? { host, trialEnded: false, verifiedKeys: [] })));
7865
+ } catch {
7866
+ }
7867
+ };
7868
+ async function resolveGate(key, host = currentHost()) {
7869
+ if (isDevHost(host)) return true;
7870
+ await Promise.resolve();
7871
+ const k = key ?? globalKey;
7872
+ const state = await keyState(k, host);
7873
+ if (state === "ok") {
7874
+ const id = keyIdOf(k);
7875
+ updateGate(host, (c) => ({ ...c, verifiedKeys: [id, ...c.verifiedKeys.filter((x) => x !== id)].slice(0, MAX_VERIFIED_KEYS) }));
7876
+ return true;
7877
+ }
7878
+ if (state === "unsupported") return true;
7879
+ const trial = (await fetchDomainStatus(host))?.trial;
7880
+ if (!trial) return true;
7881
+ updateGate(host, (c) => ({ ...c, trialEnded: !trial.active, trialEnds: trial.ends }));
7882
+ const notice = `${trial.active ? "trial" : "ended"}@${host}`;
7883
+ if (!gateNotices.has(notice)) {
7884
+ gateNotices.add(notice);
7885
+ if (trial.active) console.info(NOTICE.trial(trial.ends));
7886
+ else console.warn(NOTICE.ended);
7887
+ }
7888
+ return trial.active;
7889
+ }
7890
+ function paidBlocked(feature) {
7891
+ if (blockedWarned.has(feature)) return;
7892
+ blockedWarned.add(feature);
7893
+ const name = PAID_FEATURES[feature] ?? { en: feature, ko: feature };
7894
+ console.warn(`FreeGrid: ${name.en} is a paid feature and is off on this domain. Get a free personal key (${PERSONAL_URL}) or a company key: ${PRICING_URL}
7895
+ FreeGrid: ${name.ko}\uC740(\uB294) \uC720\uB8CC \uAE30\uB2A5\uC774\uB77C \uC774 \uB3C4\uBA54\uC778\uC5D0\uC11C \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4. \uBB34\uB8CC \uAC1C\uC778 \uD0A4(${PERSONAL_URL}) \uB610\uB294 \uD68C\uC0AC \uD0A4\uB97C \uBC1B\uC73C\uC138\uC694: ${PRICING_URL}`);
7896
+ }
7744
7897
 
7745
7898
  // src/interactions/menu.ts
7746
7899
  var OPS = {
@@ -7785,7 +7938,7 @@ var ColumnMenu = class {
7785
7938
  [loc.unpin, () => this.grid.setPinned(colId, null)]
7786
7939
  ]));
7787
7940
  const grouped = this.grid.getGroupBy();
7788
- if (!this.grid.options.treeData && (this.grid.options.groupPanel || grouped.length)) {
7941
+ if (!this.grid.options.treeData && this.grid.paid && (this.grid.options.groupPanel || grouped.length)) {
7789
7942
  const g = grouped.includes(colId) ? [[loc.ungroup, () => this.grid.setGroupBy(grouped.filter((x) => x !== colId))]] : [[loc.groupByThis, () => this.grid.setGroupBy([...grouped, colId])]];
7790
7943
  if (grouped.length) g.push([loc.expandAll, () => this.grid.expandAll(true)], [loc.collapseAll, () => this.grid.expandAll(false)]);
7791
7944
  popup.append(this.section(g));
@@ -7796,7 +7949,7 @@ var ColumnMenu = class {
7796
7949
  ];
7797
7950
  if (!def.lockVisible) items.push([loc.hideColumn, () => this.grid.setColumnsVisible([colId], false)]);
7798
7951
  items.push([loc.chooseColumns, () => this.grid.chooser.open()], [loc.resetColumns, () => this.grid.resetColumns()]);
7799
- if (this.grid.options.conditionalFormatMenu !== false) items.push([`${loc.cf.title}\u2026`, () => this.grid.formatDialog.open(colId)]);
7952
+ if (this.grid.options.conditionalFormatMenu !== false && this.grid.paid) items.push([`${loc.cf.title}\u2026`, () => this.grid.formatDialog.open(colId)]);
7800
7953
  popup.append(this.section(items));
7801
7954
  const filter = this.filterSection(colId, () => this.close());
7802
7955
  if (filter) popup.append(filter);
@@ -9704,6 +9857,7 @@ var Grid = class {
9704
9857
  this.pageSize = DEFAULT_PAGE_SIZE;
9705
9858
  /** What formulas see: every source row, and computed columns by id. */
9706
9859
  this.formulaScope = {
9860
+ off: () => !this.allow("formulas"),
9707
9861
  rows: () => this.store.state.data,
9708
9862
  version: () => this.editVersion,
9709
9863
  computed: (name) => {
@@ -9768,6 +9922,7 @@ var Grid = class {
9768
9922
  this.selectionVersion = 0;
9769
9923
  /** Rows select-all acts on: displayed data rows that are selectable. */
9770
9924
  this.selectableCache = null;
9925
+ this.paid = paidAllowed(options.licenseKey);
9771
9926
  if (!Array.isArray(options.columns)) warn("options.columns must be an array");
9772
9927
  if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
9773
9928
  this.store = new Store({
@@ -9809,12 +9964,12 @@ var Grid = class {
9809
9964
  collator: () => this.collator(),
9810
9965
  textVersion: () => `${this.store.state.locale.code}|${this.editVersion}`
9811
9966
  });
9812
- this.groupBy = this.validGroupBy(options.groupBy ?? []);
9813
- 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] }));
9814
- this.defaultExpanded = options.groupDefaultExpanded ?? 0;
9967
+ if (options.groupBy?.length && this.allow(this.rowModel.server ? "serverGroups" : "grouping")) this.groupBy = this.validGroupBy(options.groupBy);
9968
+ if (Array.isArray(options.conditionalFormats) && options.conditionalFormats.length && this.allow("conditional")) this.cfRules = options.conditionalFormats.filter((r) => r && typeof r === "object" && Array.isArray(r.columns)).map((r) => ({ ...r, columns: [...r.columns] }));
9969
+ this.defaultExpanded = options.treeData || this.paid ? options.groupDefaultExpanded ?? 0 : 0;
9815
9970
  if (options.treeData && !options.getRowId) warn("treeData needs getRowId so parent ids can be matched");
9816
9971
  this.updateRows();
9817
- this.summaryOpts = options.summary ?? null;
9972
+ this.summaryOpts = options.summary && this.allow("summary") ? options.summary : null;
9818
9973
  this.summarySnapshot = snapshot(this.summaryOpts);
9819
9974
  this.refreshSummary();
9820
9975
  const explicit = options.fontSize != null && options.fontSize > 0 ? options.fontSize : null;
@@ -9835,9 +9990,9 @@ var Grid = class {
9835
9990
  this.editor = new CellEditor(this);
9836
9991
  this.finder = new Finder(this);
9837
9992
  this.fillHandle = new FillHandle(this);
9838
- this.groupPanel = options.groupPanel ? new GroupPanel(this) : null;
9993
+ this.groupPanel = options.groupPanel && this.allow("grouping") ? new GroupPanel(this) : null;
9839
9994
  this.pinnedRows = { top: cleanRows(options.pinnedTopRows ?? []), bottom: cleanRows(options.pinnedBottomRows ?? []) };
9840
- this.sideBar = options.sideBar ? new SideBar(
9995
+ this.sideBar = options.sideBar && this.allow("sideBar") ? new SideBar(
9841
9996
  this,
9842
9997
  typeof options.sideBar === "object" && !!options.sideBar.open,
9843
9998
  (typeof options.sideBar === "object" ? options.sideBar.panels : void 0) ?? (options.pivot || options.pivotPanel ? ["columns", "filters", "pivot"] : void 0)
@@ -9870,11 +10025,23 @@ var Grid = class {
9870
10025
  this.renderer.render();
9871
10026
  this.api = this.createApi();
9872
10027
  liveGrids.set(this.renderer.root, this);
10028
+ apiGrids.set(this.api, this);
10029
+ void resolveGate(options.licenseKey);
9873
10030
  }
9874
10031
  /** Bumped whenever cell values change (edits, transactions, undo). */
9875
10032
  get valuesVersion() {
9876
10033
  return this.editVersion;
9877
10034
  }
10035
+ /** Cell ranges are on: not switched off and paid features allowed. Closed behaves exactly like `rangeSelection: false`. */
10036
+ rangesOn() {
10037
+ return this.options.rangeSelection !== false && this.paid;
10038
+ }
10039
+ /** Whether a paid feature may run; warns once per feature when it may not. */
10040
+ allow(feature) {
10041
+ if (this.paid) return true;
10042
+ paidBlocked(feature);
10043
+ return false;
10044
+ }
9878
10045
  /** The column definitions as given (setColumns), without the system columns. */
9879
10046
  columnDefs() {
9880
10047
  return [...this.sourceDefs];
@@ -9887,6 +10054,7 @@ var Grid = class {
9887
10054
  warn("setConditionalFormats: formats must be an array");
9888
10055
  return;
9889
10056
  }
10057
+ if (list.length && !this.allow("conditional")) return;
9890
10058
  this.cfRules = list.filter((r) => r && typeof r === "object" && Array.isArray(r.columns)).map((r) => ({ ...r, columns: [...r.columns] }));
9891
10059
  this.renderer.invalidate({ rows: true });
9892
10060
  this.events.emit("conditionalFormatsChange", { formats: this.getConditionalFormats() });
@@ -9984,6 +10152,7 @@ var Grid = class {
9984
10152
  return out;
9985
10153
  }
9986
10154
  setPivot(cfg) {
10155
+ if (cfg && !this.allow("pivot")) return;
9987
10156
  if (cfg && this.clientOnly("pivot")) return;
9988
10157
  if (cfg && (typeof cfg !== "object" || !Array.isArray(cfg.rows) || !Array.isArray(cfg.values))) {
9989
10158
  warn("setPivot: expected { rows: string[], values: [...] }");
@@ -10239,6 +10408,7 @@ var Grid = class {
10239
10408
  }
10240
10409
  setAdvancedFilter(filter) {
10241
10410
  const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
10411
+ if (next && !this.allow("advancedFilter")) return;
10242
10412
  if (next === this.store.state.advancedFilter) return;
10243
10413
  this.store.set({ advancedFilter: next });
10244
10414
  this.events.emit("advancedFilterChange", { advancedFilter: next });
@@ -10450,6 +10620,7 @@ var Grid = class {
10450
10620
  return [...this.groupBy];
10451
10621
  }
10452
10622
  setGroupBy(ids) {
10623
+ if (Array.isArray(ids) && ids.length && !this.allow(this.rowModel.server ? "serverGroups" : "grouping")) return;
10453
10624
  const next = this.validGroupBy(ids);
10454
10625
  if (next.join("") === this.groupBy.join("")) return;
10455
10626
  this.groupBy = next;
@@ -10735,6 +10906,7 @@ var Grid = class {
10735
10906
  warn("setSummary: summary.rows must be an array");
10736
10907
  return;
10737
10908
  }
10909
+ if (opts && !this.allow("summary")) return;
10738
10910
  if (same2(opts, this.summarySnapshot)) {
10739
10911
  this.summaryOpts = opts;
10740
10912
  return;
@@ -10759,13 +10931,14 @@ var Grid = class {
10759
10931
  return this.columns.displayed().filter((c) => !isSystemCol(c.id));
10760
10932
  }
10761
10933
  validRange(range) {
10762
- if (!range || this.options.rangeSelection === false) return null;
10934
+ if (!range || !this.rangesOn()) return null;
10763
10935
  const count = this.rowModel.getRowCount();
10764
10936
  const ok = (p) => !!p && !isSystemCol(p.colId) && !!this.columns.getColumn(p.colId) && p.rowIndex >= 0 && p.rowIndex < count;
10765
10937
  return ok(range.start) && ok(range.end) ? { start: { ...range.start }, end: { ...range.end } } : null;
10766
10938
  }
10767
10939
  /** Set the active range. Earlier Ctrl ranges are dropped unless `keepOthers`. */
10768
10940
  setRange(range, keepOthers = false) {
10941
+ if (range && (range.start?.rowIndex !== range.end?.rowIndex || range.start?.colId !== range.end?.colId) && !this.allow("range")) return;
10769
10942
  const next = this.validRange(range);
10770
10943
  const extras = keepOthers || !this.store.state.extraRanges.length ? this.store.state.extraRanges : [];
10771
10944
  const cur = this.store.state.range;
@@ -10775,6 +10948,7 @@ var Grid = class {
10775
10948
  }
10776
10949
  /** Ctrl+click / Ctrl+drag: keep what is selected (the focused cell when there is no range) and start a new range. */
10777
10950
  addRange(range) {
10951
+ if (!this.allow("range")) return;
10778
10952
  const { range: active, focus } = this.store.state;
10779
10953
  const keep = this.validRange(active ?? (focus ? { start: focus, end: focus } : null));
10780
10954
  if (keep) this.store.set({ extraRanges: [...this.store.state.extraRanges, keep] });
@@ -10786,6 +10960,7 @@ var Grid = class {
10786
10960
  return all2.map((r) => ({ start: { ...r.start }, end: { ...r.end } }));
10787
10961
  }
10788
10962
  setRanges(ranges) {
10963
+ if (Array.isArray(ranges) && ranges.length && !this.allow("range")) return;
10789
10964
  const list = (Array.isArray(ranges) ? ranges : []).map((r) => this.validRange(r)).filter((r) => !!r);
10790
10965
  this.store.set({ extraRanges: list.slice(0, -1) });
10791
10966
  this.setRange(list[list.length - 1] ?? null, true);
@@ -10809,6 +10984,7 @@ var Grid = class {
10809
10984
  return !!col && this.allRangeBounds().some((b) => rangeContains(b, rowIndex, col.index));
10810
10985
  }
10811
10986
  selectAllCells() {
10987
+ if (!this.allow("range")) return;
10812
10988
  const cols = this.dataColumns();
10813
10989
  const count = this.rowModel.getRowCount();
10814
10990
  if (!cols.length || !count) return;
@@ -10818,6 +10994,7 @@ var Grid = class {
10818
10994
  // ── clipboard ───────────────────────────────────────────────
10819
10995
  /** TSV for the range, else the focused cell; `rows` copies the checked rows instead. */
10820
10996
  selectionTSV(headers, rows = false) {
10997
+ if (!rows && this.multiCell() && !this.allow("clipboard")) return "";
10821
10998
  const code = this.store.state.locale.code;
10822
10999
  const displayed = this.columns.displayed();
10823
11000
  const b = rows ? null : this.rangeBounds();
@@ -10859,6 +11036,11 @@ var Grid = class {
10859
11036
  if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
10860
11037
  return toTSV(matrix);
10861
11038
  }
11039
+ /** The selection covers more than one cell (several ranges, or one range wider than a cell). */
11040
+ multiCell() {
11041
+ const all2 = this.allRangeBounds();
11042
+ return all2.length > 1 || all2.some((b) => b.r1 !== b.r2 || b.c1 !== b.c2);
11043
+ }
10862
11044
  /** Like Excel: ranges stack when they share columns or rows; otherwise only the active one is copied. */
10863
11045
  rangesStack(all2) {
10864
11046
  return all2.length > 1 && (all2.every((x) => x.c1 === all2[0].c1 && x.c2 === all2[0].c2) || all2.every((x) => x.r1 === all2[0].r1 && x.r2 === all2[0].r2));
@@ -10909,6 +11091,7 @@ var Grid = class {
10909
11091
  const matrix = parseTSV(text2);
10910
11092
  const result = { changed: 0, skipped: 0 };
10911
11093
  if (!matrix.length) return result;
11094
+ if ((matrix.length > 1 || matrix[0].length > 1) && !this.allow("clipboard")) return result;
10912
11095
  const displayed = this.columns.displayed();
10913
11096
  const count = this.rowModel.getRowCount();
10914
11097
  const b = this.rangeBounds();
@@ -11017,6 +11200,7 @@ var Grid = class {
11017
11200
  /** Merge blocks for every `merge` column over the displayed rows (memoized). */
11018
11201
  mergeSpans() {
11019
11202
  if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan)) return NO_SPANS;
11203
+ if (!this.allow("merge")) return NO_SPANS;
11020
11204
  const rows = this.rowModel.rows();
11021
11205
  const ver = this.editVersion * 1e6 + this.columns.displayed().length;
11022
11206
  if (this.spanCache && this.spanCache.rows === rows && this.spanCache.ver === ver) return this.spanCache.spans;
@@ -11031,7 +11215,7 @@ var Grid = class {
11031
11215
  }
11032
11216
  rangeAggregates() {
11033
11217
  const all2 = this.allRangeBounds();
11034
- if (!all2.length) return null;
11218
+ if (!all2.length || !this.paid) return null;
11035
11219
  const displayed = this.columns.displayed();
11036
11220
  const key = [JSON.stringify(all2), this.rowModel.rows(), this.editVersion, ...displayed.map((c) => c.def)];
11037
11221
  const memo = this.aggMemo;
@@ -11070,6 +11254,7 @@ var Grid = class {
11070
11254
  }
11071
11255
  /** Fill from the current selection into `target`, Excel-style (one undo step). */
11072
11256
  fillRange(target) {
11257
+ if (!this.allow("fill")) return;
11073
11258
  const src = this.rangeBounds() ?? (() => {
11074
11259
  const f = this.store.state.focus;
11075
11260
  const col = f && this.columns.getColumn(f.colId);
@@ -11655,11 +11840,13 @@ var Grid = class {
11655
11840
  this.forcedOverlay = kind === "loading" || kind === "noRows" ? kind : null;
11656
11841
  this.renderer.invalidate();
11657
11842
  }),
11658
- getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
11843
+ // closed: an empty sheet, so writeXlsx([api.getXlsxSheet()]) is not an Excel export
11844
+ getXlsxSheet: guard((o) => this.allow("excel") ? buildXlsxSheet(this, o) : { name: o?.sheetName ?? "Sheet1", rows: [] }),
11659
11845
  print: guard((o) => printHtml(buildPrintHtml(this, o))),
11660
11846
  getPrintHtml: guard((o) => buildPrintHtml(this, o)),
11661
- getPdf: guard((o) => renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality })),
11847
+ getPdf: guard((o) => this.allow("pdf") ? renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality }) : null),
11662
11848
  exportPdf: guard((name = "export.pdf", o) => {
11849
+ if (!this.allow("pdf")) return;
11663
11850
  const bytes = renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality });
11664
11851
  if (bytes) download(name, [bytes], "application/pdf");
11665
11852
  else warn("exportPdf: this browser cannot draw on a canvas");
@@ -11667,16 +11854,26 @@ var Grid = class {
11667
11854
  setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
11668
11855
  setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
11669
11856
  getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
11670
- openFilterBuilder: guard(() => this.filterBuilder.open()),
11857
+ openFilterBuilder: guard(() => {
11858
+ if (!this.allow("advancedFilter")) return;
11859
+ this.filterBuilder.open();
11860
+ }),
11671
11861
  isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
11672
11862
  applyTransaction: guard((tx) => this.applyTransaction(tx)),
11673
11863
  applyTransactionAsync: guard((tx) => this.applyTransactionAsync(tx)),
11674
11864
  flushAsyncTransactions: guard(() => this.flushAsyncTransactions()),
11675
11865
  setConditionalFormats: guard((f) => this.setConditionalFormats(f)),
11676
11866
  getConditionalFormats: guard(() => this.getConditionalFormats()),
11677
- openConditionalFormats: guard((colId) => this.formatDialog.open(colId)),
11678
- askAssistant: guard((prompt) => askAssistant(this, prompt, this.options.assistant)),
11679
- openAssistant: guard(() => this.assistantPanel.open()),
11867
+ openConditionalFormats: guard((colId) => {
11868
+ if (!this.allow("conditional")) return;
11869
+ this.formatDialog.open(colId);
11870
+ }),
11871
+ // closed: as if `assistant` were not set
11872
+ askAssistant: guard((prompt) => askAssistant(this, prompt, this.allow("assistant") ? this.options.assistant : void 0)),
11873
+ openAssistant: guard(() => {
11874
+ if (!this.allow("assistant")) return;
11875
+ this.assistantPanel.open();
11876
+ }),
11680
11877
  getState: guard(() => this.getState()),
11681
11878
  setState: guard((s, o) => this.setState(s, o)),
11682
11879
  getScrollPosition: guard(() => this.renderer.getScroll()),
@@ -11690,7 +11887,10 @@ var Grid = class {
11690
11887
  resetColumnState: guard(() => this.resetColumns()),
11691
11888
  openColumnChooser: guard(() => this.chooser.open()),
11692
11889
  closeColumnChooser: guard(() => this.chooser.close()),
11693
- toggleSideBar: guard((open, panel) => this.sideBar?.toggle(open, panel)),
11890
+ toggleSideBar: guard((open, panel) => {
11891
+ if (!this.allow("sideBar")) return;
11892
+ this.sideBar?.toggle(open, panel);
11893
+ }),
11694
11894
  setSummary: guard((s) => this.setSummary(s)),
11695
11895
  getSummaryValues: guard(() => this.summaryValues.map((v) => ({ ...v }))),
11696
11896
  getRange: guard(() => {
@@ -11720,7 +11920,7 @@ var Grid = class {
11720
11920
  canRedo: guard(() => this.history.canRedo()),
11721
11921
  fillRange: guard((t) => this.fillRange(t)),
11722
11922
  setPinnedRows: guard((r) => this.setPinnedRows(r)),
11723
- find: guard((text2) => this.finder.find(text2)),
11923
+ find: guard((text2) => this.allow("find") ? this.finder.find(text2) : 0),
11724
11924
  findNext: guard(() => this.finder.next()),
11725
11925
  findPrevious: guard(() => this.finder.previous()),
11726
11926
  clearFind: guard(() => this.finder.clear()),
@@ -11741,13 +11941,17 @@ var Grid = class {
11741
11941
  refreshRows: guard(() => this.refreshRows()),
11742
11942
  setPivot: guard((p) => this.setPivot(p)),
11743
11943
  getPivot: guard(() => this.getPivot()),
11744
- createChart: guard((o) => this.charts.create(o)),
11944
+ // closed: an inert handle whose methods do nothing
11945
+ createChart: guard((o) => this.allow("charts") ? this.charts.create(o) : { el: el("div", "fg-chart-body"), setType: () => void 0, update: () => void 0, destroy: () => void 0 }),
11745
11946
  getCsv: guard((o) => buildCsv(this, o)),
11746
11947
  getJson: guard((o) => buildJson(this, o)),
11747
11948
  exportJson: guard((name = "export.json", o) => download(name, [buildJson(this, o)], "application/json;charset=utf-8")),
11748
11949
  exportCsv: guard((name = "export.csv", o) => download(name, ["\uFEFF", buildCsv(this, o)], "text/csv;charset=utf-8")),
11749
- getXlsx: guard((o) => buildXlsx(this, o)),
11750
- exportXlsx: guard((name = "export.xlsx", o) => download(name, [buildXlsx(this, o)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")),
11950
+ getXlsx: guard((o) => this.allow("excel") ? buildXlsx(this, o) : new Uint8Array(0)),
11951
+ exportXlsx: guard((name = "export.xlsx", o) => {
11952
+ if (!this.allow("excel")) return;
11953
+ download(name, [buildXlsx(this, o)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
11954
+ }),
11751
11955
  importFile: guard((file, o) => importInto(this, file, o)),
11752
11956
  on: guard((type, fn) => this.events.on(type, fn)),
11753
11957
  destroy: () => this.destroy()
@@ -11775,10 +11979,13 @@ function buildWorkbook(sheets) {
11775
11979
  warn("buildWorkbook: no grids given; the file has one empty sheet");
11776
11980
  return writeXlsx([{ name: "Sheet1", rows: [] }]);
11777
11981
  }
11982
+ if (list.some((s) => gridOfApi(s.grid)?.allow("workbook") === false)) return new Uint8Array(0);
11778
11983
  return writeXlsx(list.map((s, i) => s.grid.getXlsxSheet({ ...s.options, sheetName: s.sheetName ?? s.options?.sheetName ?? `Sheet${i + 1}` })));
11779
11984
  }
11780
11985
  function exportWorkbook(filename, sheets) {
11781
- download(filename || "export.xlsx", [buildWorkbook(sheets)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
11986
+ const bytes = buildWorkbook(sheets);
11987
+ if (!bytes.length) return;
11988
+ download(filename || "export.xlsx", [bytes], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
11782
11989
  }
11783
11990
 
11784
11991
  // src/rest.ts