@capacms/sdk 1.0.0-next.7 → 1.0.0-next.9

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SITE_SOURCE = exports.PROTOCOL_VERSION = exports.ADMIN_SOURCE = exports.visibleMessage = exports.selectMessage = exports.scrollEdge = exports.readyMessage = exports.pickCentred = exports.normaliseOrigins = exports.hoverMessage = exports.acceptMessage = void 0;
3
+ exports.REFRESH_TIMEOUT_MS = exports.SITE_SOURCE = exports.PROTOCOL_VERSION = exports.ADMIN_SOURCE = exports.visibleMessage = exports.selectMessage = exports.scrollEdge = exports.readyMessage = exports.pickCentred = exports.normaliseOrigins = exports.hoverMessage = exports.acceptMessage = void 0;
4
4
  exports.startOverlay = startOverlay;
5
5
  /**
6
6
  * `@capacms/sdk/overlay`: the site half of Capa's live preview.
@@ -13,30 +13,52 @@ exports.startOverlay = startOverlay;
13
13
  * re-renders the draft after a save. Outside a frame it does nothing at all:
14
14
  * no listeners, no DOM, no cost.
15
15
  *
16
+ * A click on a tagged element selects its field. A ⌘-click (Ctrl-click on
17
+ * Windows and Linux) on a tagged element that is a link, or sits inside one,
18
+ * follows the link in the frame instead, so an editor can move between pages.
19
+ *
16
20
  * Vanilla DOM and no imports beyond the pure protocol module, so it works in
17
21
  * any framework and adds nothing to a bundle that does not call it.
18
22
  */
19
- const protocol_1 = require("./protocol");
20
- var protocol_2 = require("./protocol");
21
- Object.defineProperty(exports, "acceptMessage", { enumerable: true, get: function () { return protocol_2.acceptMessage; } });
22
- Object.defineProperty(exports, "hoverMessage", { enumerable: true, get: function () { return protocol_2.hoverMessage; } });
23
- Object.defineProperty(exports, "normaliseOrigins", { enumerable: true, get: function () { return protocol_2.normaliseOrigins; } });
24
- Object.defineProperty(exports, "pickCentred", { enumerable: true, get: function () { return protocol_2.pickCentred; } });
25
- Object.defineProperty(exports, "readyMessage", { enumerable: true, get: function () { return protocol_2.readyMessage; } });
26
- Object.defineProperty(exports, "scrollEdge", { enumerable: true, get: function () { return protocol_2.scrollEdge; } });
27
- Object.defineProperty(exports, "selectMessage", { enumerable: true, get: function () { return protocol_2.selectMessage; } });
28
- Object.defineProperty(exports, "visibleMessage", { enumerable: true, get: function () { return protocol_2.visibleMessage; } });
29
- Object.defineProperty(exports, "ADMIN_SOURCE", { enumerable: true, get: function () { return protocol_2.ADMIN_SOURCE; } });
30
- Object.defineProperty(exports, "PROTOCOL_VERSION", { enumerable: true, get: function () { return protocol_2.PROTOCOL_VERSION; } });
31
- Object.defineProperty(exports, "SITE_SOURCE", { enumerable: true, get: function () { return protocol_2.SITE_SOURCE; } });
23
+ const protocol_js_1 = require("./protocol.js");
24
+ var protocol_js_2 = require("./protocol.js");
25
+ Object.defineProperty(exports, "acceptMessage", { enumerable: true, get: function () { return protocol_js_2.acceptMessage; } });
26
+ Object.defineProperty(exports, "hoverMessage", { enumerable: true, get: function () { return protocol_js_2.hoverMessage; } });
27
+ Object.defineProperty(exports, "normaliseOrigins", { enumerable: true, get: function () { return protocol_js_2.normaliseOrigins; } });
28
+ Object.defineProperty(exports, "pickCentred", { enumerable: true, get: function () { return protocol_js_2.pickCentred; } });
29
+ Object.defineProperty(exports, "readyMessage", { enumerable: true, get: function () { return protocol_js_2.readyMessage; } });
30
+ Object.defineProperty(exports, "scrollEdge", { enumerable: true, get: function () { return protocol_js_2.scrollEdge; } });
31
+ Object.defineProperty(exports, "selectMessage", { enumerable: true, get: function () { return protocol_js_2.selectMessage; } });
32
+ Object.defineProperty(exports, "visibleMessage", { enumerable: true, get: function () { return protocol_js_2.visibleMessage; } });
33
+ Object.defineProperty(exports, "ADMIN_SOURCE", { enumerable: true, get: function () { return protocol_js_2.ADMIN_SOURCE; } });
34
+ Object.defineProperty(exports, "PROTOCOL_VERSION", { enumerable: true, get: function () { return protocol_js_2.PROTOCOL_VERSION; } });
35
+ Object.defineProperty(exports, "SITE_SOURCE", { enumerable: true, get: function () { return protocol_js_2.SITE_SOURCE; } });
32
36
  const BLUE = "#2563eb";
