@avocadostudio-ai/preview-adapter 0.3.2 → 0.3.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/dist/bridge-functions.d.ts +70 -0
- package/dist/bridge-functions.js +168 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/preview-bridge-core.js +15 -7
- package/package.json +2 -2
|
@@ -16,7 +16,61 @@ export declare function markdownToHtml(md: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function withPreviewParams(href: string, currentSearch: string): string;
|
|
18
18
|
export declare function findBlockNode(blockId: string): HTMLElement | null;
|
|
19
|
+
/**
|
|
20
|
+
* True while a scroll this module started is still running or being corrected.
|
|
21
|
+
*
|
|
22
|
+
* The editor treats an `iframeScrolled` report as the reader taking over —
|
|
23
|
+
* it drops the field anchor and stops following a running chat stream — so our
|
|
24
|
+
* own scrolling must not be reported. One flag for every scroll we initiate,
|
|
25
|
+
* rather than a marker at each call site, because the one call site that had no
|
|
26
|
+
* marker (block focus, which is what "Go to" uses) is exactly the one that
|
|
27
|
+
* scrolls in response to a click somewhere else.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isProgrammaticScroll(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Bring a block on screen and *keep* it there while the page settles.
|
|
32
|
+
*
|
|
33
|
+
* One `scrollIntoView` cannot do this. It resolves its destination from the
|
|
34
|
+
* layout at the instant it is called, and the preview is hardly ever settled at
|
|
35
|
+
* that instant — selection chrome is mounting, hero images are swapping in,
|
|
36
|
+
* blocks above the target are still changing height. Measured against the demo
|
|
37
|
+
* site, the target moved 478–1152px between the call and the end of the
|
|
38
|
+
* animation, every time, which left it *entirely above the viewport*: "Go to"
|
|
39
|
+
* appeared to jump to a random section, and the block the panel was now editing
|
|
40
|
+
* was nowhere on screen.
|
|
41
|
+
*
|
|
42
|
+
* So: scroll, then watch. Correct whenever the block has drifted out of the
|
|
43
|
+
* band we aimed for, and stop as soon as it holds still. Corrections wait for
|
|
44
|
+
* the previous scroll to finish (a stable `scrollY`) so we never interrupt the
|
|
45
|
+
* smooth animation we started, and they use `auto` — a second smooth animation
|
|
46
|
+
* queued behind the first reads as a lurch.
|
|
47
|
+
*
|
|
48
|
+
* A reader who starts scrolling wins immediately. Being 300px off is a smaller
|
|
49
|
+
* insult than dragging the page back out from under someone's hand.
|
|
50
|
+
*/
|
|
51
|
+
export declare function scrollBlockIntoView(el: HTMLElement, options?: {
|
|
52
|
+
behavior?: ScrollBehavior;
|
|
53
|
+
block?: ScrollLogicalPosition;
|
|
54
|
+
}): void;
|
|
19
55
|
export declare function findEditableNode(parent: HTMLElement, editablePath: string): HTMLElement | null;
|
|
56
|
+
/**
|
|
57
|
+
* The node to point at for a field, which is not always the field's own node.
|
|
58
|
+
*
|
|
59
|
+
* Plenty of editable props are never rendered as an element. Alt text is an
|
|
60
|
+
* attribute on an image; a link target is an attribute on an anchor. A renderer
|
|
61
|
+
* emits `data-editable-target` for what it *draws*, so `cards[0].imageAlt` and
|
|
62
|
+
* `cards[0].ctaHref` match nothing at all, and asking to show somebody where
|
|
63
|
+
* the problem is has so far shown them nothing.
|
|
64
|
+
*
|
|
65
|
+
* Three steps, most specific first:
|
|
66
|
+
* 1. the field's own node, when it has one
|
|
67
|
+
* 2. for alt text, the image it describes — the only thing on the page that
|
|
68
|
+
* alt text *is* about, and the inverse of the `toAltPath` convention the
|
|
69
|
+
* renderers already follow
|
|
70
|
+
* 3. the nearest enclosing node: the card, the list item, the block. Less
|
|
71
|
+
* precise, and still the difference between "that one" and nothing.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveEditableNode(parent: HTMLElement, editablePath: string): HTMLElement | null;
|
|
20
74
|
/**
|
|
21
75
|
* True when React rendered this editable field as block-level element children
|
|
22
76
|
* (parsed markdown), not a plain text node.
|
|
@@ -61,6 +115,22 @@ export declare function setNestedLabelsVisibility(visible: boolean): void;
|
|
|
61
115
|
export declare function clearChildFocus(): void;
|
|
62
116
|
export declare function showSkeleton(afterBlockId: string | null, blockType: string): void;
|
|
63
117
|
export declare function removeSkeletons(): void;
|
|
118
|
+
/**
|
|
119
|
+
* The one misintegration the overlay can diagnose for itself.
|
|
120
|
+
*
|
|
121
|
+
* Selection is entirely `[data-block-id]`: a click resolves through
|
|
122
|
+
* `closest("[data-block-id]")`, and a null result is read as "clicked outside
|
|
123
|
+
* any block", which *clears* the selection. A site that added
|
|
124
|
+
* `data-editable-target` to its components but no block wrappers therefore
|
|
125
|
+
* frames, renders, styles and scrolls correctly, and silently deselects on
|
|
126
|
+
* every click — and the README has already trained the reader to expect a
|
|
127
|
+
* preview that ignores clicks, because that is what selection mode being off
|
|
128
|
+
* looks like.
|
|
129
|
+
*
|
|
130
|
+
* Editable targets with no wrapper around them is unambiguous: nobody adds the
|
|
131
|
+
* per-field attributes by accident. Say so once, loudly, in development.
|
|
132
|
+
*/
|
|
133
|
+
export declare function missingBlockWrapperWarning(blockCount: number, editableCount: number): string | null;
|
|
64
134
|
export declare function ensureBlockBadges(): void;
|
|
65
135
|
export declare function clearListItemSelection(scope?: ParentNode): void;
|
|
66
136
|
export declare function removeOverlayControls(deleteConfirmTimer: {
|
package/dist/bridge-functions.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* communicate back to the editor through injectable callbacks, decoupled from any
|
|
7
7
|
* specific transport (postMessage vs direct).
|
|
8
8
|
*/
|
|
9
|
-
import { isImagePath, parseInline, parseRichTextBlocks, normalizeRichTextBody, resolveRichTextHeadingLevel } from "@avocadostudio-ai/shared";
|
|
9
|
+
import { isAltPath, isImagePath, toImagePath, parseInline, parseRichTextBlocks, normalizeRichTextBody, resolveRichTextHeadingLevel } from "@avocadostudio-ai/shared";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Markdown → HTML for the live-draft overlay
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -141,6 +141,108 @@ export function findBlockNode(blockId) {
|
|
|
141
141
|
}
|
|
142
142
|
return document.querySelector(`[data-block-id='${blockId}']`);
|
|
143
143
|
}
|
|
144
|
+
/** How long we keep correcting a scroll after asking for it. */
|
|
145
|
+
const SCROLL_SETTLE_BUDGET_MS = 1500;
|
|
146
|
+
let programmaticScrollUntil = 0;
|
|
147
|
+
/**
|
|
148
|
+
* True while a scroll this module started is still running or being corrected.
|
|
149
|
+
*
|
|
150
|
+
* The editor treats an `iframeScrolled` report as the reader taking over —
|
|
151
|
+
* it drops the field anchor and stops following a running chat stream — so our
|
|
152
|
+
* own scrolling must not be reported. One flag for every scroll we initiate,
|
|
153
|
+
* rather than a marker at each call site, because the one call site that had no
|
|
154
|
+
* marker (block focus, which is what "Go to" uses) is exactly the one that
|
|
155
|
+
* scrolls in response to a click somewhere else.
|
|
156
|
+
*/
|
|
157
|
+
export function isProgrammaticScroll() {
|
|
158
|
+
return Date.now() < programmaticScrollUntil;
|
|
159
|
+
}
|
|
160
|
+
/** Close enough to centred that another correction would only read as a twitch. */
|
|
161
|
+
const SCROLL_SETTLE_TOLERANCE_PX = 32;
|
|
162
|
+
/** Between samples: long enough that a smooth animation shows movement. */
|
|
163
|
+
const SCROLL_SETTLE_INTERVAL_MS = 120;
|
|
164
|
+
/**
|
|
165
|
+
* Bring a block on screen and *keep* it there while the page settles.
|
|
166
|
+
*
|
|
167
|
+
* One `scrollIntoView` cannot do this. It resolves its destination from the
|
|
168
|
+
* layout at the instant it is called, and the preview is hardly ever settled at
|
|
169
|
+
* that instant — selection chrome is mounting, hero images are swapping in,
|
|
170
|
+
* blocks above the target are still changing height. Measured against the demo
|
|
171
|
+
* site, the target moved 478–1152px between the call and the end of the
|
|
172
|
+
* animation, every time, which left it *entirely above the viewport*: "Go to"
|
|
173
|
+
* appeared to jump to a random section, and the block the panel was now editing
|
|
174
|
+
* was nowhere on screen.
|
|
175
|
+
*
|
|
176
|
+
* So: scroll, then watch. Correct whenever the block has drifted out of the
|
|
177
|
+
* band we aimed for, and stop as soon as it holds still. Corrections wait for
|
|
178
|
+
* the previous scroll to finish (a stable `scrollY`) so we never interrupt the
|
|
179
|
+
* smooth animation we started, and they use `auto` — a second smooth animation
|
|
180
|
+
* queued behind the first reads as a lurch.
|
|
181
|
+
*
|
|
182
|
+
* A reader who starts scrolling wins immediately. Being 300px off is a smaller
|
|
183
|
+
* insult than dragging the page back out from under someone's hand.
|
|
184
|
+
*/
|
|
185
|
+
export function scrollBlockIntoView(el, options) {
|
|
186
|
+
const block = options?.block ?? "center";
|
|
187
|
+
/** Where `block` wants the element's top edge, in viewport coordinates. */
|
|
188
|
+
const desiredTop = () => {
|
|
189
|
+
const height = Math.min(el.getBoundingClientRect().height, window.innerHeight);
|
|
190
|
+
if (block === "start")
|
|
191
|
+
return 0;
|
|
192
|
+
if (block === "end")
|
|
193
|
+
return Math.max(0, window.innerHeight - height);
|
|
194
|
+
return Math.max(0, Math.round((window.innerHeight - height) / 2));
|
|
195
|
+
};
|
|
196
|
+
/** `nearest` asks only that the block be on screen, so that is all we check. */
|
|
197
|
+
const offTarget = () => {
|
|
198
|
+
const rect = el.getBoundingClientRect();
|
|
199
|
+
if (block === "nearest")
|
|
200
|
+
return rect.bottom <= 0 || rect.top >= window.innerHeight;
|
|
201
|
+
return Math.abs(rect.top - desiredTop()) > SCROLL_SETTLE_TOLERANCE_PX;
|
|
202
|
+
};
|
|
203
|
+
const deadline = Date.now() + SCROLL_SETTLE_BUDGET_MS;
|
|
204
|
+
// A little past the last correction we could make, so the scroll it settles
|
|
205
|
+
// into is not reported to the editor as the reader's own.
|
|
206
|
+
programmaticScrollUntil = deadline + SCROLL_SETTLE_INTERVAL_MS * 3;
|
|
207
|
+
el.scrollIntoView({ behavior: options?.behavior ?? "smooth", block, inline: "nearest" });
|
|
208
|
+
let lastY = window.scrollY;
|
|
209
|
+
let timer = 0;
|
|
210
|
+
const stop = () => {
|
|
211
|
+
window.clearInterval(timer);
|
|
212
|
+
window.removeEventListener("wheel", handOver);
|
|
213
|
+
window.removeEventListener("touchstart", handOver);
|
|
214
|
+
window.removeEventListener("keydown", handOver);
|
|
215
|
+
};
|
|
216
|
+
// The reader took the wheel. Give the scroll position back to them at once,
|
|
217
|
+
// including the right to have their scrolling reported as theirs.
|
|
218
|
+
const handOver = () => {
|
|
219
|
+
programmaticScrollUntil = 0;
|
|
220
|
+
stop();
|
|
221
|
+
};
|
|
222
|
+
window.addEventListener("wheel", handOver, { passive: true });
|
|
223
|
+
window.addEventListener("touchstart", handOver, { passive: true });
|
|
224
|
+
window.addEventListener("keydown", handOver);
|
|
225
|
+
timer = window.setInterval(() => {
|
|
226
|
+
if (Date.now() > deadline || !el.isConnected)
|
|
227
|
+
return stop();
|
|
228
|
+
const y = window.scrollY;
|
|
229
|
+
const wasMoving = y !== lastY;
|
|
230
|
+
lastY = y;
|
|
231
|
+
// Still animating — let it land before judging where it landed.
|
|
232
|
+
if (wasMoving)
|
|
233
|
+
return;
|
|
234
|
+
if (!offTarget())
|
|
235
|
+
return stop();
|
|
236
|
+
el.scrollIntoView({ behavior: "auto", block, inline: "nearest" });
|
|
237
|
+
// No movement means the document cannot get any closer — a block near the
|
|
238
|
+
// end of the page can never be centred, and retrying would only burn the
|
|
239
|
+
// budget re-deciding that.
|
|
240
|
+
if (window.scrollY === y)
|
|
241
|
+
stop();
|
|
242
|
+
else
|
|
243
|
+
lastY = window.scrollY;
|
|
244
|
+
}, SCROLL_SETTLE_INTERVAL_MS);
|
|
245
|
+
}
|
|
144
246
|
export function findEditableNode(parent, editablePath) {
|
|
145
247
|
const nodes = parent.querySelectorAll("[data-editable-target]");
|
|
146
248
|
for (const node of nodes) {
|
|
@@ -149,6 +251,44 @@ export function findEditableNode(parent, editablePath) {
|
|
|
149
251
|
}
|
|
150
252
|
return null;
|
|
151
253
|
}
|
|
254
|
+
/** Drop the last `.segment` of a path: `cards[0].ctaHref` → `cards[0]`. */
|
|
255
|
+
function parentPath(editablePath) {
|
|
256
|
+
const cut = editablePath.lastIndexOf(".");
|
|
257
|
+
return cut > 0 ? editablePath.slice(0, cut) : null;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The node to point at for a field, which is not always the field's own node.
|
|
261
|
+
*
|
|
262
|
+
* Plenty of editable props are never rendered as an element. Alt text is an
|
|
263
|
+
* attribute on an image; a link target is an attribute on an anchor. A renderer
|
|
264
|
+
* emits `data-editable-target` for what it *draws*, so `cards[0].imageAlt` and
|
|
265
|
+
* `cards[0].ctaHref` match nothing at all, and asking to show somebody where
|
|
266
|
+
* the problem is has so far shown them nothing.
|
|
267
|
+
*
|
|
268
|
+
* Three steps, most specific first:
|
|
269
|
+
* 1. the field's own node, when it has one
|
|
270
|
+
* 2. for alt text, the image it describes — the only thing on the page that
|
|
271
|
+
* alt text *is* about, and the inverse of the `toAltPath` convention the
|
|
272
|
+
* renderers already follow
|
|
273
|
+
* 3. the nearest enclosing node: the card, the list item, the block. Less
|
|
274
|
+
* precise, and still the difference between "that one" and nothing.
|
|
275
|
+
*/
|
|
276
|
+
export function resolveEditableNode(parent, editablePath) {
|
|
277
|
+
const exact = findEditableNode(parent, editablePath);
|
|
278
|
+
if (exact)
|
|
279
|
+
return exact;
|
|
280
|
+
if (isAltPath(editablePath)) {
|
|
281
|
+
const image = findEditableNode(parent, toImagePath(editablePath));
|
|
282
|
+
if (image)
|
|
283
|
+
return image;
|
|
284
|
+
}
|
|
285
|
+
for (let path = parentPath(editablePath); path; path = parentPath(path)) {
|
|
286
|
+
const node = findEditableNode(parent, path);
|
|
287
|
+
if (node)
|
|
288
|
+
return node;
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
152
292
|
// Block-level tags that signal a field React rendered as parsed-markdown element
|
|
153
293
|
// children (e.g. RichText `body`, Tabs `content`) rather than a single text node.
|
|
154
294
|
const RICH_EDITABLE_SELECTOR = "p,ul,ol,li,blockquote,pre,table,h1,h2,h3,h4,h5,h6,hr";
|
|
@@ -334,6 +474,31 @@ export function showSkeleton(afterBlockId, blockType) {
|
|
|
334
474
|
export function removeSkeletons() {
|
|
335
475
|
document.querySelectorAll(".editor-skeleton-block").forEach((node) => node.remove());
|
|
336
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* The one misintegration the overlay can diagnose for itself.
|
|
479
|
+
*
|
|
480
|
+
* Selection is entirely `[data-block-id]`: a click resolves through
|
|
481
|
+
* `closest("[data-block-id]")`, and a null result is read as "clicked outside
|
|
482
|
+
* any block", which *clears* the selection. A site that added
|
|
483
|
+
* `data-editable-target` to its components but no block wrappers therefore
|
|
484
|
+
* frames, renders, styles and scrolls correctly, and silently deselects on
|
|
485
|
+
* every click — and the README has already trained the reader to expect a
|
|
486
|
+
* preview that ignores clicks, because that is what selection mode being off
|
|
487
|
+
* looks like.
|
|
488
|
+
*
|
|
489
|
+
* Editable targets with no wrapper around them is unambiguous: nobody adds the
|
|
490
|
+
* per-field attributes by accident. Say so once, loudly, in development.
|
|
491
|
+
*/
|
|
492
|
+
export function missingBlockWrapperWarning(blockCount, editableCount) {
|
|
493
|
+
if (blockCount > 0 || editableCount === 0)
|
|
494
|
+
return null;
|
|
495
|
+
return (`[avocado] The overlay found ${editableCount} data-editable-target attribute(s) and no ` +
|
|
496
|
+
"[data-block-id] wrapper. Nothing on this page can be selected: every click resolves to " +
|
|
497
|
+
"no block and clears the selection, whether or not selection mode is on.\n" +
|
|
498
|
+
"Wrap each block with getPreviewWrapperProps(editorMode, block.id, block.type) from " +
|
|
499
|
+
"@avocadostudio-ai/site-sdk/editor — it sets data-block-id, data-block-type and the " +
|
|
500
|
+
"editor-selectable class that the field pills are scoped to.");
|
|
501
|
+
}
|
|
337
502
|
export function ensureBlockBadges() {
|
|
338
503
|
document.querySelectorAll("[data-block-id]").forEach((node) => {
|
|
339
504
|
const blockType = node.getAttribute("data-block-type") ?? "Block";
|
|
@@ -710,7 +875,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
710
875
|
const parent = findBlockNode(parentBlockId);
|
|
711
876
|
if (!parent)
|
|
712
877
|
return;
|
|
713
|
-
const child =
|
|
878
|
+
const child = resolveEditableNode(parent, editablePath);
|
|
714
879
|
if (!child)
|
|
715
880
|
return;
|
|
716
881
|
child.classList.add("editor-child-highlight");
|
|
@@ -1103,7 +1268,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1103
1268
|
state.pendingScrollAnchorY = null;
|
|
1104
1269
|
}
|
|
1105
1270
|
else {
|
|
1106
|
-
match
|
|
1271
|
+
scrollBlockIntoView(match);
|
|
1107
1272
|
}
|
|
1108
1273
|
}
|
|
1109
1274
|
if (shouldAnimate) {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { PreviewBridge } from "./preview-bridge.tsx";
|
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.tsx";
|
|
3
3
|
export type { PreviewBridgeConfig, PreviewBridgeCoreProps } from "./preview-bridge-core.tsx";
|
|
4
4
|
export { getPreviewWrapperProps } from "./selectable.ts";
|
|
5
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.ts";
|
|
5
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.ts";
|
|
6
6
|
export type { BridgeCallbacks, BridgeState, BridgeFunctions } from "./bridge-functions.ts";
|
|
7
7
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.tsx";
|
|
8
8
|
export type { LivePreviewPage, LivePreviewBridgeApi } from "./live-preview-store.tsx";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { PreviewBridge } from "./preview-bridge.js";
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.js";
|
|
3
3
|
export { getPreviewWrapperProps } from "./selectable.js";
|
|
4
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.js";
|
|
4
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.js";
|
|
5
5
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
|
-
import { createBridgeFunctions, createBridgeState, findBlockNode, parseListItemPath, ensureBlockBadges, setNestedLabelsVisibility, showSkeleton, removeSkeletons, clearChildFocus, clearAllHighlights, clearListItemSelection, applyAiFieldLoading, cleanupOverlayElements, withPreviewParams, } from "./bridge-functions.js";
|
|
4
|
+
import { createBridgeFunctions, createBridgeState, findBlockNode, isProgrammaticScroll, parseListItemPath, ensureBlockBadges, missingBlockWrapperWarning, scrollBlockIntoView, setNestedLabelsVisibility, showSkeleton, removeSkeletons, clearChildFocus, clearAllHighlights, clearListItemSelection, applyAiFieldLoading, cleanupOverlayElements, withPreviewParams, } from "./bridge-functions.js";
|
|
5
5
|
export function PreviewBridgeCore(props) {
|
|
6
6
|
// When running standalone (no editor origin) or not embedded in an iframe, render nothing.
|
|
7
7
|
if (!props.editorOrigin || typeof window !== "undefined" && window.parent === window)
|
|
@@ -10,7 +10,6 @@ export function PreviewBridgeCore(props) {
|
|
|
10
10
|
}
|
|
11
11
|
function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathname, liveStore }) {
|
|
12
12
|
const stateRef = useRef(null);
|
|
13
|
-
const programmaticScrollUntilRef = useRef(0);
|
|
14
13
|
useEffect(() => {
|
|
15
14
|
// -- postMessage helpers ------------------------------------------------
|
|
16
15
|
const postToEditor = (type, payload) => {
|
|
@@ -50,6 +49,18 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
50
49
|
clearListItemSelection();
|
|
51
50
|
ensureBlockBadges();
|
|
52
51
|
bridge.mountGlobalImageButtons();
|
|
52
|
+
/*
|
|
53
|
+
* Deferred a frame so a page still streaming in is not accused of having no
|
|
54
|
+
* blocks. Development only: in production the site owner cannot act on it
|
|
55
|
+
* and the visitor should never see it.
|
|
56
|
+
*/
|
|
57
|
+
if (process.env.NODE_ENV !== "production") {
|
|
58
|
+
requestAnimationFrame(() => {
|
|
59
|
+
const warning = missingBlockWrapperWarning(document.querySelectorAll("[data-block-id]").length, document.querySelectorAll("[data-editable-target]").length);
|
|
60
|
+
if (warning)
|
|
61
|
+
console.error(warning);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
53
64
|
// -- Mutation observer -------------------------------------------------
|
|
54
65
|
let detectNewBlocksRaf = null;
|
|
55
66
|
const scheduleDetectNewBlocks = () => {
|
|
@@ -255,10 +266,7 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
255
266
|
const el = findBlockNode(blockId);
|
|
256
267
|
if (!el)
|
|
257
268
|
return false;
|
|
258
|
-
|
|
259
|
-
// interpret our own scroll as a user cancel.
|
|
260
|
-
programmaticScrollUntilRef.current = Date.now() + 800;
|
|
261
|
-
el.scrollIntoView({ behavior, block, inline: "nearest" });
|
|
269
|
+
scrollBlockIntoView(el, { behavior, block });
|
|
262
270
|
return true;
|
|
263
271
|
};
|
|
264
272
|
// The block may not yet be in the DOM if the iframe just re-rendered;
|
|
@@ -271,7 +279,7 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
271
279
|
// -- Scroll handler ----------------------------------------------------
|
|
272
280
|
const onScroll = () => {
|
|
273
281
|
// Swallow the scroll event if it originated from our own scrollIntoView.
|
|
274
|
-
if (
|
|
282
|
+
if (isProgrammaticScroll())
|
|
275
283
|
return;
|
|
276
284
|
callbacks.onScroll();
|
|
277
285
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/preview-adapter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"src/styles.css"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@avocadostudio-ai/shared": "^0.3.
|
|
35
|
+
"@avocadostudio-ai/shared": "^0.3.3"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"next": ">=15.0.0",
|