@capacms/sdk 1.0.0-next.1 → 1.0.0-next.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/README.md +8 -0
- 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
|
@@ -419,6 +419,14 @@ every navigation, `select { entryId, field }` on a click, and `hover`.
|
|
|
419
419
|
`acceptMessage(event, allowedOrigins, parent)` is the site's filter, exported
|
|
420
420
|
for your own tests.
|
|
421
421
|
|
|
422
|
+
Since 1.0.0-next.2 the site also sends `visible { entryId, field }` while the
|
|
423
|
+
page scrolls (at most every 150ms, and only when it changes): the tagged element
|
|
424
|
+
at the centre of the viewport, chosen by `pickCentred`. At the very top of a
|
|
425
|
+
page, where a heading can never reach the centre, it is the topmost visible
|
|
426
|
+
element instead, and at the very bottom the bottommost. The editor's "Follow the
|
|
427
|
+
page" scrolls the form to that field. It is additive and still `v: 1`: an older
|
|
428
|
+
overlay never sends it and the editor simply does not follow.
|
|
429
|
+
|
|
422
430
|
## Not yet
|
|
423
431
|
|
|
424
432
|
`--select-from-depth`, `capa convert-url`, `capa persist`, GraphQL, and `/api/`
|
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
|
+
}
|