33
37
  const TAGGED = "[data-capa-entry][data-capa-field]";
38
+ /** What a ⌘-click on a tagged element follows when it sits in one. */
39
+ const LINK = "a[href]";
34
40
  /** Survives the `location.reload()` fallback so the page comes back where it was. */
35
41
  const SCROLL_KEY = "capa-overlay:scrollY";
36
- /** How long a refresh keeps putting the scroll position back while the page re-renders. */
42
+ /** How long a refresh keeps putting the scroll position back once the page has re-rendered. */
37
43
  const RESTORE_WINDOW_MS = 1500;
44
+ /** How long an in-place refresh may take before the page reloads instead. */
45
+ exports.REFRESH_TIMEOUT_MS = 10_000;
38
46
  /** `visible` is sent at most this often while the page scrolls. */
39
47
  const VISIBLE_THROTTLE_MS = 150;
48
+ /**
49
+ * Apple platforms open a link with ⌘, Windows and Linux with Ctrl. Read from
50
+ * the browser's own platform report, falling back to the user agent.
51
+ */
52
+ function isApple(nav) {
53
+ if (!nav)
54
+ return false;
55
+ const data = nav.userAgentData;
56
+ return /mac|iphone|ipad|ipod/i.test(data?.platform || nav.platform || nav.userAgent || "");
57
+ }
58
+ /** A usable refresh timeout: a positive number of milliseconds, or the default. */
59
+ function timeoutOf(ms) {
60
+ return typeof ms === "number" && ms > 0 && Number.isFinite(ms) ? ms : exports.REFRESH_TIMEOUT_MS;
61
+ }
40
62
  const noop = () => { };
41
63
  let running = null;
42
64
  /**
@@ -58,15 +80,26 @@ function startOverlay(options) {
58
80
  return running.dispose;
59
81
  }
60
82
  function createOverlay(initial) {
61
- let origins = (0, protocol_1.normaliseOrigins)(initial.adminOrigins ?? []);
83
+ let origins = (0, protocol_js_1.normaliseOrigins)(initial.adminOrigins ?? []);
62
84
  let onRefresh = initial.onRefresh;
85
+ let refreshTimeout = timeoutOf(initial.refreshTimeoutMs);
86
+ const apple = isApple(typeof navigator === "undefined" ? undefined : navigator);
87
+ const linkHint = apple ? "⌘-click to open link" : "Ctrl-click to open link";
63
88
  /** The origin that said hello. Every message this page sends goes there and nowhere else. */
64
89
  let adminOrigin = null;
65
90
  let highlight = null;
66
91
  let outlineAll = false;
67
92
  let hovered = null;
93
+ /** The link the pointer is in, when it is also over a tagged element: a ⌘-click follows it. */
94
+ let hoveredLink = null;
68
95
  let lastReady = "";
