vue-wswg-editor 0.0.12 → 0.0.14
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 +23 -8
- package/dist/style.css +1 -1
- package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +1 -0
- package/dist/types/components/EditorPageRenderer/EditorPageRenderer.vue.d.ts +21 -0
- package/dist/types/components/EmptyState/EmptyState.vue.d.ts +2 -8
- package/dist/types/components/IframePreview/IframePreview.vue.d.ts +26 -0
- package/dist/types/components/IframePreview/iframeContent.d.ts +9 -0
- package/dist/types/components/IframePreview/iframePreviewApp.d.ts +36 -0
- package/dist/types/components/IframePreview/messageHandler.d.ts +55 -0
- package/dist/types/components/IframePreview/types.d.ts +77 -0
- package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +2 -0
- package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +2 -0
- package/dist/types/components/PageSettings/PageSettings.vue.d.ts +2 -0
- package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.vue.d.ts → WswgPageBuilder/WswgPageBuilder.vue.d.ts} +2 -0
- package/dist/types/index.d.ts +8 -2
- package/dist/types/util/registry.d.ts +2 -0
- package/dist/types/util/theme-registry.d.ts +42 -0
- package/dist/types/util/validation.d.ts +2 -2
- package/dist/vite-plugin.js +33 -29
- package/dist/vue-wswg-editor.es.js +2783 -1905
- package/package.json +1 -2
- package/src/assets/styles/_mixins.scss +15 -0
- package/src/components/AddBlockItem/AddBlockItem.vue +13 -4
- package/src/components/BlockBrowser/BlockBrowser.vue +5 -5
- package/src/components/BlockComponent/BlockComponent.vue +23 -50
- package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +12 -10
- package/src/components/BlockEditorFields/BlockEditorFields.vue +24 -4
- package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +9 -4
- package/src/components/BrowserNavigation/BrowserNavigation.vue +1 -1
- package/src/components/EditorPageRenderer/EditorPageRenderer.vue +641 -0
- package/src/components/EmptyState/EmptyState.vue +3 -12
- package/src/components/IframePreview/IframePreview.vue +211 -0
- package/src/components/IframePreview/iframeContent.ts +230 -0
- package/src/components/IframePreview/iframePreviewApp.ts +308 -0
- package/src/components/IframePreview/messageHandler.ts +219 -0
- package/src/components/IframePreview/types.ts +126 -0
- package/src/components/PageBlockList/PageBlockList.vue +8 -6
- package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +5 -3
- package/src/components/PageRenderer/PageRenderer.vue +18 -38
- package/src/components/PageSettings/PageSettings.vue +10 -6
- package/src/components/ResizeHandle/ResizeHandle.vue +68 -10
- package/src/components/{WswgJsonEditor/WswgJsonEditor.test.ts → WswgPageBuilder/WswgPageBuilder.test.ts} +8 -8
- package/src/components/WswgPageBuilder/WswgPageBuilder.vue +375 -0
- package/src/index.ts +10 -2
- package/src/shims.d.ts +4 -0
- package/src/types/Theme.d.ts +15 -0
- package/src/util/registry.ts +2 -2
- package/src/util/theme-registry.ts +397 -0
- package/src/util/validation.ts +102 -11
- package/src/vite-plugin.ts +8 -4
- package/types/vue-wswg-editor.d.ts +4 -0
- package/dist/types/components/PageRenderer/blockModules.d.ts +0 -3
- package/dist/types/components/PageRenderer/layoutModules.d.ts +0 -3
- package/src/components/PageRenderer/blockModules-alternative.ts.example +0 -9
- package/src/components/PageRenderer/blockModules-manual.ts.example +0 -19
- package/src/components/PageRenderer/blockModules-runtime.ts.example +0 -23
- package/src/components/PageRenderer/blockModules.ts +0 -32
- package/src/components/PageRenderer/layoutModules.ts +0 -32
- package/src/components/WswgJsonEditor/WswgJsonEditor.vue +0 -595
- /package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.test.d.ts → WswgPageBuilder/WswgPageBuilder.test.d.ts} +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Iframe Preview Vue Application
|
|
3
|
+
*
|
|
4
|
+
* This file creates a Vue application that runs inside the iframe preview.
|
|
5
|
+
* Since the library is consumed as source code, consuming apps' Vite builds
|
|
6
|
+
* will process this file, giving it access to virtual modules (blocks/layouts).
|
|
7
|
+
*
|
|
8
|
+
* Usage in consuming apps:
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createIframeApp, getIframeAppModuleUrl } from 'vue-wswg-editor/IframePreviewApp'
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createApp, ref, watch, h, type App } from "vue";
|
|
15
|
+
import EditorPageRenderer from "../EditorPageRenderer/EditorPageRenderer.vue";
|
|
16
|
+
import type { Block } from "../../types/Block";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the URL of this module
|
|
20
|
+
* This can be used to import the module in the iframe HTML
|
|
21
|
+
*/
|
|
22
|
+
export function getIframeAppModuleUrl(): string {
|
|
23
|
+
// Use import.meta.url to get the current module's URL
|
|
24
|
+
// In development, this will be the source file URL
|
|
25
|
+
// In production (after Vite build), this will be the bundled module URL
|
|
26
|
+
return import.meta.url;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IframeAppState {
|
|
30
|
+
pageData: Record<string, any> | null;
|
|
31
|
+
activeBlock: Block | null;
|
|
32
|
+
hoveredBlockId: string | null;
|
|
33
|
+
blocksKey: string;
|
|
34
|
+
settingsKey: string;
|
|
35
|
+
settingsOpen: boolean;
|
|
36
|
+
theme: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IframeAppCallbacks {
|
|
40
|
+
onBlockClick?: (blockId: string, block: Block | null) => void;
|
|
41
|
+
onBlockHover?: (blockId: string | null) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create and mount the Vue app for the iframe preview
|
|
46
|
+
*/
|
|
47
|
+
export async function createIframeApp(container: HTMLElement): Promise<App> {
|
|
48
|
+
// State
|
|
49
|
+
const pageData = ref<Record<string, any> | null>(null);
|
|
50
|
+
const activeBlock = ref<Block | null>(null);
|
|
51
|
+
const hoveredBlockId = ref<string | null>(null);
|
|
52
|
+
const settingsOpen = ref<boolean>(false);
|
|
53
|
+
const blocksKey = ref<string>("blocks");
|
|
54
|
+
const settingsKey = ref<string>("settings");
|
|
55
|
+
const theme = ref<string>("default");
|
|
56
|
+
// Serialize data for postMessage (handles Vue reactive proxies)
|
|
57
|
+
function serializeForPostMessage(data: any): any {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(JSON.stringify(data));
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Message handler
|
|
66
|
+
function sendToParent(message: any) {
|
|
67
|
+
if (window.parent) {
|
|
68
|
+
// Serialize message to handle Vue reactive proxies
|
|
69
|
+
const serializedMessage = serializeForPostMessage(message);
|
|
70
|
+
if (!serializedMessage) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
window.parent.postMessage(serializedMessage, "*");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Handle messages from parent
|
|
78
|
+
window.addEventListener("message", (event: MessageEvent) => {
|
|
79
|
+
const msg = event.data;
|
|
80
|
+
if (!msg || !msg.type) return;
|
|
81
|
+
|
|
82
|
+
switch (msg.type) {
|
|
83
|
+
case "UPDATE_PAGE_DATA":
|
|
84
|
+
if (msg.pageData) {
|
|
85
|
+
pageData.value = msg.pageData;
|
|
86
|
+
}
|
|
87
|
+
if (msg.blocksKey) {
|
|
88
|
+
blocksKey.value = msg.blocksKey;
|
|
89
|
+
}
|
|
90
|
+
if (msg.settingsKey) {
|
|
91
|
+
settingsKey.value = msg.settingsKey;
|
|
92
|
+
}
|
|
93
|
+
if (msg.theme) {
|
|
94
|
+
theme.value = msg.theme;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case "SET_ACTIVE_BLOCK":
|
|
98
|
+
activeBlock.value = msg.block;
|
|
99
|
+
break;
|
|
100
|
+
case "SET_HOVERED_BLOCK":
|
|
101
|
+
hoveredBlockId.value = msg.blockId;
|
|
102
|
+
break;
|
|
103
|
+
case "SET_SETTINGS_OPEN":
|
|
104
|
+
settingsOpen.value = msg.settingsOpen;
|
|
105
|
+
break;
|
|
106
|
+
case "SCROLL_TO_BLOCK": {
|
|
107
|
+
const block = document.querySelector(`[data-block-id="${msg.blockId}"]`);
|
|
108
|
+
if (block) {
|
|
109
|
+
block.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Create Vue app using render function (since we're using runtime-only Vue)
|
|
117
|
+
const app = createApp({
|
|
118
|
+
components: {
|
|
119
|
+
EditorPageRenderer,
|
|
120
|
+
},
|
|
121
|
+
setup() {
|
|
122
|
+
// Ensure PageRenderer has time to load blocks before rendering
|
|
123
|
+
const isPageReady = ref(false);
|
|
124
|
+
|
|
125
|
+
// Watch for pageData changes
|
|
126
|
+
watch(
|
|
127
|
+
() => pageData.value,
|
|
128
|
+
async (newPageData) => {
|
|
129
|
+
if (newPageData && newPageData[blocksKey.value]) {
|
|
130
|
+
// Give PageRenderer time to load block modules
|
|
131
|
+
// PageRenderer loads blocks in onBeforeMount, so we need to wait
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
133
|
+
isPageReady.value = true;
|
|
134
|
+
} else {
|
|
135
|
+
isPageReady.value = false;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{ immediate: true }
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Render function
|
|
142
|
+
return () => {
|
|
143
|
+
const currentPageData = pageData.value;
|
|
144
|
+
const hasPageData = currentPageData && currentPageData[blocksKey.value];
|
|
145
|
+
const blocks = hasPageData && currentPageData ? currentPageData[blocksKey.value] : [];
|
|
146
|
+
|
|
147
|
+
if (isPageReady.value && currentPageData) {
|
|
148
|
+
return h(EditorPageRenderer, {
|
|
149
|
+
blocks: blocks,
|
|
150
|
+
layout: currentPageData[settingsKey.value]?.layout,
|
|
151
|
+
settings: currentPageData[settingsKey.value],
|
|
152
|
+
activeBlock: activeBlock.value,
|
|
153
|
+
hoveredBlockId: hoveredBlockId.value,
|
|
154
|
+
settingsOpen: settingsOpen.value,
|
|
155
|
+
editable: true,
|
|
156
|
+
theme: theme.value,
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
// Show loading state while PageRenderer loads blocks
|
|
160
|
+
return h("div", { class: "bg-white px-5 py-12 md:py-20" }, [
|
|
161
|
+
h("div", { class: "mx-auto max-w-md pb-7 text-center" }, [
|
|
162
|
+
h(
|
|
163
|
+
"svg",
|
|
164
|
+
{
|
|
165
|
+
class: "mx-auto size-8 animate-spin text-blue-600",
|
|
166
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
167
|
+
fill: "none",
|
|
168
|
+
viewBox: "0 0 24 24",
|
|
169
|
+
},
|
|
170
|
+
[
|
|
171
|
+
h("circle", {
|
|
172
|
+
class: "opacity-25",
|
|
173
|
+
cx: "12",
|
|
174
|
+
cy: "12",
|
|
175
|
+
r: "10",
|
|
176
|
+
stroke: "currentColor",
|
|
177
|
+
"stroke-width": "4",
|
|
178
|
+
}),
|
|
179
|
+
h("path", {
|
|
180
|
+
class: "opacity-75",
|
|
181
|
+
fill: "currentColor",
|
|
182
|
+
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z",
|
|
183
|
+
}),
|
|
184
|
+
]
|
|
185
|
+
),
|
|
186
|
+
h("span", { class: "mt-4 text-gray-700" }, "Loading preview..."),
|
|
187
|
+
]),
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Dynamic import helper - using Function constructor to create a truly dynamic import
|
|
195
|
+
// that Vite cannot analyze statically, preventing build-time resolution errors
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
197
|
+
// @ts-ignore - modules may not be available in all consuming apps
|
|
198
|
+
const dynamicImport = new Function("modulePath", "return import(modulePath)");
|
|
199
|
+
|
|
200
|
+
// Try to install Unhead plugin if available
|
|
201
|
+
// This is needed for layouts that use useHead from @vueuse/head
|
|
202
|
+
try {
|
|
203
|
+
// Dynamic import to check if @vueuse/head is available
|
|
204
|
+
// This module is externalized in vite.config.ts so it won't be bundled
|
|
205
|
+
const headModule = await dynamicImport("@vueuse/head");
|
|
206
|
+
if (headModule && typeof headModule.createHead === "function") {
|
|
207
|
+
const head = headModule.createHead();
|
|
208
|
+
app.use(head);
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// @vueuse/head not available - layouts using useHead will show warnings but won't break
|
|
212
|
+
// This is expected if the consuming app doesn't use @vueuse/head
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Try to use vue-router - first try from CDN, but components use consuming app's vue-router
|
|
216
|
+
// The issue is that components from the consuming app use their own vue-router instance
|
|
217
|
+
// with different Symbol keys. We need to use the same instance they're using.
|
|
218
|
+
// Try to import vue-router dynamically - this will use the consuming app's vue-router
|
|
219
|
+
// if it's available in the module resolution context
|
|
220
|
+
let VueRouter: any = null;
|
|
221
|
+
let routerInstalled = false;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
// Try dynamic import - this should resolve to the consuming app's vue-router
|
|
225
|
+
// when the library is consumed as source code
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
227
|
+
// @ts-ignore - vue-router may not be available in all consuming apps
|
|
228
|
+
const routerModule = await import("vue-router");
|
|
229
|
+
VueRouter = routerModule;
|
|
230
|
+
} catch {
|
|
231
|
+
// Fallback to CDN version
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
233
|
+
// @ts-ignore
|
|
234
|
+
VueRouter = typeof window !== "undefined" ? (window as any).VueRouter : null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (VueRouter && typeof VueRouter.createRouter === "function") {
|
|
238
|
+
try {
|
|
239
|
+
// Create a minimal router instance
|
|
240
|
+
const router = VueRouter.createRouter({
|
|
241
|
+
history: VueRouter.createMemoryHistory(),
|
|
242
|
+
routes: [
|
|
243
|
+
{
|
|
244
|
+
path: "/",
|
|
245
|
+
component: { template: "<div></div>" },
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
path: "/:pathMatch(.*)*",
|
|
249
|
+
component: { template: "<div></div>" },
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Store original resolve to call it first, then ensure matched array exists
|
|
255
|
+
const originalResolve = router.resolve.bind(router);
|
|
256
|
+
router.resolve = (to: any) => {
|
|
257
|
+
// Call original resolve to get proper route structure
|
|
258
|
+
const resolved = originalResolve(to);
|
|
259
|
+
// Ensure the resolved route has a matched array (useLink requires this)
|
|
260
|
+
if (resolved.route && (!resolved.route.matched || resolved.route.matched.length === 0)) {
|
|
261
|
+
resolved.route.matched = [
|
|
262
|
+
{
|
|
263
|
+
path: resolved.route.path || "/",
|
|
264
|
+
name: resolved.route.name,
|
|
265
|
+
meta: resolved.route.meta || {},
|
|
266
|
+
components: {},
|
|
267
|
+
children: [],
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
}
|
|
271
|
+
return resolved;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Install router BEFORE mounting - this provides the router injection
|
|
275
|
+
// The app.use() call should handle the Symbol keys automatically
|
|
276
|
+
app.use(router);
|
|
277
|
+
|
|
278
|
+
// Wait for router to be ready before mounting
|
|
279
|
+
await router.isReady();
|
|
280
|
+
|
|
281
|
+
// Ensure currentRoute has proper structure with matched array
|
|
282
|
+
// currentRoute.value is readonly, so we navigate to ensure route is properly resolved
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
284
|
+
// @ts-ignore
|
|
285
|
+
const currentRoute = router.currentRoute.value;
|
|
286
|
+
if (currentRoute && (!currentRoute.matched || currentRoute.matched.length === 0)) {
|
|
287
|
+
// Navigate to ensure route has proper matched array
|
|
288
|
+
await router.push("/");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
routerInstalled = true;
|
|
292
|
+
} catch {
|
|
293
|
+
// Fall through to mount without router
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!routerInstalled) {
|
|
298
|
+
// Router was not installed - RouterLink components will fail
|
|
299
|
+
console.warn("[iframe preview] Router was not installed - RouterLink components will fail");
|
|
300
|
+
}
|
|
301
|
+
// Mount the app
|
|
302
|
+
app.mount(container);
|
|
303
|
+
|
|
304
|
+
// Notify parent that iframe is ready
|
|
305
|
+
sendToParent({ type: "IFRAME_READY" });
|
|
306
|
+
|
|
307
|
+
return app;
|
|
308
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IframeMessage,
|
|
3
|
+
ParentMessage,
|
|
4
|
+
UpdatePageDataMessage,
|
|
5
|
+
UpdateHTMLMessage,
|
|
6
|
+
SetActiveBlockMessage,
|
|
7
|
+
SetHoveredBlockMessage,
|
|
8
|
+
SetSettingsOpenMessage,
|
|
9
|
+
SetViewportMessage,
|
|
10
|
+
ScrollToBlockMessage,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import type { Block } from "../../types/Block";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Serialize data for postMessage (handles Vue reactive proxies)
|
|
16
|
+
*/
|
|
17
|
+
function serializeForPostMessage(data: any): any {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(JSON.stringify(data));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn("Failed to serialize data for postMessage:", error);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Send a message to the iframe
|
|
28
|
+
*/
|
|
29
|
+
export function sendToIframe(iframe: HTMLIFrameElement | null, message: IframeMessage): void {
|
|
30
|
+
if (!iframe || !iframe.contentWindow) {
|
|
31
|
+
console.warn("Cannot send message: iframe not ready");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Serialize message to handle Vue reactive proxies
|
|
36
|
+
const serializedMessage = serializeForPostMessage(message) as IframeMessage;
|
|
37
|
+
if (!serializedMessage) {
|
|
38
|
+
console.error("Failed to serialize message for postMessage");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Security: Only send to same origin
|
|
43
|
+
try {
|
|
44
|
+
iframe.contentWindow.postMessage(serializedMessage, "*");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("Error sending message to iframe:", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send page data update to iframe
|
|
52
|
+
*/
|
|
53
|
+
export function sendPageDataUpdate(
|
|
54
|
+
iframe: HTMLIFrameElement | null,
|
|
55
|
+
pageData: Record<string, any>,
|
|
56
|
+
blocksKey: string,
|
|
57
|
+
settingsKey: string,
|
|
58
|
+
theme: string
|
|
59
|
+
): void {
|
|
60
|
+
// Serialize pageData to avoid Vue reactive proxy issues
|
|
61
|
+
const serializedPageData = serializeForPostMessage(pageData);
|
|
62
|
+
const message: UpdatePageDataMessage = {
|
|
63
|
+
type: "UPDATE_PAGE_DATA",
|
|
64
|
+
pageData: serializedPageData,
|
|
65
|
+
blocksKey,
|
|
66
|
+
settingsKey,
|
|
67
|
+
theme,
|
|
68
|
+
};
|
|
69
|
+
sendToIframe(iframe, message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Send rendered HTML to iframe
|
|
74
|
+
*/
|
|
75
|
+
export function sendHTMLUpdate(
|
|
76
|
+
iframe: HTMLIFrameElement | null,
|
|
77
|
+
html: string,
|
|
78
|
+
pageData?: Record<string, any>,
|
|
79
|
+
blocksKey?: string,
|
|
80
|
+
settingsKey?: string
|
|
81
|
+
): void {
|
|
82
|
+
// Serialize pageData to avoid Vue reactive proxy issues
|
|
83
|
+
const serializedPageData = pageData ? serializeForPostMessage(pageData) : undefined;
|
|
84
|
+
const message: UpdateHTMLMessage = {
|
|
85
|
+
type: "UPDATE_HTML",
|
|
86
|
+
html,
|
|
87
|
+
pageData: serializedPageData,
|
|
88
|
+
blocksKey,
|
|
89
|
+
settingsKey,
|
|
90
|
+
};
|
|
91
|
+
sendToIframe(iframe, message);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Send active block update to iframe
|
|
96
|
+
*/
|
|
97
|
+
export function sendActiveBlock(iframe: HTMLIFrameElement | null, block: Block | null): void {
|
|
98
|
+
const message: SetActiveBlockMessage = {
|
|
99
|
+
type: "SET_ACTIVE_BLOCK",
|
|
100
|
+
block,
|
|
101
|
+
};
|
|
102
|
+
sendToIframe(iframe, message);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Send hovered block update to iframe
|
|
107
|
+
*/
|
|
108
|
+
export function sendHoveredBlock(iframe: HTMLIFrameElement | null, blockId: string | null): void {
|
|
109
|
+
const message: SetHoveredBlockMessage = {
|
|
110
|
+
type: "SET_HOVERED_BLOCK",
|
|
111
|
+
blockId,
|
|
112
|
+
};
|
|
113
|
+
sendToIframe(iframe, message);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send settings open state update to iframe
|
|
118
|
+
*/
|
|
119
|
+
export function sendSettingsOpen(iframe: HTMLIFrameElement | null, settingsOpen: boolean): void {
|
|
120
|
+
const message: SetSettingsOpenMessage = {
|
|
121
|
+
type: "SET_SETTINGS_OPEN",
|
|
122
|
+
settingsOpen,
|
|
123
|
+
};
|
|
124
|
+
sendToIframe(iframe, message);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Send viewport update to iframe
|
|
129
|
+
*/
|
|
130
|
+
export function sendViewport(iframe: HTMLIFrameElement | null, viewport: "desktop" | "mobile"): void {
|
|
131
|
+
const message: SetViewportMessage = {
|
|
132
|
+
type: "SET_VIEWPORT",
|
|
133
|
+
viewport,
|
|
134
|
+
};
|
|
135
|
+
sendToIframe(iframe, message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Send scroll to block command to iframe
|
|
140
|
+
*/
|
|
141
|
+
export function sendScrollToBlock(iframe: HTMLIFrameElement | null, blockId: string): void {
|
|
142
|
+
const message: ScrollToBlockMessage = {
|
|
143
|
+
type: "SCROLL_TO_BLOCK",
|
|
144
|
+
blockId,
|
|
145
|
+
};
|
|
146
|
+
sendToIframe(iframe, message);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate incoming message from iframe
|
|
151
|
+
*/
|
|
152
|
+
export function isValidParentMessage(message: any): message is ParentMessage {
|
|
153
|
+
if (!message || typeof message !== "object" || !message.type) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const validTypes: string[] = [
|
|
158
|
+
"BLOCK_CLICK",
|
|
159
|
+
"BLOCK_HOVER",
|
|
160
|
+
"BLOCK_REORDER",
|
|
161
|
+
"BLOCK_ADD",
|
|
162
|
+
"IFRAME_READY",
|
|
163
|
+
"BLOCK_ELEMENT_POSITION",
|
|
164
|
+
"CLICK_PARTIAL",
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
return validTypes.includes(message.type);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle incoming message from iframe
|
|
172
|
+
*/
|
|
173
|
+
export function handleIframeMessage(
|
|
174
|
+
event: MessageEvent,
|
|
175
|
+
callbacks: {
|
|
176
|
+
onBlockClick?: (blockId: string, block: any) => void;
|
|
177
|
+
onBlockHover?: (blockId: string | null) => void;
|
|
178
|
+
onBlockReorder?: (oldIndex: number, newIndex: number) => void;
|
|
179
|
+
onBlockAdd?: (blockType: string, index: number) => void;
|
|
180
|
+
onPartialClick?: (partialValue: string) => void;
|
|
181
|
+
onIframeReady?: () => void;
|
|
182
|
+
onBlockPosition?: (
|
|
183
|
+
blockId: string,
|
|
184
|
+
position: { top: number; left: number; width: number; height: number }
|
|
185
|
+
) => void;
|
|
186
|
+
}
|
|
187
|
+
): void {
|
|
188
|
+
// Security: Validate origin (in production, check against expected origin)
|
|
189
|
+
// For now, we'll accept messages from any origin since we're using blob URLs
|
|
190
|
+
if (!isValidParentMessage(event.data)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const message = event.data as ParentMessage;
|
|
195
|
+
|
|
196
|
+
switch (message.type) {
|
|
197
|
+
case "BLOCK_CLICK":
|
|
198
|
+
callbacks.onBlockClick?.(message.blockId, message.block);
|
|
199
|
+
break;
|
|
200
|
+
case "BLOCK_HOVER":
|
|
201
|
+
callbacks.onBlockHover?.(message.blockId);
|
|
202
|
+
break;
|
|
203
|
+
case "BLOCK_REORDER":
|
|
204
|
+
callbacks.onBlockReorder?.(message.oldIndex, message.newIndex);
|
|
205
|
+
break;
|
|
206
|
+
case "BLOCK_ADD":
|
|
207
|
+
callbacks.onBlockAdd?.(message.blockType, message.index);
|
|
208
|
+
break;
|
|
209
|
+
case "CLICK_PARTIAL":
|
|
210
|
+
callbacks.onPartialClick?.(message.partial);
|
|
211
|
+
break;
|
|
212
|
+
case "IFRAME_READY":
|
|
213
|
+
callbacks.onIframeReady?.();
|
|
214
|
+
break;
|
|
215
|
+
case "BLOCK_ELEMENT_POSITION":
|
|
216
|
+
callbacks.onBlockPosition?.(message.blockId, message.position);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Message types for parent ↔ iframe communication
|
|
2
|
+
|
|
3
|
+
import type { Block } from "../../types/Block";
|
|
4
|
+
|
|
5
|
+
export type MessageType =
|
|
6
|
+
| "UPDATE_PAGE_DATA"
|
|
7
|
+
| "UPDATE_HTML"
|
|
8
|
+
| "SET_ACTIVE_BLOCK"
|
|
9
|
+
| "SET_HOVERED_BLOCK"
|
|
10
|
+
| "SET_SETTINGS_OPEN"
|
|
11
|
+
| "SET_VIEWPORT"
|
|
12
|
+
| "CLICK_PARTIAL"
|
|
13
|
+
| "SCROLL_TO_BLOCK"
|
|
14
|
+
| "BLOCK_CLICK"
|
|
15
|
+
| "BLOCK_HOVER"
|
|
16
|
+
| "BLOCK_REORDER"
|
|
17
|
+
| "BLOCK_ADD"
|
|
18
|
+
| "IFRAME_READY"
|
|
19
|
+
| "BLOCK_ELEMENT_POSITION";
|
|
20
|
+
|
|
21
|
+
export interface BaseMessage {
|
|
22
|
+
type: MessageType;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UpdatePageDataMessage extends BaseMessage {
|
|
26
|
+
type: "UPDATE_PAGE_DATA";
|
|
27
|
+
pageData: Record<string, any>;
|
|
28
|
+
blocksKey: string;
|
|
29
|
+
settingsKey: string;
|
|
30
|
+
theme: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UpdateHTMLMessage extends BaseMessage {
|
|
34
|
+
type: "UPDATE_HTML";
|
|
35
|
+
html: string;
|
|
36
|
+
pageData?: Record<string, any>;
|
|
37
|
+
blocksKey?: string;
|
|
38
|
+
settingsKey?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SetActiveBlockMessage extends BaseMessage {
|
|
42
|
+
type: "SET_ACTIVE_BLOCK";
|
|
43
|
+
block: Block | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SetHoveredBlockMessage extends BaseMessage {
|
|
47
|
+
type: "SET_HOVERED_BLOCK";
|
|
48
|
+
blockId: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SetSettingsOpenMessage extends BaseMessage {
|
|
52
|
+
type: "SET_SETTINGS_OPEN";
|
|
53
|
+
settingsOpen: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SetViewportMessage extends BaseMessage {
|
|
57
|
+
type: "SET_VIEWPORT";
|
|
58
|
+
viewport: "desktop" | "mobile";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PartialClickMessage extends BaseMessage {
|
|
62
|
+
type: "CLICK_PARTIAL";
|
|
63
|
+
partial: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ScrollToBlockMessage extends BaseMessage {
|
|
67
|
+
type: "SCROLL_TO_BLOCK";
|
|
68
|
+
blockId: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BlockClickMessage extends BaseMessage {
|
|
72
|
+
type: "BLOCK_CLICK";
|
|
73
|
+
blockId: string;
|
|
74
|
+
block: any;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface BlockHoverMessage extends BaseMessage {
|
|
78
|
+
type: "BLOCK_HOVER";
|
|
79
|
+
blockId: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BlockReorderMessage extends BaseMessage {
|
|
83
|
+
type: "BLOCK_REORDER";
|
|
84
|
+
oldIndex: number;
|
|
85
|
+
newIndex: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface BlockAddMessage extends BaseMessage {
|
|
89
|
+
type: "BLOCK_ADD";
|
|
90
|
+
blockType: string;
|
|
91
|
+
index: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface IframeReadyMessage extends BaseMessage {
|
|
95
|
+
type: "IFRAME_READY";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface BlockElementPositionMessage extends BaseMessage {
|
|
99
|
+
type: "BLOCK_ELEMENT_POSITION";
|
|
100
|
+
blockId: string;
|
|
101
|
+
position: {
|
|
102
|
+
top: number;
|
|
103
|
+
left: number;
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type IframeMessage =
|
|
110
|
+
| UpdatePageDataMessage
|
|
111
|
+
| UpdateHTMLMessage
|
|
112
|
+
| SetActiveBlockMessage
|
|
113
|
+
| SetHoveredBlockMessage
|
|
114
|
+
| SetSettingsOpenMessage
|
|
115
|
+
| SetViewportMessage
|
|
116
|
+
| ScrollToBlockMessage
|
|
117
|
+
| PartialClickMessage;
|
|
118
|
+
|
|
119
|
+
export type ParentMessage =
|
|
120
|
+
| BlockClickMessage
|
|
121
|
+
| BlockHoverMessage
|
|
122
|
+
| BlockReorderMessage
|
|
123
|
+
| BlockAddMessage
|
|
124
|
+
| IframeReadyMessage
|
|
125
|
+
| BlockElementPositionMessage
|
|
126
|
+
| PartialClickMessage;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<div
|
|
5
5
|
v-for="block in pageBlocks"
|
|
6
6
|
:key="block.id"
|
|
7
|
+
data-prevent-drop="true"
|
|
7
8
|
:class="{ 'bg-blue-100 text-blue-600': hoveredBlockId === block.id }"
|
|
8
9
|
class="block-item -mx-2.5 flex cursor-pointer items-center gap-1 rounded-md p-2.5 text-sm text-neutral-900"
|
|
9
10
|
@mouseenter="setHoveredBlockId(block.id)"
|
|
@@ -24,9 +25,9 @@
|
|
|
24
25
|
|
|
25
26
|
<script setup lang="ts">
|
|
26
27
|
import { computed, defineEmits, onMounted, ref } from "vue";
|
|
27
|
-
import {
|
|
28
|
+
import { getBlock } from "../../util/theme-registry";
|
|
28
29
|
import type { Block } from "../../types/Block";
|
|
29
|
-
import {
|
|
30
|
+
import { toNiceName } from "../../util/helpers";
|
|
30
31
|
import Sortable from "sortablejs";
|
|
31
32
|
|
|
32
33
|
const emit = defineEmits<{
|
|
@@ -45,9 +46,12 @@ const pageBlocks = computed(() => {
|
|
|
45
46
|
if (!pageData.value?.[props.blocksKey]) return [];
|
|
46
47
|
// loop through pageData[blocksKey] and get the block data from registry or return a default block data
|
|
47
48
|
return pageData.value[props.blocksKey].map((block: any) => {
|
|
48
|
-
|
|
49
|
+
// Find the corresponding block in the registry
|
|
50
|
+
const registryBlock = getBlock(block.type);
|
|
51
|
+
|
|
52
|
+
if (registryBlock) {
|
|
49
53
|
return {
|
|
50
|
-
...
|
|
54
|
+
...registryBlock,
|
|
51
55
|
...block,
|
|
52
56
|
};
|
|
53
57
|
}
|
|
@@ -56,8 +60,6 @@ const pageBlocks = computed(() => {
|
|
|
56
60
|
id: block.id,
|
|
57
61
|
name: block.type,
|
|
58
62
|
label: toNiceName(block.type),
|
|
59
|
-
icon: "question-mark",
|
|
60
|
-
thumbnail: "https://via.placeholder.com/150",
|
|
61
63
|
};
|
|
62
64
|
});
|
|
63
65
|
});
|