@freegridjs/core 1.9.0 → 1.9.2

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.d.ts CHANGED
@@ -5,7 +5,7 @@ export { default as en } from './locales/en.js';
5
5
  declare function createGrid(host: HTMLElement, options: GridOptions): GridApi;
6
6
 
7
7
  /** FreeGrid version (kept in sync with releases.json by scripts/release.mjs). */
8
- declare const version = "1.9.0";
8
+ declare const version = "1.9.2";
9
9
 
10
10
  interface LicenseInfo {
11
11
  /** Licensed domains; each also covers every subdomain below it (`*.example.com` means the same as `example.com`). */
@@ -27,12 +27,13 @@ type LicenseResult = {
27
27
  reason: 'format' | 'signature' | 'expired' | 'domain' | 'version' | 'unsupported';
28
28
  info?: LicenseInfo;
29
29
  };
30
- /** Check a key's signature, expiry, domain and (perpetual keys) the major version. */
31
- declare function verifyLicense(key: string, host: string, opts?: {
30
+ type VerifyOptions = {
32
31
  publicKey?: JsonWebKey;
33
32
  now?: Date;
34
33
  version?: string;
35
- }): Promise<LicenseResult>;
34
+ };
35
+ /** Check a key's signature, expiry, domain and (perpetual keys) the major version. */
36
+ declare function verifyLicense(key: string, host: string, opts?: VerifyOptions): Promise<LicenseResult>;
36
37
  /** Set the license key once for every grid on the page. */
37
38
  declare function setLicenseKey(key: string): void;
38
39
 
package/dist/index.js CHANGED
@@ -2271,6 +2271,24 @@ var Renderer = class {
2271
2271
  else this.paintRow(row.el, node, i, cols, isSel, focusCol, hits, spans, range.first);
2272
2272
  }
2273
2273
  }
2274
+ if (spans.size) this.stickMergedText(top);
2275
+ }
2276
+ /**
2277
+ * A vertically merged cell taller than the view keeps its text in sight: the text moves down to the top of the
2278
+ * visible part of the block and stops in the block's last row. Runs on every scroll, also for rows not repainted.
2279
+ */
2280
+ stickMergedText(viewTop) {
2281
+ for (const r of REGIONS3) {
2282
+ for (const row of this.painted[r].values()) {
2283
+ for (const c of row.el.querySelectorAll(".fg-cell-merged")) {
2284
+ const blockTop = Number(c.dataset.mergeTop);
2285
+ const last = Number(c.dataset.mergeLast);
2286
+ if (!Number.isFinite(blockTop) || !Number.isFinite(last)) continue;
2287
+ const shift = `${Math.round(Math.min(last, Math.max(0, viewTop - blockTop)))}px`;
2288
+ if (c.style.getPropertyValue("--fg-merge-shift") !== shift) c.style.setProperty("--fg-merge-shift", shift);
2289
+ }
2290
+ }
2291
+ }
2274
2292
  }
