@capacms/sdk 1.0.0-next.1 → 1.0.0-next.3
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/README.md +21 -0
- package/dist/next/client.d.ts +14 -0
- package/dist/next/client.js +19 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +2 -1
- package/dist/nextjs/index.d.ts +2 -1
- package/dist/nextjs/index.js +19 -3
- package/dist/overlay/index.d.ts +2 -2
- package/dist/overlay/index.js +48 -3
- package/dist/overlay/protocol.d.ts +43 -0
- package/dist/overlay/protocol.js +78 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,6 +199,19 @@ so that moving a folder cannot silently split one page's telemetry in two. Set
|
|
|
199
199
|
`page` on the config instead when a client serves exactly one page; a value on
|
|
200
200
|
the call wins over one on the config.
|
|
201
201
|
|
|
202
|
+
A layout is not a page. `app/layout.tsx` (and any nested `layout.*` or
|
|
203
|
+
`template.*`) renders around every page below it, and Next does not tell it
|
|
204
|
+
which one, so its reads cannot be charged to the page being rendered. `routeOf`
|
|
205
|
+
returns `"(layout)"` for these files, exported as `LAYOUT_PAGE`, and a read that
|
|
206
|
+
names it sends no `Capa-Page` header at all, even when the client was created
|
|
207
|
+
with a `page`. So a Site singleton or a nav read in your root layout is simply
|
|
208
|
+
not attributed, instead of making `/` look as if it read everything:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// In app/layout.tsx: same call as in a page, and no page is recorded.
|
|
212
|
+
const site = await capa.entries.list("site", { page: routeOf(import.meta.url) });
|
|
213
|
+
```
|
|
214
|
+
|
|
202
215
|
A malformed value throws a `TypeError`. Capa itself ignores a header it cannot
|
|
203
216
|
store, because a mangled page identity must never take a blog down, so the SDK
|
|
204
217
|
is the place a typo surfaces.
|
|
@@ -419,6 +432,14 @@ every navigation, `select { entryId, field }` on a click, and `hover`.
|
|
|
419
432
|
`acceptMessage(event, allowedOrigins, parent)` is the site's filter, exported
|
|
420
433
|
for your own tests.
|
|
421
434
|
|
|
435
|
+
Since 1.0.0-next.2 the site also sends `visible { entryId, field }` while the
|
|
436
|
+
page scrolls (at most every 150ms, and only when it changes): the tagged element
|
|
437
|
+
at the centre of the viewport, chosen by `pickCentred`. At the very top of a
|
|
438
|
+
page, where a heading can never reach the centre, it is the topmost visible
|
|
439
|
+
element instead, and at the very bottom the bottommost. The editor's "Follow the
|
|
440
|
+
page" scrolls the form to that field. It is additive and still `v: 1`: an older
|
|
441
|
+
overlay never sends it and the editor simply does not follow.
|
|
442
|
+
|
|
422
443
|
## Not yet
|
|
423
444
|
|
|
424
445
|
`--select-from-depth`, `capa convert-url`, `capa persist`, GraphQL, and `/api/`
|
package/dist/next/client.d.ts
CHANGED
|
@@ -287,6 +287,20 @@ export declare class CapaError extends Error {
|
|
|
287
287
|
}
|
|
288
288
|
export declare function isCapaError(error: unknown): error is CapaError;
|
|
289
289
|
export declare function resolveNextConfig(config: CapaNextConfig): ResolvedNextConfig;
|
|
290
|
+
/**
|
|
291
|
+
* The page a layout (or template) reads for: none.
|
|
292
|
+
*
|
|
293
|
+
* A root layout renders around every page on the site, so charging its reads
|
|
294
|
+
* to `/`, the route its file sits at, made the home page look as if it read
|
|
295
|
+
* every Site singleton and nav on the site. `routeOf` returns this for a
|
|
296
|
+
* `layout.*` or `template.*` file, and a read that names it sends NO
|
|
297
|
+
* `Capa-Page` at all, even when the client was built with a `page`.
|
|
298
|
+
*
|
|
299
|
+
* Not sent as a value, because the API would drop it anyway: it is not a
|
|
300
|
+
* `PAGE_ID`, and the API ignores what it cannot store. Staying absent keeps a
|
|
301
|
+
* layout read byte-identical to one from a client that never named a page.
|
|
302
|
+
*/
|
|
303
|
+
export declare const LAYOUT_PAGE = "(layout)";
|
|
290
304
|
type SelectInput = string | ReadonlyArray<unknown>;
|
|
291
305
|
/** Serialize the SDK object form into the canonical `/api/entries` grammar. */
|
|
292
306
|
export declare function serializeSelect(select: SelectInput): string;
|
package/dist/next/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CapaError = void 0;
|
|
3
|
+
exports.LAYOUT_PAGE = exports.CapaError = void 0;
|
|
4
4
|
exports.isCapaError = isCapaError;
|
|
5
5
|
exports.resolveNextConfig = resolveNextConfig;
|
|
6
6
|
exports.serializeSelect = serializeSelect;
|
|
@@ -85,6 +85,20 @@ function resolveNextConfig(config) {
|
|
|
85
85
|
* quietly stops arriving rather than as an error.
|
|
86
86
|
*/
|
|
87
87
|
const PAGE_ID = /^\/[A-Za-z0-9._\-[\]/]{0,199}$/;
|
|
88
|
+
/**
|
|
89
|
+
* The page a layout (or template) reads for: none.
|
|
90
|
+
*
|
|
91
|
+
* A root layout renders around every page on the site, so charging its reads
|
|
92
|
+
* to `/`, the route its file sits at, made the home page look as if it read
|
|
93
|
+
* every Site singleton and nav on the site. `routeOf` returns this for a
|
|
94
|
+
* `layout.*` or `template.*` file, and a read that names it sends NO
|
|
95
|
+
* `Capa-Page` at all, even when the client was built with a `page`.
|
|
96
|
+
*
|
|
97
|
+
* Not sent as a value, because the API would drop it anyway: it is not a
|
|
98
|
+
* `PAGE_ID`, and the API ignores what it cannot store. Staying absent keeps a
|
|
99
|
+
* layout read byte-identical to one from a client that never named a page.
|
|
100
|
+
*/
|
|
101
|
+
exports.LAYOUT_PAGE = "(layout)";
|
|
88
102
|
/**
|
|
89
103
|
* The page for one call: the call's own value, else the client's, else none.
|
|
90
104
|
*
|
|
@@ -97,6 +111,10 @@ function resolvePage(configPage, callPage) {
|
|
|
97
111
|
const value = callPage !== undefined ? callPage : configPage;
|
|
98
112
|
if (value === undefined || value === null)
|
|
99
113
|
return undefined;
|
|
114
|
+
// A layout's read, named on purpose: no header, and the config's page does
|
|
115
|
+
// not stand in for it either.
|
|
116
|
+
if (value === exports.LAYOUT_PAGE)
|
|
117
|
+
return undefined;
|
|
100
118
|
if (typeof value !== "string" || !PAGE_ID.test(value)) {
|
|
101
119
|
throw new TypeError(`@capacms/sdk/next: page must be a path such as "/blog/[slug]" or "/blog/hello". Got ${JSON.stringify(value)}.`);
|
|
102
120
|
}
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CapaError, createClient, isCapaError, resolveNextConfig, serializeSelect, } from "./client";
|
|
1
|
+
export { CapaError, createClient, isCapaError, LAYOUT_PAGE, resolveNextConfig, serializeSelect, } from "./client";
|
|
2
2
|
export { capaAttrs } from "./attrs";
|
|
3
3
|
export type { CapaAttrs } from "./attrs";
|
|
4
4
|
export type { CallOptions, CapaNextClient, CapaNextConfig, EntriesResource, Entry, Filter, FilterOperator, FilterScalar, FilterValue, GetOptions, ListOptions, Page, PageDetail, PageInfo, PageSummary, PagesListOptions, PagesResource, PreviewClaim, ResponseMeta, Single, } from "./client";
|
package/dist/next/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.capaAttrs = exports.serializeSelect = exports.resolveNextConfig = exports.isCapaError = exports.createClient = exports.CapaError = void 0;
|
|
3
|
+
exports.capaAttrs = exports.serializeSelect = exports.resolveNextConfig = exports.LAYOUT_PAGE = exports.isCapaError = exports.createClient = exports.CapaError = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "CapaError", { enumerable: true, get: function () { return client_1.CapaError; } });
|
|
6
6
|
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
|
|
7
7
|
Object.defineProperty(exports, "isCapaError", { enumerable: true, get: function () { return client_1.isCapaError; } });
|
|
8
|
+
Object.defineProperty(exports, "LAYOUT_PAGE", { enumerable: true, get: function () { return client_1.LAYOUT_PAGE; } });
|
|
8
9
|
Object.defineProperty(exports, "resolveNextConfig", { enumerable: true, get: function () { return client_1.resolveNextConfig; } });
|
|
9
10
|
Object.defineProperty(exports, "serializeSelect", { enumerable: true, get: function () { return client_1.serializeSelect; } });
|
|
10
11
|
var attrs_1 = require("./attrs");
|
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CapaNextClient, type CapaNextConfig, type PagesResource, type PreviewClaim } from "../next";
|
|
1
|
+
import { LAYOUT_PAGE, type CapaNextClient, type CapaNextConfig, type PagesResource, type PreviewClaim } from "../next";
|
|
2
2
|
export interface CacheOptions {
|
|
3
3
|
tags?: string[];
|
|
4
4
|
revalidate?: number | false;
|
|
@@ -50,4 +50,5 @@ export declare function preview(token: string, client: CapaNextClient): Promise<
|
|
|
50
50
|
* this and never hold the whole client.
|
|
51
51
|
*/
|
|
52
52
|
export declare function pagesFor(client: CapaNextClient): PagesResource;
|
|
53
|
+
export { LAYOUT_PAGE };
|
|
53
54
|
export type { PreviewClaim };
|
package/dist/nextjs/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LAYOUT_PAGE = void 0;
|
|
3
4
|
exports.withCache = withCache;
|
|
4
5
|
exports.tagsFor = tagsFor;
|
|
5
6
|
exports.revalidateFromWebhook = revalidateFromWebhook;
|
|
@@ -8,6 +9,7 @@ exports.routeOf = routeOf;
|
|
|
8
9
|
exports.preview = preview;
|
|
9
10
|
exports.pagesFor = pagesFor;
|
|
10
11
|
const next_1 = require("../next");
|
|
12
|
+
Object.defineProperty(exports, "LAYOUT_PAGE", { enumerable: true, get: function () { return next_1.LAYOUT_PAGE; } });
|
|
11
13
|
/** Add Next.js fetch-cache options without importing `next/*`. */
|
|
12
14
|
function withCache(fetchImpl, options) {
|
|
13
15
|
return (async (input, init = {}) => {
|
|
@@ -64,6 +66,16 @@ async function draftClient(input) {
|
|
|
64
66
|
* src/app/(marketing)/pricing/page.tsx -> /pricing
|
|
65
67
|
* pages/blog/[slug].tsx -> /blog/[slug]
|
|
66
68
|
* app/page.tsx -> /
|
|
69
|
+
* app/layout.tsx, app/blog/template.tsx -> (layout)
|
|
70
|
+
*
|
|
71
|
+
* A LAYOUT IS NOT A PAGE. `layout.*` and `template.*` render around every page
|
|
72
|
+
* below them, and a layout is not told which one: Next hands it no pathname.
|
|
73
|
+
* So its reads cannot be charged to the page being rendered, and charging them
|
|
74
|
+
* to the route its own file sits at is wrong: a root layout's Site singleton
|
|
75
|
+
* and nav would all be recorded as reads of `/`. `routeOf` returns
|
|
76
|
+
* `LAYOUT_PAGE` (`"(layout)"`) for these files instead, and a read that names
|
|
77
|
+
* it sends no `Capa-Page` at all. Pass `routeOf(import.meta.url)` from a layout
|
|
78
|
+
* exactly as from a page and it does the right thing.
|
|
67
79
|
*
|
|
68
80
|
* WHAT IS DROPPED, and why each one:
|
|
69
81
|
*
|
|
@@ -74,8 +86,7 @@ async function draftClient(input) {
|
|
|
74
86
|
* parallel slots `@modal` the same
|
|
75
87
|
* the `(.)` intercept marker an intercepting route at the SAME
|
|
76
88
|
* level renders the segment beside it
|
|
77
|
-
* `page.*`, `
|
|
78
|
-
* `default.*`, `template.*`
|
|
89
|
+
* `page.*`, `route.*`, `default.*` leaf files, not segments
|
|
79
90
|
* `index` in the pages router the folder IS the route
|
|
80
91
|
* the extension never in a URL
|
|
81
92
|
*
|
|
@@ -87,7 +98,9 @@ async function draftClient(input) {
|
|
|
87
98
|
* case the message says to pass the page string yourself.
|
|
88
99
|
*/
|
|
89
100
|
const ROUTER_ROOTS = new Set(["app", "pages"]);
|
|
90
|
-
const LEAF_FILES = new Set(["page", "
|
|
101
|
+
const LEAF_FILES = new Set(["page", "route", "default"]);
|
|
102
|
+
/** Files that render around many routes, so they name none. */
|
|
103
|
+
const LAYOUT_FILES = new Set(["layout", "template"]);
|
|
91
104
|
/** `(..)photo`, `(..)(..)photo`, `(...)photo`: the URL is not here. */
|
|
92
105
|
const OUTER_INTERCEPT = /^(\(\.\.\.\)|(\(\.\.\))+)/;
|
|
93
106
|
function routeOf(file) {
|
|
@@ -131,6 +144,9 @@ function routeOf(file) {
|
|
|
131
144
|
const dot = part.lastIndexOf(".");
|
|
132
145
|
if (dot > 0)
|
|
133
146
|
part = part.slice(0, dot);
|
|
147
|
+
// Only in the app router: `pages/layout.tsx` is a page at `/layout`.
|
|
148
|
+
if (parts[rootIndex] === "app" && LAYOUT_FILES.has(part))
|
|
149
|
+
return next_1.LAYOUT_PAGE;
|
|
134
150
|
if (LEAF_FILES.has(part))
|
|
135
151
|
continue;
|
|
136
152
|
// The pages router: `pages/blog/index.tsx` is `/blog`.
|
package/dist/overlay/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { acceptMessage, hoverMessage, normaliseOrigins, readyMessage, selectMessage, ADMIN_SOURCE, PROTOCOL_VERSION, SITE_SOURCE, } from "./protocol";
|
|
2
|
-
export type { AdminMessage, MessageLike, SiteMessage } from "./protocol";
|
|
1
|
+
export { acceptMessage, hoverMessage, normaliseOrigins, pickCentred, readyMessage, scrollEdge, selectMessage, visibleMessage, ADMIN_SOURCE, PROTOCOL_VERSION, SITE_SOURCE, } from "./protocol";
|
|
2
|
+
export type { AdminMessage, MessageLike, SiteMessage, TaggedBox } from "./protocol";
|
|
3
3
|
export interface OverlayOptions {
|
|
4
4
|
/**
|
|
5
5
|
* The Capa admin origins allowed to drive this page, for example
|
package/dist/overlay/index.js
CHANGED
|
@@ -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.selectMessage = exports.readyMessage = exports.normaliseOrigins = exports.hoverMessage = exports.acceptMessage = void 0;
|
|
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;
|
|
4
4
|
exports.startOverlay = startOverlay;
|
|
5
5
|
/**
|
|
6
6
|
* `@capacms/sdk/overlay`: the site half of Capa's live preview.
|
|
@@ -21,8 +21,11 @@ var protocol_2 = require("./protocol");
|
|
|
21
21
|
Object.defineProperty(exports, "acceptMessage", { enumerable: true, get: function () { return protocol_2.acceptMessage; } });
|
|
22
22
|
Object.defineProperty(exports, "hoverMessage", { enumerable: true, get: function () { return protocol_2.hoverMessage; } });
|
|
23
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; } });
|
|
24
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; } });
|
|
25
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; } });
|
|
26
29
|
Object.defineProperty(exports, "ADMIN_SOURCE", { enumerable: true, get: function () { return protocol_2.ADMIN_SOURCE; } });
|
|
27
30
|
Object.defineProperty(exports, "PROTOCOL_VERSION", { enumerable: true, get: function () { return protocol_2.PROTOCOL_VERSION; } });
|
|
28
31
|
Object.defineProperty(exports, "SITE_SOURCE", { enumerable: true, get: function () { return protocol_2.SITE_SOURCE; } });
|
|
@@ -32,6 +35,8 @@ const TAGGED = "[data-capa-entry][data-capa-field]";
|
|
|
32
35
|
const SCROLL_KEY = "capa-overlay:scrollY";
|
|
33
36
|
/** How long a refresh keeps putting the scroll position back while the page re-renders. */
|
|
34
37
|
const RESTORE_WINDOW_MS = 1500;
|
|
38
|
+
/** `visible` is sent at most this often while the page scrolls. */
|
|
39
|
+
const VISIBLE_THROTTLE_MS = 150;
|
|
35
40
|
const noop = () => { };
|
|
36
41
|
let running = null;
|
|
37
42
|
/**
|
|
@@ -67,6 +72,8 @@ function createOverlay(initial) {
|
|
|
67
72
|
let readyFrame = 0;
|
|
68
73
|
let hoverFrame = 0;
|
|
69
74
|
let lastPointer = null;
|
|
75
|
+
let visibleTimer = 0;
|
|
76
|
+
let lastVisible = "";
|
|
70
77
|
const post = (message) => {
|
|
71
78
|
if (!adminOrigin)
|
|
72
79
|
return;
|
|
@@ -195,6 +202,41 @@ function createOverlay(initial) {
|
|
|
195
202
|
return;
|
|
196
203
|
readyFrame = window.requestAnimationFrame(() => sendReady(false));
|
|
197
204
|
};
|
|
205
|
+
// ------------------------------------------------------------- visible ---
|
|
206
|
+
/**
|
|
207
|
+
* Which tagged element sits at the centre of the viewport, reported when it
|
|
208
|
+
* changes. The editor's "Follow the page" scrolls the form to match. Sent
|
|
209
|
+
* only to an admin that has said hello, and only on a change, so a still
|
|
210
|
+
* page sends nothing.
|
|
211
|
+
*/
|
|
212
|
+
const reportVisible = () => {
|
|
213
|
+
visibleTimer = 0;
|
|
214
|
+
if (!adminOrigin)
|
|
215
|
+
return;
|
|
216
|
+
const boxes = tagged().map((el) => {
|
|
217
|
+
const rect = el.getBoundingClientRect();
|
|
218
|
+
return {
|
|
219
|
+
entryId: el.getAttribute("data-capa-entry") ?? "",
|
|
220
|
+
field: el.getAttribute("data-capa-field") ?? "",
|
|
221
|
+
top: rect.top,
|
|
222
|
+
bottom: rect.bottom,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
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);
|
|
227
|
+
if (!pick)
|
|
228
|
+
return;
|
|
229
|
+
const key = `${pick.entryId}\n${pick.field}`;
|
|
230
|
+
if (key === lastVisible)
|
|
231
|
+
return;
|
|
232
|
+
lastVisible = key;
|
|
233
|
+
post((0, protocol_1.visibleMessage)(pick.entryId, pick.field));
|
|
234
|
+
};
|
|
235
|
+
const onScroll = () => {
|
|
236
|
+
render();
|
|
237
|
+
if (!visibleTimer)
|
|
238
|
+
visibleTimer = window.setTimeout(reportVisible, VISIBLE_THROTTLE_MS);
|
|
239
|
+
};
|
|
198
240
|
// -------------------------------------------------------------- scroll ---
|
|
199
241
|
const restoreScroll = () => {
|
|
200
242
|
if (!pendingScroll)
|
|
@@ -247,6 +289,7 @@ function createOverlay(initial) {
|
|
|
247
289
|
switch (message.type) {
|
|
248
290
|
case "hello":
|
|
249
291
|
adminOrigin = event.origin;
|
|
292
|
+
lastVisible = "";
|
|
250
293
|
sendReady(true);
|
|
251
294
|
render();
|
|
252
295
|
return;
|
|
@@ -321,7 +364,7 @@ function createOverlay(initial) {
|
|
|
321
364
|
restoreScroll();
|
|
322
365
|
});
|
|
323
366
|
window.addEventListener("message", onMessage);
|
|
324
|
-
window.addEventListener("scroll",
|
|
367
|
+
window.addEventListener("scroll", onScroll, { capture: true, passive: true });
|
|
325
368
|
window.addEventListener("resize", render, { passive: true });
|
|
326
369
|
window.addEventListener("popstate", scheduleReady);
|
|
327
370
|
document.addEventListener("click", onClick, true);
|
|
@@ -330,7 +373,7 @@ function createOverlay(initial) {
|
|
|
330
373
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
331
374
|
const dispose = () => {
|
|
332
375
|
window.removeEventListener("message", onMessage);
|
|
333
|
-
window.removeEventListener("scroll",
|
|
376
|
+
window.removeEventListener("scroll", onScroll, { capture: true });
|
|
334
377
|
window.removeEventListener("resize", render);
|
|
335
378
|
window.removeEventListener("popstate", scheduleReady);
|
|
336
379
|
document.removeEventListener("click", onClick, true);
|
|
@@ -340,6 +383,8 @@ function createOverlay(initial) {
|
|
|
340
383
|
for (const id of [frame, readyFrame, hoverFrame])
|
|
341
384
|
if (id)
|
|
342
385
|
window.cancelAnimationFrame(id);
|
|
386
|
+
if (visibleTimer)
|
|
387
|
+
window.clearTimeout(visibleTimer);
|
|
343
388
|
layer?.remove();
|
|
344
389
|
layer = null;
|
|
345
390
|
if (running?.dispose === dispose)
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
* ready { path, entries } after hello, and after every navigation
|
|
16
16
|
* select { entryId, field } a tagged element was clicked
|
|
17
17
|
* hover { entryId, field } | { entryId: "", field: null }
|
|
18
|
+
* visible { entryId, field } the tagged element at the centre of the
|
|
19
|
+
* viewport changed (throttled, on scroll)
|
|
20
|
+
*
|
|
21
|
+
* `visible` arrived in 1.0.0-next.2 and is OPTIONAL: it is still `v: 1`,
|
|
22
|
+
* because an admin that does not know it drops an unknown type, and an older
|
|
23
|
+
* overlay simply never sends it. Additive messages keep the version; only a
|
|
24
|
+
* change to an existing shape would move it.
|
|
18
25
|
*
|
|
19
26
|
* The admin keeps its own copy of these shapes (`apps/admin`, no dependency on
|
|
20
27
|
* this package). If you change one side, change the other.
|
|
@@ -60,6 +67,12 @@ export type SiteMessage = {
|
|
|
60
67
|
type: "hover";
|
|
61
68
|
entryId: string;
|
|
62
69
|
field: string | null;
|
|
70
|
+
} | {
|
|
71
|
+
source: typeof SITE_SOURCE;
|
|
72
|
+
v: 1;
|
|
73
|
+
type: "visible";
|
|
74
|
+
entryId: string;
|
|
75
|
+
field: string;
|
|
63
76
|
};
|
|
64
77
|
/** The parts of a `MessageEvent` the filter reads, so a test can hand in a literal. */
|
|
65
78
|
export interface MessageLike {
|
|
@@ -89,3 +102,33 @@ export declare function acceptMessage(event: MessageLike, allowedOrigins: readon
|
|
|
89
102
|
export declare function readyMessage(path: string, entries: string[]): SiteMessage;
|
|
90
103
|
export declare function selectMessage(entryId: string, field: string): SiteMessage;
|
|
91
104
|
export declare function hoverMessage(entryId: string, field: string | null): SiteMessage;
|
|
105
|
+
export declare function visibleMessage(entryId: string, field: string): SiteMessage;
|
|
106
|
+
/** The part of a tagged element `pickCentred` reads, so a test can hand in literals. */
|
|
107
|
+
export interface TaggedBox {
|
|
108
|
+
entryId: string;
|
|
109
|
+
field: string;
|
|
110
|
+
/** `getBoundingClientRect()`, viewport coordinates. */
|
|
111
|
+
top: number;
|
|
112
|
+
bottom: number;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The tagged element a reader is looking at: the one that contains the
|
|
116
|
+
* viewport's horizontal centre line, and when several do (a field inside a
|
|
117
|
+
* card that is also tagged) the SMALLEST, because the innermost element is
|
|
118
|
+
* the specific one. When none crosses the line, the nearest one that is at
|
|
119
|
+
* least partly on screen. Null when nothing tagged is on screen at all.
|
|
120
|
+
*
|
|
121
|
+
* `edge` is where the page is scrolled to. A heading near the top of a page
|
|
122
|
+
* can never reach the centre line, because the page cannot scroll above its
|
|
123
|
+
* top; at the top the topmost visible element is the one being read, and at
|
|
124
|
+
* the bottom the bottommost. The same rule a table of contents' scroll spy
|
|
125
|
+
* uses.
|
|
126
|
+
*
|
|
127
|
+
* Pure, so the admin's "follow the page" can be tested without a DOM.
|
|
128
|
+
*/
|
|
129
|
+
export declare function pickCentred(boxes: readonly TaggedBox[], viewportHeight: number, edge?: "top" | "bottom" | null): TaggedBox | null;
|
|
130
|
+
/**
|
|
131
|
+
* Which edge a scroll position is at, for `pickCentred`. A page that does not
|
|
132
|
+
* scroll at all is at neither: nothing moves, and the centre rule stands.
|
|
133
|
+
*/
|
|
134
|
+
export declare function scrollEdge(scrollY: number, viewportHeight: number, scrollHeight: number): "top" | "bottom" | null;
|
package/dist/overlay/protocol.js
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* ready { path, entries } after hello, and after every navigation
|
|
17
17
|
* select { entryId, field } a tagged element was clicked
|
|
18
18
|
* hover { entryId, field } | { entryId: "", field: null }
|
|
19
|
+
* visible { entryId, field } the tagged element at the centre of the
|
|
20
|
+
* viewport changed (throttled, on scroll)
|
|
21
|
+
*
|
|
22
|
+
* `visible` arrived in 1.0.0-next.2 and is OPTIONAL: it is still `v: 1`,
|
|
23
|
+
* because an admin that does not know it drops an unknown type, and an older
|
|
24
|
+
* overlay simply never sends it. Additive messages keep the version; only a
|
|
25
|
+
* change to an existing shape would move it.
|
|
19
26
|
*
|
|
20
27
|
* The admin keeps its own copy of these shapes (`apps/admin`, no dependency on
|
|
21
28
|
* this package). If you change one side, change the other.
|
|
@@ -27,6 +34,9 @@ exports.acceptMessage = acceptMessage;
|
|
|
27
34
|
exports.readyMessage = readyMessage;
|
|
28
35
|
exports.selectMessage = selectMessage;
|
|
29
36
|
exports.hoverMessage = hoverMessage;
|
|
37
|
+
exports.visibleMessage = visibleMessage;
|
|
38
|
+
exports.pickCentred = pickCentred;
|
|
39
|
+
exports.scrollEdge = scrollEdge;
|
|
30
40
|
exports.PROTOCOL_VERSION = 1;
|
|
31
41
|
exports.ADMIN_SOURCE = "capa-admin";
|
|
32
42
|
exports.SITE_SOURCE = "capa";
|
|
@@ -104,3 +114,71 @@ function hoverMessage(entryId, field) {
|
|
|
104
114
|
? { source: exports.SITE_SOURCE, v: 1, type: "hover", entryId: "", field: null }
|
|
105
115
|
: { source: exports.SITE_SOURCE, v: 1, type: "hover", entryId, field };
|
|
106
116
|
}
|
|
117
|
+
function visibleMessage(entryId, field) {
|
|
118
|
+
return { source: exports.SITE_SOURCE, v: 1, type: "visible", entryId, field };
|
|
119
|
+
}
|
|
120
|
+
/** Above any element height, so a box that misses the centre line never outranks one that crosses it. */
|
|
121
|
+
const MISS_FLOOR = 1e9;
|
|
122
|
+
/**
|
|
123
|
+
* The tagged element a reader is looking at: the one that contains the
|
|
124
|
+
* viewport's horizontal centre line, and when several do (a field inside a
|
|
125
|
+
* card that is also tagged) the SMALLEST, because the innermost element is
|
|
126
|
+
* the specific one. When none crosses the line, the nearest one that is at
|
|
127
|
+
* least partly on screen. Null when nothing tagged is on screen at all.
|
|
128
|
+
*
|
|
129
|
+
* `edge` is where the page is scrolled to. A heading near the top of a page
|
|
130
|
+
* can never reach the centre line, because the page cannot scroll above its
|
|
131
|
+
* top; at the top the topmost visible element is the one being read, and at
|
|
132
|
+
* the bottom the bottommost. The same rule a table of contents' scroll spy
|
|
133
|
+
* uses.
|
|
134
|
+
*
|
|
135
|
+
* Pure, so the admin's "follow the page" can be tested without a DOM.
|
|
136
|
+
*/
|
|
137
|
+
function pickCentred(boxes, viewportHeight, edge = null) {
|
|
138
|
+
const centre = viewportHeight / 2;
|
|
139
|
+
let best = null;
|
|
140
|
+
let bestScore = Infinity;
|
|
141
|
+
for (const b of boxes) {
|
|
142
|
+
if (!b.entryId || !b.field)
|
|
143
|
+
continue;
|
|
144
|
+
if (b.bottom <= 0 || b.top >= viewportHeight || b.bottom <= b.top)
|
|
145
|
+
continue;
|
|
146
|
+
const height = b.bottom - b.top;
|
|
147
|
+
let score;
|
|
148
|
+
if (edge === "top") {
|
|
149
|
+
// Topmost first; of two that start together, the smaller (inner) one.
|
|
150
|
+
score = b.top * MISS_FLOOR + height;
|
|
151
|
+
}
|
|
152
|
+
else if (edge === "bottom") {
|
|
153
|
+
score = (viewportHeight - b.bottom) * MISS_FLOOR + height;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Crossing the line scores by height (smaller wins) and always beats a
|
|
157
|
+
// box that misses it, which scores by its distance from the line on top
|
|
158
|
+
// of a floor no height can reach.
|
|
159
|
+
score =
|
|
160
|
+
b.top <= centre && b.bottom >= centre
|
|
161
|
+
? height
|
|
162
|
+
: MISS_FLOOR + Math.min(Math.abs(b.top - centre), Math.abs(b.bottom - centre));
|
|
163
|
+
}
|
|
164
|
+
if (score < bestScore) {
|
|
165
|
+
best = b;
|
|
166
|
+
bestScore = score;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return best;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Which edge a scroll position is at, for `pickCentred`. A page that does not
|
|
173
|
+
* scroll at all is at neither: nothing moves, and the centre rule stands.
|
|
174
|
+
*/
|
|
175
|
+
function scrollEdge(scrollY, viewportHeight, scrollHeight) {
|
|
176
|
+
const max = scrollHeight - viewportHeight;
|
|
177
|
+
if (max <= 1)
|
|
178
|
+
return null;
|
|
179
|
+
if (scrollY <= 1)
|
|
180
|
+
return "top";
|
|
181
|
+
if (scrollY >= max - 1)
|
|
182
|
+
return "bottom";
|
|
183
|
+
return null;
|
|
184
|
+
}
|