@freegridjs/core 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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
@@ -3492,7 +3495,7 @@ function attachBody(grid) {
3492
3495
  const data = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
3493
3496
  if (data.length) {
3494
3497
  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 } });
3498
+ if (grid.rangesOn()) grid.setRange({ start: { rowIndex: ev.rowIndex, colId: data[0].id }, end: { rowIndex: ev.rowIndex, colId: data[data.length - 1].id } });
3496
3499
  }
3497
3500
  return;
3498
3501
  }
@@ -3501,9 +3504,9 @@ function attachBody(grid) {
3501
3504
  const prev = grid.store.state.focus;
3502
3505
  const onBox = ev.colId === SELECT_COL_ID;
3503
3506
  if (dragged) {
3504
- } else if (e.shiftKey && prev && !onBox && !isSystemCol(prev.colId) && grid.options.rangeSelection !== false) {
3507
+ } else if (e.shiftKey && prev && !onBox && !isSystemCol(prev.colId) && grid.rangesOn()) {
3505
3508
  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) {
3509
+ } else if ((e.ctrlKey || e.metaKey) && !onBox && !isSystemCol(ev.colId) && grid.rangesOn()) {
3507
3510
  const at = { rowIndex: ev.rowIndex, colId: ev.colId };
3508
3511
  grid.addRange({ start: at, end: at });
3509
3512
  grid.setFocus(ev.rowIndex, ev.colId);
@@ -3569,7 +3572,7 @@ function attachBody(grid) {
3569
3572
  grid.selectAllCells();
3570
3573
  return true;
3571
3574
  case "find":
3572
- if (grid.options.findBar === false) return false;
3575
+ if (grid.options.findBar === false || !grid.paid) return false;
3573
3576
  grid.finder.openBar();
3574
3577
  return true;
3575
3578
  case "paste":
@@ -3679,7 +3682,7 @@ function attachBody(grid) {
3679
3682
  clearTimeout(pressTimer);
3680
3683
  pressTimer = window.setTimeout(() => {
3681
3684
  cancel();
3682
- if (grid.options.contextMenu === false) return;
3685
+ if (!grid.contextMenu.enabled()) return;
3683
3686
  suppressClick = true;
3684
3687
  if (!isSystemCol(hit.colId)) {
3685
3688
  grid.setFocus(hit.rowIndex, hit.colId);
@@ -3698,7 +3701,7 @@ function attachBody(grid) {
3698
3701
  return;
3699
3702
  }
3700
3703
  if (e.button === 0 && onHandleDown(e)) return;
3701
- if (grid.options.rangeSelection === false) return;
3704
+ if (!grid.rangesOn()) return;
3702
3705
  if (e.button !== 0 || e.shiftKey || e.target.closest("input, a, button")) return;
3703
3706
  const start = renderer.cellAt(e.target);
3704
3707
  if (!start || isSystemCol(start.colId)) return;
@@ -3746,7 +3749,7 @@ function attachBody(grid) {
3746
3749
  grid.setRange(null);
3747
3750
  return;
3748
3751
  }
3749
- if (ctrl && e.key.toLowerCase() === "a" && grid.options.rangeSelection !== false) {
3752
+ if (ctrl && e.key.toLowerCase() === "a" && grid.rangesOn()) {
3750
3753
  e.preventDefault();
3751
3754
  grid.selectAllCells();
3752
3755
  return;
@@ -3763,7 +3766,7 @@ function attachBody(grid) {
3763
3766
  }
3764
3767
  const size = { rows: count, cols: cols.length, pageRows: renderer.pageRows() };
3765
3768
  const firstData = cols.findIndex((c) => !isSystemCol(c.id));
3766
- const ranges = grid.options.rangeSelection !== false;
3769
+ const ranges = grid.rangesOn();
3767
3770
  if (ranges && e.shiftKey && e.key !== "Tab" && NAV_KEYS.has(e.key)) {
3768
3771
  const anchor = f ?? { rowIndex: 0, colId: cols[firstData].id };
3769
3772
  const range = grid.store.state.range;
@@ -4264,6 +4267,10 @@ var ContextMenu = class {
4264
4267
  this.close();
4265
4268
  this.detach();
4266
4269
  }
4270
+ /** The cell menu is on: not switched off and the paid feature allowed (closed: the browser's menu shows). */
4271
+ enabled() {
4272
+ return this.grid.options.contextMenu !== false && this.grid.allow("contextMenu");
4273
+ }
4267
4274
  onContextMenu(e) {
4268
4275
  const { grid } = this;
4269
4276
  if (grid.options.contextMenu === false) return;
@@ -4278,7 +4285,7 @@ var ContextMenu = class {
4278
4285
  return;
4279
4286
  }
4280
4287
  const hit = grid.renderer.cellAt(target);
4281
- if (!hit) return;
4288
+ if (!hit || !this.enabled()) return;
4282
4289
  e.preventDefault();
4283
4290
  grid.renderer.gridEl.focus({ preventScroll: true });
4284
4291
  if (!isSystemCol(hit.colId) && !grid.rangeContains(hit.rowIndex, hit.colId)) {
@@ -4290,7 +4297,7 @@ var ContextMenu = class {
4290
4297
  /** Keyboard: Shift+F10 or the ContextMenu key. */
4291
4298
  openAtFocus() {
4292
4299
  const f = this.grid.store.state.focus;
4293
- if (!f || this.grid.options.contextMenu === false) return;
4300
+ if (!f || !this.enabled()) return;
4294
4301
  const r = this.grid.renderer.root.getBoundingClientRect();
4295
4302
  const cell = this.grid.renderer.root.querySelector(`.fg-row[data-row="${f.rowIndex}"] .fg-cell[data-col="${CSS.escape(f.colId)}"]`);
4296
4303
  const box = cell?.getBoundingClientRect() ?? r;
@@ -4333,7 +4340,7 @@ var ContextMenu = class {
4333
4340
  if (grid.options.rowSelection === "multiple") items.push({ label: loc.selectAllRows, action: () => grid.api.selectAll() });
4334
4341
  items.push({ label: loc.chooseColumns, action: () => grid.chooser.open() });
4335
4342
  const b = grid.rangeBounds();
4336
- if (b && (b.r2 > b.r1 || b.c2 > b.c1)) {
4343
+ if (b && (b.r2 > b.r1 || b.c2 > b.c1) && grid.paid) {
4337
4344
  items.push(
4338
4345
  "separator",
4339
4346
  { label: loc.chartBar, action: () => grid.api.createChart({ type: "bar" }) },
@@ -4344,12 +4351,12 @@ var ContextMenu = class {
4344
4351
  items.push(
4345
4352
  "separator",
4346
4353
  { label: loc.exportCsv, action: () => grid.api.exportCsv() },
4347
- { label: loc.exportExcel, action: () => grid.api.exportXlsx() },
4354
+ ...grid.paid ? [{ label: loc.exportExcel, action: () => grid.api.exportXlsx() }] : [],
4348
4355
  { 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() }
4356
+ ...grid.options.assistant?.url && grid.paid ? [{ label: `${loc.ai.title}\u2026`, action: () => grid.api.openAssistant() }] : [],
4357
+ ...grid.paid ? [{ label: loc.exportPdf, action: () => grid.api.exportPdf() }] : []
4351
4358
  );
4352
- if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
4359
+ if (grid.options.advancedFilter !== false && !grid.options.pivot && grid.paid) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
4353
4360
  items.push("separator", { label: loc.print, action: () => grid.api.print() });
4354
4361
  return items;
4355
4362
  }
@@ -4492,8 +4499,7 @@ var FillHandle = class {
4492
4499
  this.handle.addEventListener("pointerdown", this.onDown);
4493
4500
  }
4494
4501
  enabled() {
4495
- const o = this.grid.options;
4496
- return o.fillHandle !== false && o.rangeSelection !== false;
4502
+ return this.grid.options.fillHandle !== false && this.grid.rangesOn();
4497
4503
  }
4498
4504
  /** Current source block: the range, or the focused cell. */
4499
4505
  source() {
@@ -4670,7 +4676,7 @@ var Finder = class {
4670
4676
  this.countEl.textContent = !this.query ? "" : this.matches.length ? `${this.index + 1}/${this.matches.length}` : loc.noMatches;
4671
4677
  }
4672
4678
  openBar() {
4673
- if (this.grid.options.findBar === false) return;
4679
+ if (this.grid.options.findBar === false || !this.grid.paid) return;
4674
4680
  if (this.bar) {
4675
4681
  this.bar.querySelector("input").focus();
4676
4682
  return;
@@ -7603,12 +7609,36 @@ function buildPivot(source, config, defOf, labels, locale) {
7603
7609
  return { columns: [...rowColumns, ...valueColumns], rows, groupBy: cfg.rows.slice(0, -1) };
7604
7610
  }
7605
7611
 
7612
+ // src/features.ts
7613
+ var PAID_FEATURES = {
7614
+ grouping: { en: "Row grouping and aggregation", ko: "\uD589 \uADF8\uB8F9\xB7\uC9D1\uACC4" },
7615
+ pivot: { en: "Pivot", ko: "\uD53C\uBC97" },
7616
+ charts: { en: "Charts", ko: "\uCC28\uD2B8" },
7617
+ range: { en: "Cell range selection", ko: "\uC140 \uBC94\uC704 \uC120\uD0DD" },
7618
+ clipboard: { en: "Range copy and paste", ko: "\uBC94\uC704 \uBCF5\uC0AC\xB7\uBD99\uC5EC\uB123\uAE30" },
7619
+ fill: { en: "Fill handle", ko: "\uCC44\uC6B0\uAE30 \uD578\uB4E4" },
7620
+ excel: { en: "Excel export", ko: "\uC5D1\uC140 \uB0B4\uBCF4\uB0B4\uAE30" },
7621
+ pdf: { en: "PDF export", ko: "PDF \uB0B4\uBCF4\uB0B4\uAE30" },
7622
+ serverGroups: { en: "Server-side groups", ko: "\uC11C\uBC84 \uADF8\uB8F9" },
7623
+ advancedFilter: { en: "Advanced filter", ko: "\uACE0\uAE09 \uD544\uD130" },
7624
+ sideBar: { en: "Side bar panels", ko: "\uC0AC\uC774\uB4DC\uBC14 \uD328\uB110" },
7625
+ contextMenu: { en: "Context menu", ko: "\uCEE8\uD14D\uC2A4\uD2B8 \uBA54\uB274" },
7626
+ summary: { en: "Summary rows", ko: "\uD569\uACC4 \uD589" },
7627
+ formulas: { en: "Formulas", ko: "\uC218\uC2DD" },
7628
+ find: { en: "Find", ko: "\uCC3E\uAE30" },
7629
+ conditional: { en: "Conditional formatting", ko: "\uC870\uAC74\uBD80 \uC11C\uC2DD" },
7630
+ assistant: { en: "AI assistant", ko: "AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8" },
7631
+ merge: { en: "Merged cells", ko: "\uC140 \uBCD1\uD569" },
7632
+ workbook: { en: "Workbook", ko: "\uC6CC\uD06C\uBD81" }
7633
+ };
7634
+
7606
7635
  // src/version.ts
7607
- var version = "1.8.0";
7636
+ var version = "1.9.0";
7608
7637
 
7609
7638
  // src/license.ts
7610
7639
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
7611
7640
  var PRICING_URL = "https://freegrid.app/pricing/";
7641
+ var PERSONAL_URL = "https://freegrid.app/license/personal/";
7612
7642
  var USAGE_URL = "https://freegrid.app/api/usage";
7613
7643
  var STATUS_URL = "https://freegrid.app/api/domain-status";
7614
7644
  var majorOf = (v) => Number.parseInt(String(v).split(".")[0], 10);
@@ -7624,11 +7654,9 @@ function domainMatches(host, domains) {
7624
7654
  const h = host.toLowerCase().replace(/\.$/, "");
7625
7655
  return domains.some((d) => {
7626
7656
  const raw = d.toLowerCase().replace(/\.$/, "");
7627
- const wildcard = raw.startsWith("*.");
7628
- const dom = wildcard ? raw.slice(2) : raw;
7657
+ const dom = raw.startsWith("*.") ? raw.slice(2) : raw;
7629
7658
  if (!dom) return false;
7630
- if (h === dom || h === `www.${dom}`) return true;
7631
- return wildcard && h.endsWith(`.${dom}`);
7659
+ return h === dom || h.endsWith(`.${dom}`);
7632
7660
  });
7633
7661
  }
7634
7662
  var isDay = (s) => {
@@ -7646,6 +7674,7 @@ async function verifyLicense(key, host, opts = {}) {
7646
7674
  info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
7647
7675
  if (!Array.isArray(info.domains) || !isDay(info.expires)) return { ok: false, reason: "format" };
7648
7676
  if (info.major !== void 0 && !(Number.isInteger(info.major) && info.major > 0)) return { ok: false, reason: "format" };
7677
+ if (info.plan !== void 0 && info.plan !== "company" && info.plan !== "personal") return { ok: false, reason: "format" };
7649
7678
  } catch {
7650
7679
  return { ok: false, reason: "format" };
7651
7680
  }
@@ -7666,13 +7695,18 @@ async function verifyLicense(key, host, opts = {}) {
7666
7695
  }
7667
7696
  var globalKey;
7668
7697
  var statuses = /* @__PURE__ */ new Map();
7669
- var badgeCheck = null;
7698
+ var statusChecks = /* @__PURE__ */ new Map();
7699
+ var gateNotices = /* @__PURE__ */ new Set();
7700
+ var blockedWarned = /* @__PURE__ */ new Set();
7670
7701
  function setLicenseKey(key) {
7671
7702
  globalKey = key;
7672
7703
  }
7673
7704
  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}`,
7705
+ 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}
7706
+ 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}`,
7707
+ 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.`,
7708
+ 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}
7709
+ 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
7710
  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
7711
  signature: "FreeGrid: the license key could not be verified. / \uB77C\uC774\uC120\uC2A4 \uD0A4\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
7678
7712
  expired: "FreeGrid: the license key has expired. / \uB77C\uC774\uC120\uC2A4 \uAE30\uAC04\uC774 \uB05D\uB0AC\uC2B5\uB2C8\uB2E4.",
@@ -7716,31 +7750,98 @@ ${PRICING_URL}`);
7716
7750
  statuses.set(id, status);
7717
7751
  return status;
7718
7752
  }
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;
7753
+ var gateHost;
7754
+ var currentHost = () => gateHost ?? (typeof location !== "undefined" ? location.hostname : "");
7755
+ var parseStatus = (d) => {
7756
+ if (!d || typeof d !== "object") return null;
7757
+ const { badge, trial } = d;
7758
+ const valid = !!trial && typeof trial === "object" && isDay(trial.started) && isDay(trial.ends) && typeof trial.active === "boolean";
7759
+ return { badge: badge === true, trial: valid ? { started: trial.started, ends: trial.ends, active: trial.active } : null };
7760
+ };
7761
+ function fetchDomainStatus(host = currentHost()) {
7762
+ if (isDevHost(host) || typeof fetch !== "function") return Promise.resolve(null);
7763
+ const pending = statusChecks.get(host);
7764
+ if (pending) return pending;
7722
7765
  const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7723
7766
  let store = null;
7724
7767
  try {
7725
7768
  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);
7769
+ const cached = JSON.parse(store?.getItem("freegrid:status") ?? "null");
7770
+ const status = cached && cached.host === host && cached.day === day ? parseStatus(cached.status) : null;
7771
+ if (status) {
7772
+ const hit = Promise.resolve(status);
7773
+ statusChecks.set(host, hit);
7774
+ return hit;
7775
+ }
7728
7776
  } catch {
7729
7777
  }
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 {
7778
+ const check = fetch(STATUS_URL, { mode: "cors", credentials: "omit" }).then((r) => r.ok ? r.json() : null).then((d) => {
7779
+ const status = parseStatus(d);
7780
+ if (status) {
7781
+ try {
7782
+ store?.setItem("freegrid:status", JSON.stringify({ host, day, status }));
7783
+ } catch {
7784
+ }
7735
7785
  }
7736
- return show;
7737
- }, () => false);
7738
- return badgeCheck;
7786
+ return status;
7787
+ }).catch(() => null);
7788
+ statusChecks.set(host, check);
7789
+ return check;
7739
7790
  }
7740
- async function needsFreeBadge(key, host = typeof location !== "undefined" ? location.hostname : "") {
7791
+ function badgeRequested(host = currentHost()) {
7792
+ return fetchDomainStatus(host).then((s) => s?.badge === true);
7793
+ }
7794
+ async function needsFreeBadge(key, host = currentHost()) {
7741
7795
  if (await checkLicense(key, host)) return false;
7742
7796
  return badgeRequested(host);
7743
7797
  }
7798
+ var GATE_KEY = "freegrid:gate";
7799
+ var keyIdOf = (key) => {
7800
+ if (!key) return "";
7801
+ const parts = key.trim().split(".");
7802
+ return parts[parts.length - 1].slice(-16);
7803
+ };
7804
+ function paidAllowed(key, host = currentHost()) {
7805
+ if (isDevHost(host)) return true;
7806
+ try {
7807
+ const cache2 = JSON.parse(localStorage.getItem(GATE_KEY) ?? "null");
7808
+ if (!cache2 || cache2.host !== host || cache2.keyId !== keyIdOf(key ?? globalKey)) return true;
7809
+ return cache2.paid !== false;
7810
+ } catch {
7811
+ return true;
7812
+ }
7813
+ }
7814
+ var writeGate = (cache2) => {
7815
+ try {
7816
+ localStorage.setItem(GATE_KEY, JSON.stringify(cache2));
7817
+ } catch {
7818
+ }
7819
+ };
7820
+ async function resolveGate(key, host = currentHost()) {
7821
+ const k = key ?? globalKey;
7822
+ const keyId = keyIdOf(k);
7823
+ if (await checkLicense(k, host)) {
7824
+ writeGate({ host, keyId, paid: true });
7825
+ return true;
7826
+ }
7827
+ const trial = (await fetchDomainStatus(host))?.trial;
7828
+ if (!trial) return true;
7829
+ writeGate({ host, keyId, paid: trial.active, trialEnds: trial.ends });
7830
+ const notice = `${trial.active ? "trial" : "ended"}@${host}`;
7831
+ if (!gateNotices.has(notice)) {
7832
+ gateNotices.add(notice);
7833
+ if (trial.active) console.info(NOTICE.trial(trial.ends));
7834
+ else console.warn(NOTICE.ended);
7835
+ }
7836
+ return trial.active;
7837
+ }
7838
+ function paidBlocked(feature) {
7839
+ if (blockedWarned.has(feature)) return;
7840
+ blockedWarned.add(feature);
7841
+ const name = PAID_FEATURES[feature] ?? { en: feature, ko: feature };
7842
+ 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}
7843
+ 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}`);
7844
+ }
7744
7845
 
7745
7846
  // src/interactions/menu.ts
7746
7847
  var OPS = {
@@ -7785,7 +7886,7 @@ var ColumnMenu = class {
7785
7886
  [loc.unpin, () => this.grid.setPinned(colId, null)]
7786
7887
  ]));
7787
7888
  const grouped = this.grid.getGroupBy();
7788
- if (!this.grid.options.treeData && (this.grid.options.groupPanel || grouped.length)) {
7889
+ if (!this.grid.options.treeData && this.grid.paid && (this.grid.options.groupPanel || grouped.length)) {
7789
7890
  const g = grouped.includes(colId) ? [[loc.ungroup, () => this.grid.setGroupBy(grouped.filter((x) => x !== colId))]] : [[loc.groupByThis, () => this.grid.setGroupBy([...grouped, colId])]];
7790
7891
  if (grouped.length) g.push([loc.expandAll, () => this.grid.expandAll(true)], [loc.collapseAll, () => this.grid.expandAll(false)]);
7791
7892
  popup.append(this.section(g));
@@ -7796,7 +7897,7 @@ var ColumnMenu = class {
7796
7897
  ];
7797
7898
  if (!def.lockVisible) items.push([loc.hideColumn, () => this.grid.setColumnsVisible([colId], false)]);
7798
7899
  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)]);
7900
+ if (this.grid.options.conditionalFormatMenu !== false && this.grid.paid) items.push([`${loc.cf.title}\u2026`, () => this.grid.formatDialog.open(colId)]);
7800
7901
  popup.append(this.section(items));
7801
7902
  const filter = this.filterSection(colId, () => this.close());
7802
7903
  if (filter) popup.append(filter);
@@ -9704,6 +9805,7 @@ var Grid = class {
9704
9805
  this.pageSize = DEFAULT_PAGE_SIZE;
9705
9806
  /** What formulas see: every source row, and computed columns by id. */
9706
9807
  this.formulaScope = {
9808
+ off: () => !this.allow("formulas"),
9707
9809
  rows: () => this.store.state.data,
9708
9810
  version: () => this.editVersion,
9709
9811
  computed: (name) => {
@@ -9768,6 +9870,7 @@ var Grid = class {
9768
9870
  this.selectionVersion = 0;
9769
9871
  /** Rows select-all acts on: displayed data rows that are selectable. */
9770
9872
  this.selectableCache = null;
9873
+ this.paid = paidAllowed(options.licenseKey);
9771
9874
  if (!Array.isArray(options.columns)) warn("options.columns must be an array");
9772
9875
  if (options.data != null && !Array.isArray(options.data)) warn("options.data must be an array");
9773
9876
  this.store = new Store({
@@ -9809,12 +9912,12 @@ var Grid = class {
9809
9912
  collator: () => this.collator(),
9810
9913
  textVersion: () => `${this.store.state.locale.code}|${this.editVersion}`
9811
9914
  });
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;
9915
+ if (options.groupBy?.length && this.allow(this.rowModel.server ? "serverGroups" : "grouping")) this.groupBy = this.validGroupBy(options.groupBy);
9916
+ 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] }));
9917
+ this.defaultExpanded = options.treeData || this.paid ? options.groupDefaultExpanded ?? 0 : 0;
9815
9918
  if (options.treeData && !options.getRowId) warn("treeData needs getRowId so parent ids can be matched");
9816
9919
  this.updateRows();
9817
- this.summaryOpts = options.summary ?? null;
9920
+ this.summaryOpts = options.summary && this.allow("summary") ? options.summary : null;
9818
9921
  this.summarySnapshot = snapshot(this.summaryOpts);
9819
9922
  this.refreshSummary();
9820
9923
  const explicit = options.fontSize != null && options.fontSize > 0 ? options.fontSize : null;
@@ -9835,9 +9938,9 @@ var Grid = class {
9835
9938
  this.editor = new CellEditor(this);
9836
9939
  this.finder = new Finder(this);
9837
9940
  this.fillHandle = new FillHandle(this);
9838
- this.groupPanel = options.groupPanel ? new GroupPanel(this) : null;
9941
+ this.groupPanel = options.groupPanel && this.allow("grouping") ? new GroupPanel(this) : null;
9839
9942
  this.pinnedRows = { top: cleanRows(options.pinnedTopRows ?? []), bottom: cleanRows(options.pinnedBottomRows ?? []) };
9840
- this.sideBar = options.sideBar ? new SideBar(
9943
+ this.sideBar = options.sideBar && this.allow("sideBar") ? new SideBar(
9841
9944
  this,
9842
9945
  typeof options.sideBar === "object" && !!options.sideBar.open,
9843
9946
  (typeof options.sideBar === "object" ? options.sideBar.panels : void 0) ?? (options.pivot || options.pivotPanel ? ["columns", "filters", "pivot"] : void 0)
@@ -9870,11 +9973,23 @@ var Grid = class {
9870
9973
  this.renderer.render();
9871
9974
  this.api = this.createApi();
9872
9975
  liveGrids.set(this.renderer.root, this);
9976
+ apiGrids.set(this.api, this);
9977
+ void resolveGate(options.licenseKey);
9873
9978
  }
9874
9979
  /** Bumped whenever cell values change (edits, transactions, undo). */
9875
9980
  get valuesVersion() {
9876
9981
  return this.editVersion;
9877
9982
  }
9983
+ /** Cell ranges are on: not switched off and paid features allowed. Closed behaves exactly like `rangeSelection: false`. */
9984
+ rangesOn() {
9985
+ return this.options.rangeSelection !== false && this.paid;
9986
+ }
9987
+ /** Whether a paid feature may run; warns once per feature when it may not. */
9988
+ allow(feature) {
9989
+ if (this.paid) return true;
9990
+ paidBlocked(feature);
9991
+ return false;
9992
+ }
9878
9993
  /** The column definitions as given (setColumns), without the system columns. */
9879
9994
  columnDefs() {
9880
9995
  return [...this.sourceDefs];
@@ -9887,6 +10002,7 @@ var Grid = class {
9887
10002
  warn("setConditionalFormats: formats must be an array");
9888
10003
  return;
9889
10004
  }
10005
+ if (list.length && !this.allow("conditional")) return;
9890
10006
  this.cfRules = list.filter((r) => r && typeof r === "object" && Array.isArray(r.columns)).map((r) => ({ ...r, columns: [...r.columns] }));
9891
10007
  this.renderer.invalidate({ rows: true });
9892
10008
  this.events.emit("conditionalFormatsChange", { formats: this.getConditionalFormats() });
@@ -9984,6 +10100,7 @@ var Grid = class {
9984
10100
  return out;
9985
10101
  }
9986
10102
  setPivot(cfg) {
10103
+ if (cfg && !this.allow("pivot")) return;
9987
10104
  if (cfg && this.clientOnly("pivot")) return;
9988
10105
  if (cfg && (typeof cfg !== "object" || !Array.isArray(cfg.rows) || !Array.isArray(cfg.values))) {
9989
10106
  warn("setPivot: expected { rows: string[], values: [...] }");
@@ -10239,6 +10356,7 @@ var Grid = class {
10239
10356
  }
10240
10357
  setAdvancedFilter(filter) {
10241
10358
  const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
10359
+ if (next && !this.allow("advancedFilter")) return;
10242
10360
  if (next === this.store.state.advancedFilter) return;
10243
10361
  this.store.set({ advancedFilter: next });
10244
10362
  this.events.emit("advancedFilterChange", { advancedFilter: next });
@@ -10450,6 +10568,7 @@ var Grid = class {
10450
10568
  return [...this.groupBy];
10451
10569
  }
10452
10570
  setGroupBy(ids) {
10571
+ if (Array.isArray(ids) && ids.length && !this.allow(this.rowModel.server ? "serverGroups" : "grouping")) return;
10453
10572
  const next = this.validGroupBy(ids);
10454
10573
  if (next.join("") === this.groupBy.join("")) return;
10455
10574
  this.groupBy = next;
@@ -10735,6 +10854,7 @@ var Grid = class {
10735
10854
  warn("setSummary: summary.rows must be an array");
10736
10855
  return;
10737
10856
  }
10857
+ if (opts && !this.allow("summary")) return;
10738
10858
  if (same2(opts, this.summarySnapshot)) {
10739
10859
  this.summaryOpts = opts;
10740
10860
  return;
@@ -10759,13 +10879,14 @@ var Grid = class {
10759
10879
  return this.columns.displayed().filter((c) => !isSystemCol(c.id));
10760
10880
  }
10761
10881
  validRange(range) {
10762
- if (!range || this.options.rangeSelection === false) return null;
10882
+ if (!range || !this.rangesOn()) return null;
10763
10883
  const count = this.rowModel.getRowCount();
10764
10884
  const ok = (p) => !!p && !isSystemCol(p.colId) && !!this.columns.getColumn(p.colId) && p.rowIndex >= 0 && p.rowIndex < count;
10765
10885
  return ok(range.start) && ok(range.end) ? { start: { ...range.start }, end: { ...range.end } } : null;
10766
10886
  }
10767
10887
  /** Set the active range. Earlier Ctrl ranges are dropped unless `keepOthers`. */
10768
10888
  setRange(range, keepOthers = false) {
10889
+ if (range && (range.start?.rowIndex !== range.end?.rowIndex || range.start?.colId !== range.end?.colId) && !this.allow("range")) return;
10769
10890
  const next = this.validRange(range);
10770
10891
  const extras = keepOthers || !this.store.state.extraRanges.length ? this.store.state.extraRanges : [];
10771
10892
  const cur = this.store.state.range;
@@ -10775,6 +10896,7 @@ var Grid = class {
10775
10896
  }
10776
10897
  /** Ctrl+click / Ctrl+drag: keep what is selected (the focused cell when there is no range) and start a new range. */
10777
10898
  addRange(range) {
10899
+ if (!this.allow("range")) return;
10778
10900
  const { range: active, focus } = this.store.state;
10779
10901
  const keep = this.validRange(active ?? (focus ? { start: focus, end: focus } : null));
10780
10902
  if (keep) this.store.set({ extraRanges: [...this.store.state.extraRanges, keep] });
@@ -10786,6 +10908,7 @@ var Grid = class {
10786
10908
  return all2.map((r) => ({ start: { ...r.start }, end: { ...r.end } }));
10787
10909
  }
10788
10910
  setRanges(ranges) {
10911
+ if (Array.isArray(ranges) && ranges.length && !this.allow("range")) return;
10789
10912
  const list = (Array.isArray(ranges) ? ranges : []).map((r) => this.validRange(r)).filter((r) => !!r);
10790
10913
  this.store.set({ extraRanges: list.slice(0, -1) });
10791
10914
  this.setRange(list[list.length - 1] ?? null, true);
@@ -10809,6 +10932,7 @@ var Grid = class {
10809
10932
  return !!col && this.allRangeBounds().some((b) => rangeContains(b, rowIndex, col.index));
10810
10933
  }
10811
10934
  selectAllCells() {
10935
+ if (!this.allow("range")) return;
10812
10936
  const cols = this.dataColumns();
10813
10937
  const count = this.rowModel.getRowCount();
10814
10938
  if (!cols.length || !count) return;
@@ -10818,6 +10942,7 @@ var Grid = class {
10818
10942
  // ── clipboard ───────────────────────────────────────────────
10819
10943
  /** TSV for the range, else the focused cell; `rows` copies the checked rows instead. */
10820
10944
  selectionTSV(headers, rows = false) {
10945
+ if (!rows && this.multiCell() && !this.allow("clipboard")) return "";
10821
10946
  const code = this.store.state.locale.code;
10822
10947
  const displayed = this.columns.displayed();
10823
10948
  const b = rows ? null : this.rangeBounds();
@@ -10859,6 +10984,11 @@ var Grid = class {
10859
10984
  if (headers) matrix.unshift(cols.map((c) => c.def.header ?? c.def.field ?? c.id));
10860
10985
  return toTSV(matrix);
10861
10986
  }
10987
+ /** The selection covers more than one cell (several ranges, or one range wider than a cell). */
10988
+ multiCell() {
10989
+ const all2 = this.allRangeBounds();
10990
+ return all2.length > 1 || all2.some((b) => b.r1 !== b.r2 || b.c1 !== b.c2);
10991
+ }
10862
10992
  /** Like Excel: ranges stack when they share columns or rows; otherwise only the active one is copied. */
10863
10993
  rangesStack(all2) {
10864
10994
  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 +11039,7 @@ var Grid = class {
10909
11039
  const matrix = parseTSV(text2);
10910
11040
  const result = { changed: 0, skipped: 0 };
10911
11041
  if (!matrix.length) return result;
11042
+ if ((matrix.length > 1 || matrix[0].length > 1) && !this.allow("clipboard")) return result;
10912
11043
  const displayed = this.columns.displayed();
10913
11044
  const count = this.rowModel.getRowCount();
10914
11045
  const b = this.rangeBounds();
@@ -11017,6 +11148,7 @@ var Grid = class {
11017
11148
  /** Merge blocks for every `merge` column over the displayed rows (memoized). */
11018
11149
  mergeSpans() {
11019
11150
  if (this.rowModel.server || !this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan)) return NO_SPANS;
11151
+ if (!this.allow("merge")) return NO_SPANS;
11020
11152
  const rows = this.rowModel.rows();
11021
11153
  const ver = this.editVersion * 1e6 + this.columns.displayed().length;
11022
11154
  if (this.spanCache && this.spanCache.rows === rows && this.spanCache.ver === ver) return this.spanCache.spans;
@@ -11031,7 +11163,7 @@ var Grid = class {
11031
11163
  }
11032
11164
  rangeAggregates() {
11033
11165
  const all2 = this.allRangeBounds();
11034
- if (!all2.length) return null;
11166
+ if (!all2.length || !this.paid) return null;
11035
11167
  const displayed = this.columns.displayed();
11036
11168
  const key = [JSON.stringify(all2), this.rowModel.rows(), this.editVersion, ...displayed.map((c) => c.def)];
11037
11169
  const memo = this.aggMemo;
@@ -11070,6 +11202,7 @@ var Grid = class {
11070
11202
  }
11071
11203
  /** Fill from the current selection into `target`, Excel-style (one undo step). */
11072
11204
  fillRange(target) {
11205
+ if (!this.allow("fill")) return;
11073
11206
  const src = this.rangeBounds() ?? (() => {
11074
11207
  const f = this.store.state.focus;
11075
11208
  const col = f && this.columns.getColumn(f.colId);
@@ -11655,11 +11788,13 @@ var Grid = class {
11655
11788
  this.forcedOverlay = kind === "loading" || kind === "noRows" ? kind : null;
11656
11789
  this.renderer.invalidate();
11657
11790
  }),
11658
- getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
11791
+ // closed: an empty sheet, so writeXlsx([api.getXlsxSheet()]) is not an Excel export
11792
+ getXlsxSheet: guard((o) => this.allow("excel") ? buildXlsxSheet(this, o) : { name: o?.sheetName ?? "Sheet1", rows: [] }),
11659
11793
  print: guard((o) => printHtml(buildPrintHtml(this, o))),
11660
11794
  getPrintHtml: guard((o) => buildPrintHtml(this, o)),
11661
- getPdf: guard((o) => renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality })),
11795
+ getPdf: guard((o) => this.allow("pdf") ? renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality }) : null),
11662
11796
  exportPdf: guard((name = "export.pdf", o) => {
11797
+ if (!this.allow("pdf")) return;
11663
11798
  const bytes = renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality });
11664
11799
  if (bytes) download(name, [bytes], "application/pdf");
11665
11800
  else warn("exportPdf: this browser cannot draw on a canvas");
@@ -11667,16 +11802,26 @@ var Grid = class {
11667
11802
  setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
11668
11803
  setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
11669
11804
  getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
11670
- openFilterBuilder: guard(() => this.filterBuilder.open()),
11805
+ openFilterBuilder: guard(() => {
11806
+ if (!this.allow("advancedFilter")) return;
11807
+ this.filterBuilder.open();
11808
+ }),
11671
11809
  isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
11672
11810
  applyTransaction: guard((tx) => this.applyTransaction(tx)),
11673
11811
  applyTransactionAsync: guard((tx) => this.applyTransactionAsync(tx)),
11674
11812
  flushAsyncTransactions: guard(() => this.flushAsyncTransactions()),
11675
11813
  setConditionalFormats: guard((f) => this.setConditionalFormats(f)),
11676
11814
  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()),
11815
+ openConditionalFormats: guard((colId) => {
11816
+ if (!this.allow("conditional")) return;
11817
+ this.formatDialog.open(colId);
11818
+ }),
11819
+ // closed: as if `assistant` were not set
11820
+ askAssistant: guard((prompt) => askAssistant(this, prompt, this.allow("assistant") ? this.options.assistant : void 0)),
11821
+ openAssistant: guard(() => {
11822
+ if (!this.allow("assistant")) return;
11823
+ this.assistantPanel.open();
11824
+ }),
11680
11825
  getState: guard(() => this.getState()),
11681
11826
  setState: guard((s, o) => this.setState(s, o)),
11682
11827
  getScrollPosition: guard(() => this.renderer.getScroll()),
@@ -11690,7 +11835,10 @@ var Grid = class {
11690
11835
  resetColumnState: guard(() => this.resetColumns()),
11691
11836
  openColumnChooser: guard(() => this.chooser.open()),
11692
11837
  closeColumnChooser: guard(() => this.chooser.close()),
11693
- toggleSideBar: guard((open, panel) => this.sideBar?.toggle(open, panel)),
11838
+ toggleSideBar: guard((open, panel) => {
11839
+ if (!this.allow("sideBar")) return;
11840
+ this.sideBar?.toggle(open, panel);
11841
+ }),
11694
11842
  setSummary: guard((s) => this.setSummary(s)),
11695
11843
  getSummaryValues: guard(() => this.summaryValues.map((v) => ({ ...v }))),
11696
11844
  getRange: guard(() => {
@@ -11720,7 +11868,7 @@ var Grid = class {
11720
11868
  canRedo: guard(() => this.history.canRedo()),
11721
11869
  fillRange: guard((t) => this.fillRange(t)),
11722
11870
  setPinnedRows: guard((r) => this.setPinnedRows(r)),
11723
- find: guard((text2) => this.finder.find(text2)),
11871
+ find: guard((text2) => this.allow("find") ? this.finder.find(text2) : 0),
11724
11872
  findNext: guard(() => this.finder.next()),
11725
11873
  findPrevious: guard(() => this.finder.previous()),
11726
11874
  clearFind: guard(() => this.finder.clear()),
@@ -11741,13 +11889,17 @@ var Grid = class {
11741
11889
  refreshRows: guard(() => this.refreshRows()),
11742
11890
  setPivot: guard((p) => this.setPivot(p)),
11743
11891
  getPivot: guard(() => this.getPivot()),
11744
- createChart: guard((o) => this.charts.create(o)),
11892
+ // closed: an inert handle whose methods do nothing
11893
+ 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
11894
  getCsv: guard((o) => buildCsv(this, o)),
11746
11895
  getJson: guard((o) => buildJson(this, o)),
11747
11896
  exportJson: guard((name = "export.json", o) => download(name, [buildJson(this, o)], "application/json;charset=utf-8")),
11748
11897
  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")),
11898
+ getXlsx: guard((o) => this.allow("excel") ? buildXlsx(this, o) : new Uint8Array(0)),
11899
+ exportXlsx: guard((name = "export.xlsx", o) => {
11900
+ if (!this.allow("excel")) return;
11901
+ download(name, [buildXlsx(this, o)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
11902
+ }),
11751
11903
  importFile: guard((file, o) => importInto(this, file, o)),
11752
11904
  on: guard((type, fn) => this.events.on(type, fn)),
11753
11905
  destroy: () => this.destroy()
@@ -11775,10 +11927,13 @@ function buildWorkbook(sheets) {
11775
11927
  warn("buildWorkbook: no grids given; the file has one empty sheet");
11776
11928
  return writeXlsx([{ name: "Sheet1", rows: [] }]);
11777
11929
  }
11930
+ if (list.some((s) => gridOfApi(s.grid)?.allow("workbook") === false)) return new Uint8Array(0);
11778
11931
  return writeXlsx(list.map((s, i) => s.grid.getXlsxSheet({ ...s.options, sheetName: s.sheetName ?? s.options?.sheetName ?? `Sheet${i + 1}` })));
11779
11932
  }
11780
11933
  function exportWorkbook(filename, sheets) {
11781
- download(filename || "export.xlsx", [buildWorkbook(sheets)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
11934
+ const bytes = buildWorkbook(sheets);
11935
+ if (!bytes.length) return;
11936
+ download(filename || "export.xlsx", [bytes], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
11782
11937
  }
11783
11938
 
11784
11939
  // src/rest.ts