2275
2293
  /**
2276
2294
  * Same row, only the column window moved: drop the cells that left and paint the ones that came in.
@@ -2349,9 +2367,16 @@ var Renderer = class {
2349
2367
  cellEl.style.top = `${-(this.rowTop(i) - this.rowTop(s.start[i]))}px`;
2350
2368
  cellEl.style.height = `${this.rowTop(s.end[i]) + this.rowH(s.end[i]) - this.rowTop(s.start[i])}px`;
2351
2369
  cellEl.style.bottom = "auto";
2370
+ cellEl.dataset.mergeTop = String(this.rowTop(s.start[i]));
2371
+ cellEl.dataset.mergeLast = String(this.rowTop(s.end[i]) - this.rowTop(s.start[i]));
2352
2372
  drawsMerge = true;
2353
2373
  } else cellEl.classList.add("fg-cell-merge-hidden");
2354
2374
  }
2375
+ if (!cellEl.classList.contains("fg-cell-merged") && cellEl.dataset.mergeTop !== void 0) {
2376
+ delete cellEl.dataset.mergeTop;
2377
+ delete cellEl.dataset.mergeLast;
2378
+ cellEl.style.removeProperty("--fg-merge-shift");
2379
+ }
2355
2380
  if (!isSystemCol(col.id) && bounds.some((b) => col.index >= b.c1 && col.index <= b.c2)) cellEl.classList.add("fg-cell-range");
2356
2381
  });
2357
2382
  rowEl.style.zIndex = drawsMerge ? "1" : "";
@@ -3453,6 +3478,7 @@ function attachBody(grid) {
3453
3478
  const keyOf = (e) => mirrored(grid, e);
3454
3479
  let lastPointer = "mouse";
3455
3480
  let pressTimer = 0;
3481
+ let endGesture = null;
3456
3482
  let suppressClick = false;
3457
3483
  const toEvent = (e) => {
3458
3484
  const hit = renderer.cellAt(e.target);
@@ -3675,6 +3701,7 @@ function attachBody(grid) {
3675
3701
  window.removeEventListener("pointermove", move);
3676
3702
  window.removeEventListener("pointerup", cancel);
3677
3703
  window.removeEventListener("pointercancel", cancel);
3704
+ if (endGesture === cancel) endGesture = null;
3678
3705
  };
3679
3706
  const move = (ev) => {
3680
3707
  if (Math.hypot(ev.clientX - x0, ev.clientY - y0) > 8) cancel();
@@ -3690,6 +3717,8 @@ function attachBody(grid) {
3690
3717
  }
3691
3718
  grid.contextMenu.open(x0, y0, hit.rowIndex, hit.colId);
3692
3719
  }, 550);
3720
+ endGesture?.();
3721
+ endGesture = cancel;
3693
3722
  window.addEventListener("pointermove", move);
3694
3723
  window.addEventListener("pointerup", cancel);
3695
3724
  window.addEventListener("pointercancel", cancel);
@@ -3723,11 +3752,16 @@ function attachBody(grid) {
3723
3752
  const up = () => {
3724
3753
  window.removeEventListener("pointermove", move);
3725
3754
  window.removeEventListener("pointerup", up);
3755
+ window.removeEventListener("pointercancel", up);
3726
3756
  root.classList.remove("fg-range-dragging");
3727
3757
  if (dragging) suppressClick = true;
3758
+ if (endGesture === up) endGesture = null;
3728
3759
  };
3760
+ endGesture?.();
3761
+ endGesture = up;
3729
3762
  window.addEventListener("pointermove", move);
3730
3763
  window.addEventListener("pointerup", up);
3764
+ window.addEventListener("pointercancel", up);
3731
3765
  };
3732
3766
  const onKeyDown = (e) => {
3733
3767
  if (e.target !== root && e.target !== renderer.gridEl) return;
@@ -3826,6 +3860,8 @@ function attachBody(grid) {
3826
3860
  root.addEventListener("paste", onPaste);
3827
3861
  root.addEventListener("cut", onCut);
3828
3862
  return () => {
3863
+ clearTimeout(pressTimer);
3864
+ endGesture?.();
3829
3865
  body.removeEventListener("click", onClick);
3830
3866
  body.removeEventListener("dblclick", onDblClick);
3831
3867
  body.removeEventListener("pointerdown", onPointerDown);
@@ -7633,7 +7669,7 @@ var PAID_FEATURES = {
7633
7669
  };
7634
7670
 
7635
7671
  // src/version.ts
7636
- var version = "1.9.0";
7672
+ var version = "1.9.2";
7637
7673
 
7638
7674
  // src/license.ts
7639
7675
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -7648,11 +7684,17 @@ var b64urlDecode = (s) => {
7648
7684
  };
7649
7685
  function isDevHost(host) {
7650
7686
  const h = host.toLowerCase().replace(/^\[|\]$/g, "");
7651
- 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);
7687
+ 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;
7688
+ const ip = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h);
7689
+ if (!ip) return false;
7690
+ const [a, b, c, d] = ip.slice(1).map(Number);
7691
+ if ([a, b, c, d].some((n) => n > 255)) return false;
7692
+ return a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31;
7652
7693
  }
7653
7694
  function domainMatches(host, domains) {
7654
7695
  const h = host.toLowerCase().replace(/\.$/, "");
7655
7696
  return domains.some((d) => {
7697
+ if (typeof d !== "string") return false;
7656
7698
  const raw = d.toLowerCase().replace(/\.$/, "");
7657
7699
  const dom = raw.startsWith("*.") ? raw.slice(2) : raw;
7658
7700
  if (!dom) return false;
@@ -7666,20 +7708,32 @@ var isDay = (s) => {
7666
7708
  return d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
7667
7709
  };
7668
7710
  var today = (now) => `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
7669
- async function verifyLicense(key, host, opts = {}) {
7711
+ function checkTerms(key, host, opts = {}) {
7670
7712
  const parts = typeof key === "string" ? key.trim().split(".") : [];
7671
- if (parts.length !== 3 || parts[0] !== "FG1") return { ok: false, reason: "format" };
7713
+ if (parts.length !== 3 || parts[0] !== "FG1") return { fail: { ok: false, reason: "format" } };
7672
7714
  let info;
7673
7715
  try {
7674
7716
  info = JSON.parse(new TextDecoder().decode(b64urlDecode(parts[1])));
7675
- if (!Array.isArray(info.domains) || !isDay(info.expires)) return { ok: false, reason: "format" };
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" };
7717
+ if (!info || !Array.isArray(info.domains) || !info.domains.every((d) => typeof d === "string") || !isDay(info.expires)) return { fail: { ok: false, reason: "format" } };
7718
+ if (info.major !== void 0 && !(Number.isInteger(info.major) && info.major > 0)) return { fail: { ok: false, reason: "format" } };
7719
+ if (info.plan !== void 0 && info.plan !== "company" && info.plan !== "personal") return { fail: { ok: false, reason: "format" } };
7678
7720
  } catch {
7679
- return { ok: false, reason: "format" };
7721
+ return { fail: { ok: false, reason: "format" } };
7680
7722
  }
7681
- const subtle = typeof crypto !== "undefined" ? crypto.subtle : void 0;
7682
- if (!subtle) return { ok: false, reason: "unsupported", info };
7723
+ let fail = null;
7724
+ if (today(opts.now ?? /* @__PURE__ */ new Date()) > info.expires) fail = { ok: false, reason: "expired", info };
7725
+ else if (!domainMatches(host, info.domains)) fail = { ok: false, reason: "domain", info };
7726
+ else if (info.major !== void 0 && majorOf(opts.version ?? version) > info.major) fail = { ok: false, reason: "version", info };
7727
+ return { parts, info, fail };
7728
+ }
7729
+ var hasSubtle = () => typeof crypto !== "undefined" && !!crypto.subtle;
7730
+ async function verifyLicense(key, host, opts = {}) {
7731
+ const checked = checkTerms(key, host, opts);
7732
+ if (!("info" in checked)) return checked.fail;
7733
+ const { parts, info } = checked;
7734
+ const terms = () => checked.fail;
7735
+ if (!hasSubtle()) return terms() ?? { ok: false, reason: "unsupported", info };
7736
+ const subtle = crypto.subtle;
7683
7737
  try {
7684
7738
  const pub = await subtle.importKey("jwk", opts.publicKey ?? PUBLIC_KEY, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
7685
7739
  const signed = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
@@ -7688,10 +7742,7 @@ async function verifyLicense(key, host, opts = {}) {
7688
7742
  } catch {
7689
7743
  return { ok: false, reason: "signature", info };
7690
7744
  }
7691
- if (today(opts.now ?? /* @__PURE__ */ new Date()) > info.expires) return { ok: false, reason: "expired", info };
7692
- if (!domainMatches(host, info.domains)) return { ok: false, reason: "domain", info };
7693
- if (info.major !== void 0 && majorOf(opts.version ?? version) > info.major) return { ok: false, reason: "version", info };
7694
- return { ok: true, info };
7745
+ return terms() ?? { ok: true, info };
7695
7746
  }
7696
7747
  var globalKey;
7697
7748
  var statuses = /* @__PURE__ */ new Map();
@@ -7729,23 +7780,26 @@ function reportUsage(info, host = typeof location !== "undefined" ? location.hos
7729
7780
  }
7730
7781
  function checkLicense(key = globalKey, host = typeof location !== "undefined" ? location.hostname : "") {
7731
7782
  if (isDevHost(host)) return Promise.resolve(true);
7732
- const k = key ?? globalKey;
7783
+ return keyState(key ?? globalKey, host).then((s) => s === "ok" || s === "unsupported");
7784
+ }
7785
+ function keyState(k, host) {
7733
7786
  const id = `${k ?? ""}@${host}`;
7734
7787
  let status = statuses.get(id);
7735
7788
  if (status) return status;
7736
7789
  if (!k) {
7737
7790
  console.info(NOTICE.missing);
7738
7791
  reportUsage({ version, licensed: false }, host);
7739
- status = Promise.resolve(false);
7792
+ status = Promise.resolve("none");
7740
7793
  } else {
7741
7794
  status = verifyLicense(k, host).then((res) => {
7742
7795
  reportUsage({ version, licensed: res.ok, company: res.ok ? res.info.company : void 0 }, host);
7743
- if (res.ok || res.reason === "unsupported") return res.ok || res.reason === "unsupported";
7796
+ if (res.ok) return "ok";
7797
+ if (res.reason === "unsupported") return "unsupported";
7744
7798
  const extra = res.info ? ` (${res.info.domains.join(", ")}, ${res.info.major ? `${res.info.major}.x` : res.info.expires})` : "";
7745
7799
  console.warn(`${NOTICE[res.reason]}${extra}
7746
7800
  ${PRICING_URL}`);
7747
- return false;
7748
- }, () => false);
7801
+ return "bad";
7802
+ }, () => "bad");
7749
7803
  }
7750
7804
  statuses.set(id, status);
7751
7805
  return status;
@@ -7792,41 +7846,64 @@ function badgeRequested(host = currentHost()) {
7792
7846
  return fetchDomainStatus(host).then((s) => s?.badge === true);
7793
7847
  }
7794
7848
  async function needsFreeBadge(key, host = currentHost()) {
7849
+ await Promise.resolve();
7795
7850
  if (await checkLicense(key, host)) return false;
7796
7851
  return badgeRequested(host);
7797
7852
  }
7798
7853
  var GATE_KEY = "freegrid:gate";
7854
+ var MAX_VERIFIED_KEYS = 5;
7799
7855
  var keyIdOf = (key) => {
7800
7856
  if (!key) return "";
7801
7857
  const parts = key.trim().split(".");
7802
7858
  return parts[parts.length - 1].slice(-16);
7803
7859
  };
7860
+ var readGate = (host) => {
7861
+ const c = JSON.parse(localStorage.getItem(GATE_KEY) ?? "null");
7862
+ 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;
7863
+ return { host, trialEnded: c.trialEnded, ...typeof c.trialEnds === "string" ? { trialEnds: c.trialEnds } : {}, verifiedKeys: c.verifiedKeys };
7864
+ };
7804
7865
  function paidAllowed(key, host = currentHost()) {
7805
7866
  if (isDevHost(host)) return true;
7867
+ const k = key ?? globalKey;
7868
+ if (k && !hasSubtle()) {
7869
+ const checked = checkTerms(k, host);
7870
+ if ("info" in checked && !checked.fail) return true;
7871
+ }
7806
7872
  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;
7873
+ const cache2 = readGate(host);
7874
+ if (!cache2) return true;
7875
+ const id = keyIdOf(k);
7876
+ if (id && cache2.verifiedKeys.includes(id)) return true;
7877
+ return !cache2.trialEnded;
7810
7878
  } catch {
7811
7879
  return true;
7812
7880
  }
7813
7881
  }