96
+ /** Where a refresh keeps the page. `until` is Infinity while the refresh is still pending. */
69
97
  let pendingScroll = null;
98
+ /** Counts refreshes, so only the latest one settles the scroll or reloads. */
99
+ let refreshes = 0;
100
+ let refreshTimer = 0;
101
+ /** True while this overlay replays a ⌘-click as a plain click, which it must let through. */
102
+ let following = false;
70
103
  let layer = null;
71
104
  let frame = 0;
72
105
  let readyFrame = 0;
@@ -161,13 +194,16 @@ function createOverlay(initial) {
161
194
  boxes.push(b);
162
195
  }
163
196
  }
197
+ // Over a link, the label says how to follow it.
198
+ const hint = hovered && hoveredLink ? ` · ${linkHint}` : "";
164
199
  if (hovered && hovered.isConnected && !active.includes(hovered)) {
165
- const b = box(hovered, "hover", hovered.getAttribute("data-capa-field"));
200
+ const b = box(hovered, "hover", `${hovered.getAttribute("data-capa-field") ?? ""}${hint}`);
166
201
  if (b)
167
202
  boxes.push(b);
168
203
  }
169
204
  active.forEach((el, i) => {
170
- const label = i === 0 ? (highlight?.field ?? "entry") : null;
205
+ const name = i === 0 ? (highlight?.field ?? "entry") : null;
206
+ const label = el === hovered && hint ? `${name ?? el.getAttribute("data-capa-field") ?? ""}${hint}` : name;
171
207
  const b = box(el, "active", label);
172
208
  if (b)
173
209
  boxes.push(b);
@@ -195,7 +231,7 @@ function createOverlay(initial) {
195
231
  if (!force && key === lastReady)
196
232
  return;
197
233
  lastReady = key;
198
- post((0, protocol_1.readyMessage)(path, entries));
234
+ post((0, protocol_js_1.readyMessage)(path, entries));
199
235
  };
200
236
  const scheduleReady = () => {
201
237
  if (readyFrame)
@@ -222,15 +258,15 @@ function createOverlay(initial) {
222
258
  bottom: rect.bottom,
223
259
  };
224
260
  });
225
- const edge = (0, protocol_1.scrollEdge)(window.scrollY, window.innerHeight, document.documentElement.scrollHeight);
226
- const pick = (0, protocol_1.pickCentred)(boxes, window.innerHeight, edge);
261
+ const edge = (0, protocol_js_1.scrollEdge)(window.scrollY, window.innerHeight, document.documentElement.scrollHeight);
262
+ const pick = (0, protocol_js_1.pickCentred)(boxes, window.innerHeight, edge);
227
263
  if (!pick)
228
264
  return;
229
265
  const key = `${pick.entryId}\n${pick.field}`;
230
266
  if (key === lastVisible)
231
267
  return;
232
268
  lastVisible = key;
233
- post((0, protocol_1.visibleMessage)(pick.entryId, pick.field));
269
+ post((0, protocol_js_1.visibleMessage)(pick.entryId, pick.field));
234
270
  };
