@cfp-studio/iframe 18.3.0 → 18.4.1
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 +6 -0
- package/package.json +2 -2
- package/preview/__previewjs_internal__/copy-cut-paste.ts +78 -72
- package/preview/__previewjs_internal__/error-message.ts +34 -34
- package/preview/__previewjs_internal__/index.ts +70 -71
- package/preview/__previewjs_internal__/links.ts +27 -27
- package/preview/__previewjs_internal__/logs.ts +94 -94
- package/preview/__previewjs_internal__/object-inspect.js +458 -540
- package/preview/__previewjs_internal__/overlay.spec.ts +86 -0
- package/preview/__previewjs_internal__/overlay.ts +197 -6
- package/preview/__previewjs_internal__/renderer/index.tsx +2 -2
- package/preview/__previewjs_internal__/run-renderer.ts +94 -97
- package/preview/__previewjs_internal__/runtime-bridge.ts +183 -1
- package/preview/__previewjs_internal__/state.ts +2 -2
- package/preview/__previewjs_internal__/vite-hmr-listener.ts +67 -66
- package/preview/index.html +78 -79
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { pickActionId, runOverlayAction, setOverlayActionHandler } from "./overlay";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 覆盖层按钮的命中判定(纯逻辑)。
|
|
7
|
+
*
|
|
8
|
+
* 这段出过两个只能手测发现的静默 bug,所以至少把「元素栈 → 动作 id」抽出来断言:
|
|
9
|
+
* - 用 `button.click()` 伪造点击时坐标是 0,会被再次命中 → 选中左上角元素;
|
|
10
|
+
* - 整个覆盖层设成可交互后,inspect 关闭时变成挡住应用的透明蒙版。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 造一个最小可用的伪元素:只实现 `closest` 与 `dataset`。 */
|
|
14
|
+
function fakeButton(actionId?: string): Element {
|
|
15
|
+
const self: { dataset: Record<string, string>; closest: (selector: string) => Element | null } = {
|
|
16
|
+
dataset: actionId ? { cfpActionId: actionId } : {},
|
|
17
|
+
closest: (selector: string) =>
|
|
18
|
+
selector.includes("data-cfp-action-id") && actionId ? (self as unknown as Element) : null,
|
|
19
|
+
};
|
|
20
|
+
return self as unknown as Element;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 按钮内部的图标元素:自身没有 dataset,`closest` 能回溯到按钮。 */
|
|
24
|
+
function fakeIcon(button: Element): Element {
|
|
25
|
+
return {
|
|
26
|
+
closest: (selector: string) => (selector.includes("data-cfp-action-id") ? button : null),
|
|
27
|
+
} as unknown as Element;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 普通应用元素:`closest` 找不到按钮。 */
|
|
31
|
+
function fakePageElement(): Element {
|
|
32
|
+
return { closest: () => null } as unknown as Element;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("pickActionId", () => {
|
|
36
|
+
test("栈里有按钮时返回它的动作 id", () => {
|
|
37
|
+
expect(pickActionId([fakeButton("screenshot")])).toBe("screenshot");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("点在按钮内的 svg 上也能命中(图标是按钮的子元素)", () => {
|
|
41
|
+
// 真实场景:栈顶是 <path>/<svg>,按钮在下一层。
|
|
42
|
+
const button = fakeButton("copy-context");
|
|
43
|
+
expect(pickActionId([fakeIcon(button), button])).toBe("copy-context");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("按钮有 data 但值为空时算未命中", () => {
|
|
47
|
+
// 防止空字符串被当成合法动作 id 发出去。
|
|
48
|
+
expect(pickActionId([fakeButton(""), fakePageElement()])).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("纯页面元素(点到应用本身)→ undefined", () => {
|
|
52
|
+
expect(pickActionId([fakePageElement(), fakePageElement()])).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("空栈 → undefined", () => {
|
|
56
|
+
expect(pickActionId([])).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("栈里同时有页面元素和按钮时,优先返回按钮", () => {
|
|
60
|
+
// 这是「点按钮不该选中底层元素」的核心断言。
|
|
61
|
+
expect(pickActionId([fakePageElement(), fakeButton("screenshot")])).toBe("screenshot");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("元素没有 closest 方法时不崩", () => {
|
|
65
|
+
// `elementsFromPoint` 在某些环境会返回非 Element 的对象。
|
|
66
|
+
expect(pickActionId([{} as Element])).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("按钮的触发路径唯一(避免一次点击执行两次)", () => {
|
|
71
|
+
// 回归:按钮曾自带 click 监听器,而 inspect 的 handleInspectClick 挂在 document
|
|
72
|
+
// 捕获阶段、先于它执行 —— 同一个动作跑两遍(实测点一次 📋 生成两个 packet,
|
|
73
|
+
// selectionId 只差最后一位)。现在只有 `runOverlayAction` 一处触发。
|
|
74
|
+
test("runOverlayAction 只回调一次", async () => {
|
|
75
|
+
const calls: string[] = [];
|
|
76
|
+
setOverlayActionHandler((id) => calls.push(id));
|
|
77
|
+
runOverlayAction("copy-context");
|
|
78
|
+
expect(calls).toEqual(["copy-context"]);
|
|
79
|
+
setOverlayActionHandler(undefined);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("没有注册处理函数时不抛错", () => {
|
|
83
|
+
setOverlayActionHandler(undefined);
|
|
84
|
+
expect(() => runOverlayAction("screenshot")).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -15,6 +15,35 @@ export interface PageOverlayItem {
|
|
|
15
15
|
label?: string;
|
|
16
16
|
color?: string;
|
|
17
17
|
borderWidth?: number;
|
|
18
|
+
/**
|
|
19
|
+
* 边框线型。
|
|
20
|
+
*
|
|
21
|
+
* inspect 用线型区分状态:`dashed` = 鼠标悬停(跟着指针走),
|
|
22
|
+
* `solid` = 已点选(停留不动)。只靠颜色区分对色弱用户不够。
|
|
23
|
+
*/
|
|
24
|
+
borderStyle?: "solid" | "dashed";
|
|
25
|
+
/** 背景透明度倍数;选中态更淡,避免遮住内容。 */
|
|
26
|
+
backgroundAlpha?: number;
|
|
27
|
+
/**
|
|
28
|
+
* label 右侧的 inline 按钮(inspect 选中态用:截图 / 复制上下文)。
|
|
29
|
+
*
|
|
30
|
+
* `id` 由调用方定义,点击时通过 `onOverlayAction` 回调回传。
|
|
31
|
+
*/
|
|
32
|
+
actions?: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
/**
|
|
36
|
+
* 内联 SVG 元素的完整标记(不含 `<svg>` 外层)。
|
|
37
|
+
*
|
|
38
|
+
* 为什么不用 emoji / 字体图标:overlay 在 iframe 内的 Shadow DOM 里,
|
|
39
|
+
* 拿不到 VS Code 的 codicon 字体;emoji 在不同平台字形与基线不一致。
|
|
40
|
+
*
|
|
41
|
+
* 为什么用**描边**而不是填充:实测填充版(含 `evenodd`)缩到 12px 后
|
|
42
|
+
* 相机与剪贴板都变成实心色块,镜头/分隔线完全看不清。描边版在
|
|
43
|
+
* 同一尺寸下轮廓清晰。
|
|
44
|
+
*/
|
|
45
|
+
iconMarkup: string;
|
|
46
|
+
}>;
|
|
18
47
|
}
|
|
19
48
|
|
|
20
49
|
export interface PageOverlayPayload {
|
|
@@ -22,6 +51,32 @@ export interface PageOverlayPayload {
|
|
|
22
51
|
items: PageOverlayItem[];
|
|
23
52
|
}
|
|
24
53
|
|
|
54
|
+
/**
|
|
55
|
+
* 把图标标记包进 `<svg>`。
|
|
56
|
+
*
|
|
57
|
+
* 用描边(`stroke: currentColor`)而非填充 —— 12px 下轮廓比实心块更易辨认。
|
|
58
|
+
* `vector-effect: non-scaling-stroke` 保证缩放到按钮尺寸后线宽不被压细。
|
|
59
|
+
*/
|
|
60
|
+
function renderIconSvg(markup: string): string {
|
|
61
|
+
return (
|
|
62
|
+
`<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" ` +
|
|
63
|
+
`stroke-width="1.4" stroke-linejoin="round" stroke-linecap="round" ` +
|
|
64
|
+
`style="vector-effect:non-scaling-stroke" aria-hidden="true">${markup}</svg>`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 覆盖层按钮的点击回调。由 bridge 注册(拿到 runtimeNodeId 后发消息给宿主)。
|
|
70
|
+
*
|
|
71
|
+
* 用回调而不是在 overlay 里直接 postMessage:overlay 只管画,
|
|
72
|
+
* 不知道「当前选中哪个节点」这类业务状态。
|
|
73
|
+
*/
|
|
74
|
+
let onOverlayAction: ((actionId: string) => void) | undefined;
|
|
75
|
+
|
|
76
|
+
export function setOverlayActionHandler(handler: ((actionId: string) => void) | undefined): void {
|
|
77
|
+
onOverlayAction = handler;
|
|
78
|
+
}
|
|
79
|
+
|
|
25
80
|
let host: HTMLDivElement | undefined;
|
|
26
81
|
let shadowRoot: ShadowRoot | undefined;
|
|
27
82
|
const layers = new Map<string, HTMLDivElement>();
|
|
@@ -43,10 +98,46 @@ export function setPageOverlay(layerId: string, payload: PageOverlayPayload | un
|
|
|
43
98
|
element.style.height = `${item.height}px`;
|
|
44
99
|
element.style.borderColor = item.color ?? "#3794ff";
|
|
45
100
|
element.style.borderWidth = `${item.borderWidth ?? 1}px`;
|
|
101
|
+
if (item.borderStyle) {
|
|
102
|
+
element.style.borderStyle = item.borderStyle;
|
|
103
|
+
}
|
|
104
|
+
if (item.backgroundAlpha !== undefined) {
|
|
105
|
+
// 颜色里的 alpha 写死在 CSS 里不方便逐项调,这里直接覆盖背景。
|
|
106
|
+
element.style.background = `rgba(55, 148, 255, ${item.backgroundAlpha})`;
|
|
107
|
+
}
|
|
46
108
|
if (item.label) {
|
|
47
109
|
const label = document.createElement("span");
|
|
48
110
|
label.className = "cfp-overlay-label";
|
|
49
|
-
|
|
111
|
+
// 标签底色跟随该 item 的边框色:hover 是蓝、选中是橙。
|
|
112
|
+
// 之前写死在 CSS 里,导致 hover 的标签也显示成选中的橙色(实测反馈)。
|
|
113
|
+
label.style.setProperty("--cfp-label-bg", item.color ?? "#3794ff");
|
|
114
|
+
// 文字与按钮是**两个独立的圆角块**(不是同一块里加分隔线):
|
|
115
|
+
// 外层 `label` 只做定位与排布,背景落在两个子块上。
|
|
116
|
+
const text = document.createElement("span");
|
|
117
|
+
text.className = "cfp-overlay-label-text";
|
|
118
|
+
text.textContent = item.label;
|
|
119
|
+
label.appendChild(text);
|
|
120
|
+
if (item.actions?.length) {
|
|
121
|
+
const group = document.createElement("span");
|
|
122
|
+
group.className = "cfp-overlay-actions";
|
|
123
|
+
for (const action of item.actions) {
|
|
124
|
+
const button = document.createElement("button");
|
|
125
|
+
button.className = "cfp-overlay-action";
|
|
126
|
+
button.type = "button";
|
|
127
|
+
// 供 `overlayActionAt` 命中测试用。
|
|
128
|
+
button.dataset.cfpActionId = action.id;
|
|
129
|
+
button.title = action.title;
|
|
130
|
+
button.setAttribute("aria-label", action.title);
|
|
131
|
+
button.innerHTML = renderIconSvg(action.iconMarkup);
|
|
132
|
+
// **不要给按钮加 click 监听器**:inspect 的 `handleInspectClick` 挂在
|
|
133
|
+
// document 的**捕获阶段**,它先跑(命中按钮 → `runOverlayAction`),
|
|
134
|
+
// 之后事件继续传播到按钮自己的监听器 —— 同一个动作会执行两次
|
|
135
|
+
// (实测:点一次 📋 生成两个 packet,selectionId 只差最后一位)。
|
|
136
|
+
// 捕获阶段那处已经 stopPropagation,按钮这里不需要(也无法)再挡。
|
|
137
|
+
group.appendChild(button);
|
|
138
|
+
}
|
|
139
|
+
label.appendChild(group);
|
|
140
|
+
}
|
|
50
141
|
element.appendChild(label);
|
|
51
142
|
}
|
|
52
143
|
layer.appendChild(element);
|
|
@@ -124,6 +215,52 @@ export function isOverlayChrome(element: Element): boolean {
|
|
|
124
215
|
return Boolean(element.closest("[data-cfp-overlay], [data-cfp-shield]"));
|
|
125
216
|
}
|
|
126
217
|
|
|
218
|
+
/**
|
|
219
|
+
* 某个页面坐标是否落在覆盖层的**按钮**上。
|
|
220
|
+
*
|
|
221
|
+
* 为什么需要它:inspect 的点击监听挂在 `document` 的**捕获阶段**
|
|
222
|
+
* (`addEventListener("click", handler, true)`),比按钮自己的 click 先跑;
|
|
223
|
+
* 而 `document.elementsFromPoint()` 不穿透 shadow DOM,只能看到 host。
|
|
224
|
+
* 于是「点按钮」会被当成「点页面」,选中了父元素 —— 按钮形同虚设。
|
|
225
|
+
*
|
|
226
|
+
* 这里用 `shadowRoot.elementsFromPoint` 直接问覆盖层内部,是唯一可靠的判定。
|
|
227
|
+
* 找到按钮就返回它的 `id`,调用方据此执行动作并跳过选中逻辑。
|
|
228
|
+
*/
|
|
229
|
+
export function overlayActionAt(clientX: number, clientY: number): string | undefined {
|
|
230
|
+
const root = shadowRoot;
|
|
231
|
+
if (!root || !host?.isConnected) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return pickActionId(root.elementsFromPoint?.(clientX, clientY) ?? []);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 从 `elementsFromPoint` 的栈里挑出按钮的动作 id(纯函数,便于单测)。
|
|
239
|
+
*
|
|
240
|
+
* 抽出来的原因:这段逻辑出过两个静默 bug ——
|
|
241
|
+
* 1. 用 `button.click()` 伪造点击时 clientX/Y 是 0,会被再次命中并选中左上角元素;
|
|
242
|
+
* 2. 整个覆盖层设成可交互后,inspect 关闭时它变成一张挡住应用的透明蒙版。
|
|
243
|
+
* 两者都不容易用手测复现,至少让「栈 → 动作」这一步可以被断言。
|
|
244
|
+
*
|
|
245
|
+
* 判定用 `closest` 而不是 `instanceof HTMLButtonElement`:图标是按钮内的 `<svg>`,
|
|
246
|
+
* 点在图标的 path 上时栈顶不是 button 本身。
|
|
247
|
+
*/
|
|
248
|
+
export function pickActionId(stack: readonly Element[]): string | undefined {
|
|
249
|
+
for (const element of stack) {
|
|
250
|
+
const button = element.closest?.("button[data-cfp-action-id]") as HTMLElement | null;
|
|
251
|
+
const id = button?.dataset?.cfpActionId;
|
|
252
|
+
if (id) {
|
|
253
|
+
return id;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 执行某个覆盖层按钮的动作(由 `overlayActionAt` 的结果驱动)。 */
|
|
260
|
+
export function runOverlayAction(actionId: string): void {
|
|
261
|
+
onOverlayAction?.(actionId);
|
|
262
|
+
}
|
|
263
|
+
|
|
127
264
|
function ensureLayer(layerId: string): HTMLDivElement {
|
|
128
265
|
const existing = layers.get(layerId);
|
|
129
266
|
if (existing?.isConnected) {
|
|
@@ -147,7 +284,14 @@ function ensureHost(): void {
|
|
|
147
284
|
host = document.createElement("div");
|
|
148
285
|
host.dataset.cfpOverlay = "true";
|
|
149
286
|
host.setAttribute("aria-hidden", "true");
|
|
150
|
-
|
|
287
|
+
// `mode: "open"` 是必需的,不能改回 closed。
|
|
288
|
+
//
|
|
289
|
+
// `document.elementsFromPoint()` **不穿透 shadow DOM**:closed 模式下它只返回
|
|
290
|
+
// 外层 host 元素,`event.target` 也被 retarget 成 host ——
|
|
291
|
+
// 于是 inspect 的点击检测永远拿不到按钮,表现为「点按钮却选中了父元素」。
|
|
292
|
+
// open 模式下 elementsFromPoint 会返回 shadow 内部的按钮,
|
|
293
|
+
// `isOverlayChrome`(用 closest 判断)也才能把它们识别为覆盖层内容。
|
|
294
|
+
shadowRoot = host.attachShadow({ mode: "open" });
|
|
151
295
|
const style = document.createElement("style");
|
|
152
296
|
style.textContent = `
|
|
153
297
|
:host {
|
|
@@ -171,16 +315,63 @@ function ensureHost(): void {
|
|
|
171
315
|
background: rgba(55, 148, 255, 0.12);
|
|
172
316
|
}
|
|
173
317
|
.cfp-overlay-label {
|
|
318
|
+
/*
|
|
319
|
+
* 只有标签自己可交互,**整层保持 pointer-events: none**。
|
|
320
|
+
*
|
|
321
|
+
* 早期把整层设为 auto,结果:inspect 关闭时也会渲染选中框(树→预览同步),
|
|
322
|
+
* 于是那块透明的全屏层盖住了应用,页面从此点不动,直到重开 inspect 或刷新
|
|
323
|
+
* (实测反馈)。现在标签外的区域仍然穿透。
|
|
324
|
+
*/
|
|
325
|
+
pointer-events: auto;
|
|
326
|
+
/* 只做定位与排布:背景在两个字块上,中间留 gap 形成视觉间隔。 */
|
|
174
327
|
position: absolute;
|
|
175
328
|
left: 0;
|
|
176
|
-
top: -
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
329
|
+
top: -22px;
|
|
330
|
+
display: flex;
|
|
331
|
+
align-items: center;
|
|
332
|
+
gap: 4px;
|
|
180
333
|
color: white;
|
|
181
334
|
font: 11px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
182
335
|
white-space: nowrap;
|
|
183
336
|
}
|
|
337
|
+
.cfp-overlay-label-text,
|
|
338
|
+
.cfp-overlay-actions {
|
|
339
|
+
/* 两个独立的圆角块;底色由 --cfp-label-bg 决定(跟随 item.color)。 */
|
|
340
|
+
display: inline-flex;
|
|
341
|
+
align-items: center;
|
|
342
|
+
padding: 2px 6px;
|
|
343
|
+
border-radius: 3px;
|
|
344
|
+
background: var(--cfp-label-bg, #3794ff);
|
|
345
|
+
}
|
|
346
|
+
.cfp-overlay-actions {
|
|
347
|
+
/* 按钮块自身的内边距小一些,图标周围不显得松。 */
|
|
348
|
+
padding: 2px 3px;
|
|
349
|
+
gap: 3px;
|
|
350
|
+
}
|
|
351
|
+
.cfp-overlay-action {
|
|
352
|
+
/* 父级是 pointer-events:none(不挡页面命中测试),按钮要单独开启。 */
|
|
353
|
+
pointer-events: auto;
|
|
354
|
+
display: inline-flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
justify-content: center;
|
|
357
|
+
width: 16px;
|
|
358
|
+
height: 16px;
|
|
359
|
+
padding: 0;
|
|
360
|
+
border: 0;
|
|
361
|
+
border-radius: 2px;
|
|
362
|
+
background: rgba(255, 255, 255, 0.22);
|
|
363
|
+
color: inherit;
|
|
364
|
+
font-size: 10px;
|
|
365
|
+
line-height: 1;
|
|
366
|
+
cursor: pointer;
|
|
367
|
+
}
|
|
368
|
+
.cfp-overlay-action svg {
|
|
369
|
+
display: block;
|
|
370
|
+
pointer-events: none;
|
|
371
|
+
}
|
|
372
|
+
.cfp-overlay-action:hover {
|
|
373
|
+
background: rgba(255, 255, 255, 0.4);
|
|
374
|
+
}
|
|
184
375
|
`;
|
|
185
376
|
shadowRoot.appendChild(style);
|
|
186
377
|
document.documentElement.appendChild(host);
|
|
@@ -7,6 +7,6 @@ import type { RendererLoader } from "../../../src";
|
|
|
7
7
|
* @throws {Error} 当该占位函数被调用时,会抛出“Dummy loader was invoked!”错误。
|
|
8
8
|
*/
|
|
9
9
|
export const loadRenderer: RendererLoader = () => {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// 这是一个占位函数,在运行时会被特定框架的实现所替换。
|
|
11
|
+
throw new Error("调用到占位 loader 函数");
|
|
12
12
|
};
|
|
@@ -3,73 +3,72 @@ import { publishRuntimeSnapshot } from "./runtime-bridge";
|
|
|
3
3
|
import { getState } from "./state";
|
|
4
4
|
|
|
5
5
|
export async function runRenderer({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
wrapperModule,
|
|
7
|
+
wrapperName,
|
|
8
|
+
previewableModule,
|
|
9
|
+
previewableName,
|
|
10
|
+
renderId,
|
|
11
|
+
shouldAbortRender,
|
|
12
|
+
loadRenderer,
|
|
13
13
|
}: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
wrapperModule: any;
|
|
15
|
+
wrapperName: string;
|
|
16
|
+
previewableModule: any;
|
|
17
|
+
previewableName: string;
|
|
18
|
+
renderId: number;
|
|
19
|
+
shouldAbortRender: () => boolean;
|
|
20
|
+
loadRenderer: RendererLoader;
|
|
21
21
|
}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`
|
|
22
|
+
const currentState = getState();
|
|
23
|
+
if (!currentState || shouldAbortRender()) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
27
|
+
kind: "before-render",
|
|
28
|
+
});
|
|
29
|
+
const { render, jsxFactory } = await loadRenderer({
|
|
30
|
+
wrapperModule,
|
|
31
|
+
wrapperName,
|
|
32
|
+
previewableModule,
|
|
33
|
+
previewableName,
|
|
34
|
+
renderId,
|
|
35
|
+
shouldAbortRender,
|
|
36
|
+
});
|
|
37
|
+
if (shouldAbortRender()) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { autogenCallbackProps, properties } = await previewableModule.PreviewJsEval(
|
|
41
|
+
`
|
|
43
42
|
let ${currentState.autogenCallbackPropsSource};
|
|
44
43
|
let ${currentState.propsAssignmentSource};
|
|
45
44
|
return { autogenCallbackProps, properties };
|
|
46
45
|
`,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
{
|
|
47
|
+
__jsxFactory__: jsxFactory,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
if (shouldAbortRender()) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Fix: break transformFunctions(properties || presetProps, []) into 2 parts
|
|
54
|
+
// 1. transformFunctions(properties, [])
|
|
55
|
+
// 2. transformFunctions(presetProps, [])
|
|
56
|
+
await render(({ presetProps, presetGlobalProps }) => ({
|
|
57
|
+
...transformFunctions(autogenCallbackProps, []),
|
|
58
|
+
...transformFunctions(presetGlobalProps, []),
|
|
59
|
+
...transformFunctions(properties, []),
|
|
60
|
+
...transformFunctions(presetProps, []),
|
|
61
|
+
}));
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
if (shouldAbortRender()) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Publish before reportEvent: custom preview webviews may not install a
|
|
67
|
+
// __PREVIEWJS_CONTROLLER__, while the runtime bridge is independent.
|
|
68
|
+
setTimeout(publishRuntimeSnapshot, 0);
|
|
69
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
70
|
+
kind: "rendered",
|
|
71
|
+
});
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
/**
|
|
@@ -78,41 +77,39 @@ export async function runRenderer({
|
|
|
78
77
|
*/
|
|
79
78
|
|
|
80
79
|
function transformFunctions(value: any, path: string[]): any {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
return value;
|
|
80
|
+
if (value && typeof value === "object") {
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
// Array.
|
|
83
|
+
return value.map((v, i) => transformFunctions(v, [...path, i.toString(10)]));
|
|
84
|
+
}
|
|
85
|
+
if (value.constructor === Object) {
|
|
86
|
+
// Plain object (i.e. not Set or Map or any class instance).
|
|
87
|
+
if (value.$$typeof) {
|
|
88
|
+
// This is likely a React component, e.g. instantiated in a JSX expression.
|
|
89
|
+
// We don't want to show an action invoked when the JSX function is invoked,
|
|
90
|
+
// so skip it.
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
if (value.__v_isVNode) {
|
|
94
|
+
// This is likely a Vue component (at least Vue 3).
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
return Object.fromEntries(
|
|
98
|
+
Object.entries(value).map(([k, v]) => [
|
|
99
|
+
k,
|
|
100
|
+
typeof v === "function"
|
|
101
|
+
? (...args: unknown[]) => {
|
|
102
|
+
window.__PREVIEWJS_IFRAME__.reportEvent({
|
|
103
|
+
kind: "action",
|
|
104
|
+
type: "fn",
|
|
105
|
+
path: [...path, k].join("."),
|
|
106
|
+
});
|
|
107
|
+
return v(...args);
|
|
108
|
+
}
|
|
109
|
+
: transformFunctions(v, [...path, k]),
|
|
110
|
+
]),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
118
115
|
}
|