7814
- var writeGate = (cache2) => {
7882
+ var updateGate = (host, patch) => {
7815
7883
  try {
7816
- localStorage.setItem(GATE_KEY, JSON.stringify(cache2));
7884
+ let cur = null;
7885
+ try {
7886
+ cur = readGate(host);
7887
+ } catch {
7888
+ }
7889
+ localStorage.setItem(GATE_KEY, JSON.stringify(patch(cur ?? { host, trialEnded: false, verifiedKeys: [] })));
7817
7890
  } catch {
7818
7891
  }
7819
7892
  };
7820
7893
  async function resolveGate(key, host = currentHost()) {
7894
+ if (isDevHost(host)) return true;
7895
+ await Promise.resolve();
7821
7896
  const k = key ?? globalKey;
7822
- const keyId = keyIdOf(k);
7823
- if (await checkLicense(k, host)) {
7824
- writeGate({ host, keyId, paid: true });
7897
+ const state = await keyState(k, host);
7898
+ if (state === "ok") {
7899
+ const id = keyIdOf(k);
7900
+ updateGate(host, (c) => ({ ...c, verifiedKeys: [id, ...c.verifiedKeys.filter((x) => x !== id)].slice(0, MAX_VERIFIED_KEYS) }));
7825
7901
  return true;
7826
7902
  }
7903
+ if (state === "unsupported") return true;
7827
7904
  const trial = (await fetchDomainStatus(host))?.trial;
7828
7905
  if (!trial) return true;
7829
- writeGate({ host, keyId, paid: trial.active, trialEnds: trial.ends });
7906
+ updateGate(host, (c) => ({ ...c, trialEnded: !trial.active, trialEnds: trial.ends }));
7830
7907
  const notice = `${trial.active ? "trial" : "ended"}@${host}`;
7831
7908
  if (!gateNotices.has(notice)) {
7832
7909
  gateNotices.add(notice);