235
271
  const onScroll = () => {
236
272
  render();
@@ -263,27 +299,63 @@ function createOverlay(initial) {
263
299
  catch {
264
300
  // Storage blocked (a sandboxed or third-party frame): start at the top.
265
301
  }
302
+ /** The editor scrolled on purpose: stop putting the old position back. */
303
+ const onScrollIntent = () => {
304
+ pendingScroll = null;
305
+ };
306
+ const reload = (y) => {
307
+ try {
308
+ window.sessionStorage.setItem(SCROLL_KEY, String(y));
309
+ }
310
+ catch {
311
+ // Nowhere to keep it; the reload lands at the top.
312
+ }
313
+ window.location.reload();
314
+ };
315
+ /**
316
+ * Re-render the draft in place, or reload. While an in-place refresh is
317
+ * pending the position is held (a page whose re-render moves things keeps
318
+ * its place), and for a moment after it lands. A refresh that fails, or is
319
+ * still pending after `refreshTimeout`, reloads from where the editor is.
320
+ */
266
321
  const refresh = () => {
267
322
  const y = window.scrollY;
268
- if (!onRefresh) {
269
- try {
270
- window.sessionStorage.setItem(SCROLL_KEY, String(y));
271
- }
272
- catch {
273
- // Nowhere to keep it; the reload lands at the top.
274
- }
275
- window.location.reload();
323
+ const refreshFn = onRefresh;
324
+ if (!refreshFn) {
325
+ reload(y);
276
326
  return;
277
327
  }
278
- pendingScroll = { y, until: Date.now() + RESTORE_WINDOW_MS };
328
+ const id = ++refreshes;
329
+ pendingScroll = { y, until: Infinity };
330
+ if (refreshTimer)
331
+ window.clearTimeout(refreshTimer);
332
+ refreshTimer = window.setTimeout(() => {
333
+ refreshTimer = 0;
334
+ if (id === refreshes)
335
+ reload(pendingScroll?.y ?? window.scrollY);
336
+ }, refreshTimeout);
279
337
  Promise.resolve()
280
- .then(() => onRefresh?.())
281
- .catch(() => window.location.reload())
282
- .finally(() => window.requestAnimationFrame(restoreScroll));
338
+ .then(() => refreshFn())
339
+ .then(() => {
340
+ // A later refresh owns the timer and the scroll from here on.
341
+ if (id !== refreshes)
342
+ return;
343
+ window.clearTimeout(refreshTimer);
344
+ refreshTimer = 0;
345
+ if (pendingScroll)
346
+ pendingScroll = { y: pendingScroll.y, until: Date.now() + RESTORE_WINDOW_MS };
347
+ window.requestAnimationFrame(restoreScroll);
348
+ }, () => {
349
+ if (id !== refreshes)
350
+ return;
351
+ window.clearTimeout(refreshTimer);
352
+ refreshTimer = 0;
353
+ reload(pendingScroll?.y ?? window.scrollY);
354
+ });
283
355
  };
284
356
  // ------------------------------------------------------------ messages ---
285
357
  const onMessage = (event) => {
286
- const message = (0, protocol_1.acceptMessage)(event, origins, window.parent);
358
+ const message = (0, protocol_js_1.acceptMessage)(event, origins, window.parent);
287
359
  if (!message)
288
360
  return;
289
361
  switch (message.type) {
@@ -316,31 +388,71 @@ function createOverlay(initial) {
316
388
  }
317
389
  };
318
390
  // --------------------------------------------------------- interaction ---
319
- const taggedFrom = (target) => {
391
+ const closestFrom = (target, selector) => {
320
392
  const el = target;
321
- return el && typeof el.closest === "function" ? el.closest(TAGGED) : null;
393
+ return el && typeof el.closest === "function" ? el.closest(selector) : null;
394
+ };
395
+ const taggedFrom = (target) => closestFrom(target, TAGGED);
396
+ /**
397
+ * A ⌘-click on a tagged link: click the same spot again, without the
398
+ * modifier, and let it through. The site then follows the link in this
399
+ * frame exactly as it would for a visitor's click: a Next `<Link>`
400
+ * navigates client-side, a plain `<a>` loads the page, `target` and the
401
+ * site's own handlers are honoured. The browser's own ⌘-click would open a
402
+ * new tab, outside the editor.
403
+ */
404
+ const follow = (event) => {
405
+ const target = event.target;
406
+ const click = new MouseEvent("click", {
407
+ bubbles: true,
408
+ cancelable: true,
409
+ composed: true,
410
+ view: window,
411
+ detail: event.detail,
412
+ screenX: event.screenX,
413
+ screenY: event.screenY,
414
+ clientX: event.clientX,
415
+ clientY: event.clientY,
416
+ button: 0,
417
+ });
418
+ following = true;
419
+ try {
420
+ target.dispatchEvent(click);
421
+ }
422
+ finally {
423
+ following = false;
424
+ }
322
425
  };
323
426
  const onClick = (event) => {
324
427
  // Only once the admin has said hello: a framed page that is not talking to
325
- // Capa keeps its links working.
326
- if (!adminOrigin)
428
+ // Capa keeps its links working. And never the plain click `follow` sends.
429
+ if (!adminOrigin || following)
327
430
  return;
328
431
  const el = taggedFrom(event.target);
329
432
  if (!el)
330
433
  return;
331
434
  event.preventDefault();
332
435
  event.stopPropagation();
333
- post((0, protocol_1.selectMessage)(el.getAttribute("data-capa-entry") ?? "", el.getAttribute("data-capa-field") ?? ""));
436
+ if ((apple ? event.metaKey : event.ctrlKey) && closestFrom(event.target, LINK)) {
437
+ follow(event);
438
+ return;
439
+ }
440
+ post((0, protocol_js_1.selectMessage)(el.getAttribute("data-capa-entry") ?? "", el.getAttribute("data-capa-field") ?? ""));
334
441
  };
335
442
  const updateHover = () => {
336
443
  hoverFrame = 0;
337
444
  const el = taggedFrom(lastPointer);
338
- if (el === hovered)
445
+ const link = el ? closestFrom(lastPointer, LINK) : null;
446
+ if (el === hovered && link === hoveredLink)
339
447
  return;
448
+ const moved = el !== hovered;
340
449
  hovered = el;
341
- post(el
342
- ? (0, protocol_1.hoverMessage)(el.getAttribute("data-capa-entry") ?? "", el.getAttribute("data-capa-field"))
343
- : (0, protocol_1.hoverMessage)("", null));
450
+ hoveredLink = link;
451
+ if (moved) {
452
+ post(el
453
+ ? (0, protocol_js_1.hoverMessage)(el.getAttribute("data-capa-entry") ?? "", el.getAttribute("data-capa-field"))
454
+ : (0, protocol_js_1.hoverMessage)("", null));
455
+ }
344
456
  render();
345
457
  };
346
458
  const onPointerMove = (event) => {
@@ -363,10 +475,13 @@ function createOverlay(initial) {
363
475
  render();
364
476
  restoreScroll();
365
477
  });
478
+ const INTENT = ["wheel", "touchmove", "keydown"];
366
479
  window.addEventListener("message", onMessage);
367
480
  window.addEventListener("scroll", onScroll, { capture: true, passive: true });
368
481
  window.addEventListener("resize", render, { passive: true });
369
482
  window.addEventListener("popstate", scheduleReady);
483
+ for (const type of INTENT)
484
+ window.addEventListener(type, onScrollIntent, { capture: true, passive: true });
370
485
  document.addEventListener("click", onClick, true);
371
486
  document.addEventListener("pointermove", onPointerMove, { capture: true, passive: true });
372
487
  document.documentElement.addEventListener("pointerleave", onPointerLeave);
@@ -376,6 +491,8 @@ function createOverlay(initial) {
376
491
  window.removeEventListener("scroll", onScroll, { capture: true });
377
492
  window.removeEventListener("resize", render);
378
493
  window.removeEventListener("popstate", scheduleReady);
494
+ for (const type of INTENT)
495
+ window.removeEventListener(type, onScrollIntent, { capture: true });
379
496
  document.removeEventListener("click", onClick, true);
380
497
  document.removeEventListener("pointermove", onPointerMove, { capture: true });
381
498
  document.documentElement.removeEventListener("pointerleave", onPointerLeave);
@@ -385,6 +502,11 @@ function createOverlay(initial) {
385
502
  window.cancelAnimationFrame(id);
386
503
  if (visibleTimer)
387
504
  window.clearTimeout(visibleTimer);
505
+ // A refresh still pending when the overlay goes must not reload the page later.
506
+ if (refreshTimer)
507
+ window.clearTimeout(refreshTimer);
508
+ refreshTimer = 0;
509
+ refreshes++;
388
510
  layer?.remove();
389
511
  layer = null;
390
512
  if (running?.dispose === dispose)
@@ -393,8 +515,9 @@ function createOverlay(initial) {
393
515
  return {
394
516
  dispose,
395
517
  setOptions: (next) => {
396
- origins = (0, protocol_1.normaliseOrigins)(next.adminOrigins ?? []);
518
+ origins = (0, protocol_js_1.normaliseOrigins)(next.adminOrigins ?? []);
397
519
  onRefresh = next.onRefresh;
520
+ refreshTimeout = timeoutOf(next.refreshTimeoutMs);
398
521
  if (adminOrigin && !origins.includes(adminOrigin))
399
522
  adminOrigin = null;
400
523
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capacms/sdk",
3
- "version": "1.0.0-next.7",
3
+ "version": "1.0.0-next.9",
4
4
  "description": "The TypeScript SDK for Capa's content API: REST and GraphQL reads typed from your models, Next.js caching and live preview, and codegen.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://docs.capacms.com/api",
@@ -34,11 +34,19 @@
34
34
  "default": "./dist/nextjs/index.js"
35
35
  },
36
36
  "./overlay": {
37
+ "import": {
38
+ "types": "./dist/esm/overlay/index.d.ts",
39
+ "default": "./dist/esm/overlay/index.js"
40
+ },
37
41
  "types": "./dist/overlay/index.d.ts",
38
42
  "default": "./dist/overlay/index.js"
39
43
  },
40
44
  "./package.json": "./package.json",
41
45
  "./nextjs/overlay": {
46
+ "import": {
47
+ "types": "./dist/esm/nextjs/overlay.d.ts",
48
+ "default": "./dist/esm/nextjs/overlay.js"
49
+ },
42
50
  "types": "./dist/nextjs/overlay.d.ts",
43
51
  "default": "./dist/nextjs/overlay.js"
44
52
  }
@@ -87,8 +95,8 @@
87
95
  "@capa/shared": "0.1.0"
88
96
  },
89
97
  "scripts": {
90
- "build": "tsc -p tsconfig.json",
91
- "typecheck": "tsc -p tsconfig.json --noEmit",
92
- "test": "tsc -p tsconfig.json && node --test test/comments.test.js test/codegen.test.js test/client.test.js test/next-client.test.js test/nextjs.test.js test/webhooks.test.js test/attrs.test.js test/overlay.test.js test/inflate.test.js test/field-names.test.js test/next-graphql.test.js test/graphql-codegen.test.js test/graphql-contract.test.js test/readme-snippets.test.js test/nextjs-app.test.js test/package-json.test.js test/changelog.test.js test/published-docs.test.js test/builder-messages.test.js && tsc -p test/types/tsconfig.consumer.json --noEmit && tsc -p test/types/tsconfig.nonstrict.json --noEmit"
98
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.esm.json && node scripts/esm-scope.js",
99
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.esm.json --noEmit",
100
+ "test": "pnpm run build && node --test test/comments.test.js test/codegen.test.js test/client.test.js test/next-client.test.js test/nextjs.test.js test/webhooks.test.js test/attrs.test.js test/overlay.test.js test/overlay-dom.test.js test/nextjs-overlay-refresh.test.js test/nextjs-overlay-bundle.test.js test/inflate.test.js test/field-names.test.js test/next-graphql.test.js test/graphql-codegen.test.js test/graphql-contract.test.js test/readme-snippets.test.js test/nextjs-app.test.js test/nextjs-preview-app.test.js test/package-json.test.js test/changelog.test.js test/published-docs.test.js test/builder-messages.test.js && tsc -p test/types/tsconfig.consumer.json --noEmit && tsc -p test/types/tsconfig.nonstrict.json --noEmit && tsc -p test/types/tsconfig.next.json --noEmit"
93
101
  }
94
102
  }