vue-wswg-editor 0.0.11 → 0.0.13
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/AddBlockItem/AddBlockItem.vue.d.ts +6 -0
- package/dist/types/components/BlockBrowser/BlockBrowser.vue.d.ts +2 -0
- package/dist/types/components/BlockComponent/BlockComponent.vue.d.ts +15 -0
- package/dist/types/components/BlockEditorFieldNode/BlockEditorFieldNode.vue.d.ts +15 -0
- package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +17 -0
- package/dist/types/components/BlockImageFieldNode/BlockImageNode.vue.d.ts +19 -0
- package/dist/types/components/BlockMarginFieldNode/BlockMarginNode.vue.d.ts +23 -0
- package/dist/types/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue.d.ts +15 -0
- package/dist/types/components/BrowserNavigation/BrowserNavigation.vue.d.ts +5 -0
- package/dist/types/components/EditorPageRenderer/EditorPageRenderer.vue.d.ts +21 -0
- package/dist/types/components/EmptyState/EmptyState.vue.d.ts +9 -0
- 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/PageBlockList/PageBlockList.vue.d.ts +19 -0
- package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +37 -0
- package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +28 -0
- package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +15 -0
- package/dist/types/components/PageSettings/PageSettings.vue.d.ts +19 -0
- package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +6 -0
- package/dist/types/components/WswgPageBuilder/WswgPageBuilder.test.d.ts +1 -0
- package/dist/types/components/WswgPageBuilder/WswgPageBuilder.vue.d.ts +38 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/util/fieldConfig.d.ts +87 -0
- package/dist/types/util/helpers.d.ts +28 -0
- package/dist/types/util/registry.d.ts +27 -0
- package/dist/types/util/theme-registry.d.ts +42 -0
- package/dist/types/util/validation.d.ts +26 -0
- package/dist/types/vite-plugin.d.ts +9 -0
- package/dist/vite-plugin.js +80 -0
- package/dist/vue-wswg-editor.es.js +2854 -2006
- 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 +210 -0
- package/src/components/IframePreview/iframePreviewApp.ts +221 -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 +9 -33
- 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 +104 -13
- package/src/vite-plugin.ts +8 -4
- package/types/vue-wswg-editor.d.ts +4 -0
- 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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="containerRef" class="iframe-preview-container">
|
|
3
|
+
<iframe ref="iframeRef" title="Page preview" :src="iframeSrc" class="iframe-preview" frameborder="0"></iframe>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from "vue";
|
|
9
|
+
import { generateIframeHTML } from "./iframeContent";
|
|
10
|
+
import {
|
|
11
|
+
sendPageDataUpdate,
|
|
12
|
+
sendActiveBlock,
|
|
13
|
+
sendHoveredBlock,
|
|
14
|
+
sendSettingsOpen,
|
|
15
|
+
sendScrollToBlock,
|
|
16
|
+
handleIframeMessage,
|
|
17
|
+
} from "./messageHandler";
|
|
18
|
+
import type { Block } from "../../types/Block";
|
|
19
|
+
import { getIframeAppModuleUrl } from "./iframePreviewApp";
|
|
20
|
+
|
|
21
|
+
// Get the iframe app module URL
|
|
22
|
+
// This will be processed by the consuming app's Vite build
|
|
23
|
+
const iframeAppModuleUrl = getIframeAppModuleUrl();
|
|
24
|
+
|
|
25
|
+
const props = defineProps<{
|
|
26
|
+
pageData?: Record<string, any>;
|
|
27
|
+
activeBlock: Block | null;
|
|
28
|
+
hoveredBlockId: string | null;
|
|
29
|
+
viewport: "desktop" | "mobile";
|
|
30
|
+
editable?: boolean;
|
|
31
|
+
blocksKey?: string;
|
|
32
|
+
settingsKey?: string;
|
|
33
|
+
settingsOpen?: boolean;
|
|
34
|
+
theme?: string;
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
const emit = defineEmits<{
|
|
38
|
+
(e: "click-block", block: Block | null): void;
|
|
39
|
+
(e: "hover-block", blockId: string | null): void;
|
|
40
|
+
(e: "block-reorder", oldIndex: number, newIndex: number): void;
|
|
41
|
+
(e: "block-add", blockType: string, index: number): void;
|
|
42
|
+
(e: "click-partial", partialValue: string): void;
|
|
43
|
+
}>();
|
|
44
|
+
|
|
45
|
+
const iframeRef = ref<HTMLIFrameElement | null>(null);
|
|
46
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
47
|
+
const iframeReady = ref(false);
|
|
48
|
+
const iframeSrc = ref<string>("");
|
|
49
|
+
|
|
50
|
+
const blocksKey = computed(() => props.blocksKey || "blocks");
|
|
51
|
+
const settingsKey = computed(() => props.settingsKey || "settings");
|
|
52
|
+
|
|
53
|
+
// Generate blob URL for iframe
|
|
54
|
+
function createIframeSrc(): string {
|
|
55
|
+
const html = generateIframeHTML(iframeAppModuleUrl);
|
|
56
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
57
|
+
return URL.createObjectURL(blob);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Update iframe content - send pageData to Vue app in iframe
|
|
61
|
+
async function updateIframeContent() {
|
|
62
|
+
if (!iframeRef.value || !iframeReady.value) return;
|
|
63
|
+
if (!props.pageData || !props.pageData[blocksKey.value]) return;
|
|
64
|
+
|
|
65
|
+
await nextTick();
|
|
66
|
+
sendPageDataUpdate(iframeRef.value, props.pageData, blocksKey.value, settingsKey.value, props.theme || "default");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Setup message listener
|
|
70
|
+
let messageListener: ((event: MessageEvent) => void) | null = null;
|
|
71
|
+
|
|
72
|
+
function setupMessageListener() {
|
|
73
|
+
messageListener = (event: MessageEvent) => {
|
|
74
|
+
// Only handle messages from our iframe
|
|
75
|
+
if (event.source !== iframeRef.value?.contentWindow) return;
|
|
76
|
+
|
|
77
|
+
handleIframeMessage(event, {
|
|
78
|
+
onBlockClick: (_blockId: string, block: any) => {
|
|
79
|
+
emit("click-block", block);
|
|
80
|
+
},
|
|
81
|
+
onBlockHover: (blockId: string | null) => {
|
|
82
|
+
emit("hover-block", blockId);
|
|
83
|
+
},
|
|
84
|
+
onBlockReorder: (oldIndex: number, newIndex: number) => {
|
|
85
|
+
emit("block-reorder", oldIndex, newIndex);
|
|
86
|
+
},
|
|
87
|
+
onBlockAdd: (blockType: string, index: number) => {
|
|
88
|
+
emit("block-add", blockType, index);
|
|
89
|
+
},
|
|
90
|
+
onPartialClick: (partialValue: string) => {
|
|
91
|
+
emit("click-partial", partialValue);
|
|
92
|
+
},
|
|
93
|
+
onIframeReady: () => {
|
|
94
|
+
iframeReady.value = true;
|
|
95
|
+
// Wait a bit for iframe Vue app to be fully ready
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
updateIframeContent();
|
|
98
|
+
// Send initial settingsOpen state
|
|
99
|
+
if (iframeRef.value && props.settingsOpen !== undefined) {
|
|
100
|
+
sendSettingsOpen(iframeRef.value, props.settingsOpen);
|
|
101
|
+
}
|
|
102
|
+
}, 100);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
window.addEventListener("message", messageListener);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cleanupMessageListener() {
|
|
111
|
+
if (messageListener) {
|
|
112
|
+
window.removeEventListener("message", messageListener);
|
|
113
|
+
messageListener = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Watch for pageData changes
|
|
118
|
+
watch(
|
|
119
|
+
[() => props.pageData, () => props.theme],
|
|
120
|
+
async () => {
|
|
121
|
+
await nextTick();
|
|
122
|
+
updateIframeContent();
|
|
123
|
+
},
|
|
124
|
+
{ deep: true }
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Watch for activeBlock changes
|
|
128
|
+
watch(
|
|
129
|
+
() => props.activeBlock,
|
|
130
|
+
async (block) => {
|
|
131
|
+
if (iframeRef.value && iframeReady.value) {
|
|
132
|
+
sendActiveBlock(iframeRef.value, block || null);
|
|
133
|
+
// Also scroll to block
|
|
134
|
+
if (block?.id) {
|
|
135
|
+
await nextTick();
|
|
136
|
+
sendScrollToBlock(iframeRef.value, block.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Watch for hoveredBlockId changes
|
|
143
|
+
watch(
|
|
144
|
+
() => props.hoveredBlockId,
|
|
145
|
+
(blockId) => {
|
|
146
|
+
if (iframeRef.value && iframeReady.value) {
|
|
147
|
+
sendHoveredBlock(iframeRef.value, blockId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Watch for settingsOpen changes
|
|
153
|
+
watch(
|
|
154
|
+
() => props.settingsOpen,
|
|
155
|
+
(settingsOpen) => {
|
|
156
|
+
if (iframeRef.value && iframeReady.value) {
|
|
157
|
+
sendSettingsOpen(iframeRef.value, settingsOpen ?? false);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
onMounted(() => {
|
|
163
|
+
iframeSrc.value = createIframeSrc();
|
|
164
|
+
setupMessageListener();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
onBeforeUnmount(() => {
|
|
168
|
+
cleanupMessageListener();
|
|
169
|
+
// Cleanup blob URL
|
|
170
|
+
if (iframeSrc.value.startsWith("blob:")) {
|
|
171
|
+
URL.revokeObjectURL(iframeSrc.value);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<style scoped lang="scss">
|
|
177
|
+
.iframe-preview-container {
|
|
178
|
+
// position: relative;
|
|
179
|
+
// width: 100%;
|
|
180
|
+
// overflow: hidden;
|
|
181
|
+
// background-color: #ededed;
|
|
182
|
+
height: -webkit-fill-available;
|
|
183
|
+
|
|
184
|
+
&.mobile-viewport {
|
|
185
|
+
.iframe-preview {
|
|
186
|
+
max-width: 384px;
|
|
187
|
+
margin: 0 auto;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.iframe-preview {
|
|
193
|
+
display: block;
|
|
194
|
+
width: 100%;
|
|
195
|
+
height: 100%;
|
|
196
|
+
background: white;
|
|
197
|
+
border: none;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.hidden-renderer {
|
|
201
|
+
position: absolute;
|
|
202
|
+
top: -9999px;
|
|
203
|
+
left: -9999px;
|
|
204
|
+
visibility: hidden;
|
|
205
|
+
width: 1px;
|
|
206
|
+
height: 1px;
|
|
207
|
+
overflow: hidden;
|
|
208
|
+
pointer-events: none;
|
|
209
|
+
opacity: 0;
|
|
210
|
+
}
|
|
211
|
+
</style>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract CSS variables from the parent document
|
|
3
|
+
* Extracts variables from :root and specific selectors like .wswg-page-builder
|
|
4
|
+
*/
|
|
5
|
+
function extractParentCSSVariables(): string {
|
|
6
|
+
if (typeof document === "undefined" || typeof window === "undefined") return "";
|
|
7
|
+
|
|
8
|
+
const variables: string[] = [];
|
|
9
|
+
|
|
10
|
+
// Get all CSS variables from :root
|
|
11
|
+
const rootVariables = new Set<string>();
|
|
12
|
+
const allStyles = Array.from(document.styleSheets);
|
|
13
|
+
|
|
14
|
+
// Extract from stylesheets
|
|
15
|
+
allStyles.forEach((sheet) => {
|
|
16
|
+
try {
|
|
17
|
+
const rules = Array.from(sheet.cssRules || sheet.rules || []);
|
|
18
|
+
rules.forEach((rule) => {
|
|
19
|
+
if (rule instanceof CSSStyleRule) {
|
|
20
|
+
const { selectorText, style } = rule;
|
|
21
|
+
// Check if this rule targets :root or html
|
|
22
|
+
if (selectorText === ":root" || selectorText === "html" || selectorText === "html:root") {
|
|
23
|
+
for (let i = 0; i < style.length; i++) {
|
|
24
|
+
const property = style[i];
|
|
25
|
+
if (property.startsWith("--")) {
|
|
26
|
+
const value = style.getPropertyValue(property);
|
|
27
|
+
rootVariables.add(`${property}: ${value};`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
// Cross-origin stylesheets will throw errors, ignore them
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Also extract from inline styles in <style> tags
|
|
39
|
+
const styleTags = document.querySelectorAll("head style");
|
|
40
|
+
styleTags.forEach((style) => {
|
|
41
|
+
const content = style.textContent || style.innerHTML;
|
|
42
|
+
if (content) {
|
|
43
|
+
// Match CSS variable declarations
|
|
44
|
+
const varRegex = /--[\w-]+\s*:\s*[^;]+;/g;
|
|
45
|
+
const matches = content.match(varRegex);
|
|
46
|
+
if (matches) {
|
|
47
|
+
matches.forEach((match) => {
|
|
48
|
+
rootVariables.add(match.trim());
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (rootVariables.size > 0) {
|
|
55
|
+
variables.push(`:root {`);
|
|
56
|
+
rootVariables.forEach((variable) => {
|
|
57
|
+
variables.push(` ${variable}`);
|
|
58
|
+
});
|
|
59
|
+
variables.push(`}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return variables.length > 0 ? `<style>${variables.join("\n")}</style>` : "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract stylesheets from the parent document
|
|
67
|
+
* This includes both <link rel="stylesheet"> tags and <style> tags
|
|
68
|
+
*/
|
|
69
|
+
function extractParentStylesheets(): string {
|
|
70
|
+
if (typeof document === "undefined") return "";
|
|
71
|
+
|
|
72
|
+
const stylesheets: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Extract <link rel="stylesheet"> tags
|
|
75
|
+
const linkTags = document.querySelectorAll('head link[rel="stylesheet"]');
|
|
76
|
+
linkTags.forEach((link) => {
|
|
77
|
+
const href = link.getAttribute("href");
|
|
78
|
+
if (href) {
|
|
79
|
+
// Convert relative URLs to absolute URLs
|
|
80
|
+
const absoluteHref = new URL(href, window.location.href).href;
|
|
81
|
+
stylesheets.push(`<link rel="stylesheet" href="${absoluteHref}">`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Extract <style> tags
|
|
86
|
+
const styleTags = document.querySelectorAll("head style");
|
|
87
|
+
styleTags.forEach((style) => {
|
|
88
|
+
const content = style.textContent || style.innerHTML;
|
|
89
|
+
if (content) {
|
|
90
|
+
stylesheets.push(`<style>${content}</style>`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return stylesheets.join("\n ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate HTML content for the iframe
|
|
99
|
+
* This creates a standalone HTML page with a Vue app that will be loaded in the iframe
|
|
100
|
+
* The Vue app uses PageRenderer to render blocks reactively
|
|
101
|
+
*
|
|
102
|
+
* @param iframeAppModuleUrl - URL to the iframe app module bundle
|
|
103
|
+
* This should be obtained using import.meta.url from iframePreviewApp.ts
|
|
104
|
+
*/
|
|
105
|
+
export function generateIframeHTML(iframeAppModuleUrl?: string): string {
|
|
106
|
+
const parentStylesheets = extractParentStylesheets();
|
|
107
|
+
const parentCSSVariables = extractParentCSSVariables();
|
|
108
|
+
const vueCdnUrl = "https://unpkg.com/vue@3/dist/vue.esm-browser.js";
|
|
109
|
+
// Get parent origin to use as base URL for relative asset paths
|
|
110
|
+
const parentOrigin = typeof window !== "undefined" ? window.location.origin : "";
|
|
111
|
+
|
|
112
|
+
// Define Vue feature flags before Vue is loaded
|
|
113
|
+
// These must be defined as global constants before any Vue code runs
|
|
114
|
+
const vueFeatureFlagsScript = `<script>
|
|
115
|
+
// Define Vue feature flags to prevent warnings
|
|
116
|
+
// These must be defined before Vue or the library code is imported
|
|
117
|
+
// Using var to make them available globally (not just on window)
|
|
118
|
+
var __VUE_OPTIONS_API__ = true;
|
|
119
|
+
var __VUE_PROD_DEVTOOLS__ = false;
|
|
120
|
+
var __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false;
|
|
121
|
+
|
|
122
|
+
// Also set on window for compatibility
|
|
123
|
+
window.__VUE_OPTIONS_API__ = true;
|
|
124
|
+
window.__VUE_PROD_DEVTOOLS__ = false;
|
|
125
|
+
window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false;
|
|
126
|
+
</script>`;
|
|
127
|
+
|
|
128
|
+
// Generate script that loads Vue and initializes the iframe app
|
|
129
|
+
const appScript = iframeAppModuleUrl
|
|
130
|
+
? `<script type="module">
|
|
131
|
+
import { createApp } from '${vueCdnUrl}';
|
|
132
|
+
import { createIframeApp } from '${iframeAppModuleUrl}';
|
|
133
|
+
|
|
134
|
+
const appEl = document.getElementById('app');
|
|
135
|
+
if (appEl) {
|
|
136
|
+
createIframeApp(appEl).catch(error => {
|
|
137
|
+
console.error('Failed to create iframe app:', error);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
</script>`
|
|
141
|
+
: `<script type="module">
|
|
142
|
+
// Fallback: Wait for parent to send module URL via postMessage
|
|
143
|
+
import { createApp } from '${vueCdnUrl}';
|
|
144
|
+
|
|
145
|
+
let appInitialized = false;
|
|
146
|
+
|
|
147
|
+
window.addEventListener('message', async (event) => {
|
|
148
|
+
if (event.data.type === 'INIT_IFRAME_APP' && event.data.moduleUrl && !appInitialized) {
|
|
149
|
+
appInitialized = true;
|
|
150
|
+
try {
|
|
151
|
+
const { createIframeApp } = await import(event.data.moduleUrl);
|
|
152
|
+
const appEl = document.getElementById('app');
|
|
153
|
+
if (appEl) {
|
|
154
|
+
createIframeApp(appEl);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('Failed to load iframe app module:', error);
|
|
158
|
+
// Fallback: create minimal Vue app
|
|
159
|
+
const appEl = document.getElementById('app');
|
|
160
|
+
if (appEl) {
|
|
161
|
+
createApp({
|
|
162
|
+
template: '<div class="p-4">Loading preview...</div>'
|
|
163
|
+
}).mount(appEl);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Notify parent that we're ready to receive module URL
|
|
170
|
+
if (window.parent) {
|
|
171
|
+
window.parent.postMessage({ type: 'IFRAME_READY_FOR_APP' }, '*');
|
|
172
|
+
}
|
|
173
|
+
</script>`;
|
|
174
|
+
|
|
175
|
+
return `<!DOCTYPE html>
|
|
176
|
+
<html lang="en">
|
|
177
|
+
<head>
|
|
178
|
+
<meta charset="UTF-8">
|
|
179
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
180
|
+
<title>Page Preview</title>
|
|
181
|
+
${parentOrigin ? `<base href="${parentOrigin}/">` : ""}
|
|
182
|
+
${parentStylesheets}
|
|
183
|
+
${parentCSSVariables}
|
|
184
|
+
<style>
|
|
185
|
+
/* Additional iframe-specific styles */
|
|
186
|
+
html, body {
|
|
187
|
+
margin: 0;
|
|
188
|
+
padding: 0;
|
|
189
|
+
width: 100%;
|
|
190
|
+
height: 100%;
|
|
191
|
+
overflow-x: hidden;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#app {
|
|
195
|
+
width: 100%;
|
|
196
|
+
min-height: 100vh;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.page-renderer-wrapper {
|
|
200
|
+
width: 100%;
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
203
|
+
</head>
|
|
204
|
+
<body>
|
|
205
|
+
<div id="app"></div>
|
|
206
|
+
${vueFeatureFlagsScript}
|
|
207
|
+
${appScript}
|
|
208
|
+
</body>
|
|
209
|
+
</html>`;
|
|
210
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
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 (error) {
|
|
61
|
+
console.warn("[iframe] Failed to serialize data for postMessage:", error);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Message handler
|
|
67
|
+
function sendToParent(message: any) {
|
|
68
|
+
if (window.parent) {
|
|
69
|
+
// Serialize message to handle Vue reactive proxies
|
|
70
|
+
const serializedMessage = serializeForPostMessage(message);
|
|
71
|
+
if (!serializedMessage) {
|
|
72
|
+
console.error("[iframe] Failed to serialize message for postMessage");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
window.parent.postMessage(serializedMessage, "*");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle messages from parent
|
|
80
|
+
window.addEventListener("message", (event: MessageEvent) => {
|
|
81
|
+
const msg = event.data;
|
|
82
|
+
if (!msg || !msg.type) return;
|
|
83
|
+
|
|
84
|
+
switch (msg.type) {
|
|
85
|
+
case "UPDATE_PAGE_DATA":
|
|
86
|
+
if (msg.pageData) {
|
|
87
|
+
pageData.value = msg.pageData;
|
|
88
|
+
}
|
|
89
|
+
if (msg.blocksKey) {
|
|
90
|
+
blocksKey.value = msg.blocksKey;
|
|
91
|
+
}
|
|
92
|
+
if (msg.settingsKey) {
|
|
93
|
+
settingsKey.value = msg.settingsKey;
|
|
94
|
+
}
|
|
95
|
+
if (msg.theme) {
|
|
96
|
+
theme.value = msg.theme;
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case "SET_ACTIVE_BLOCK":
|
|
100
|
+
activeBlock.value = msg.block;
|
|
101
|
+
break;
|
|
102
|
+
case "SET_HOVERED_BLOCK":
|
|
103
|
+
hoveredBlockId.value = msg.blockId;
|
|
104
|
+
break;
|
|
105
|
+
case "SET_SETTINGS_OPEN":
|
|
106
|
+
settingsOpen.value = msg.settingsOpen;
|
|
107
|
+
break;
|
|
108
|
+
case "SCROLL_TO_BLOCK": {
|
|
109
|
+
const block = document.querySelector(`[data-block-id="${msg.blockId}"]`);
|
|
110
|
+
if (block) {
|
|
111
|
+
block.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Create Vue app using render function (since we're using runtime-only Vue)
|
|
119
|
+
const app = createApp({
|
|
120
|
+
components: {
|
|
121
|
+
EditorPageRenderer,
|
|
122
|
+
},
|
|
123
|
+
setup() {
|
|
124
|
+
// Ensure PageRenderer has time to load blocks before rendering
|
|
125
|
+
const isPageReady = ref(false);
|
|
126
|
+
|
|
127
|
+
// Watch for pageData changes
|
|
128
|
+
watch(
|
|
129
|
+
() => pageData.value,
|
|
130
|
+
async (newPageData) => {
|
|
131
|
+
if (newPageData && newPageData[blocksKey.value]) {
|
|
132
|
+
// Give PageRenderer time to load block modules
|
|
133
|
+
// PageRenderer loads blocks in onBeforeMount, so we need to wait
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
135
|
+
isPageReady.value = true;
|
|
136
|
+
} else {
|
|
137
|
+
isPageReady.value = false;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{ immediate: true }
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Render function
|
|
144
|
+
return () => {
|
|
145
|
+
const currentPageData = pageData.value;
|
|
146
|
+
const hasPageData = currentPageData && currentPageData[blocksKey.value];
|
|
147
|
+
const blocks = hasPageData && currentPageData ? currentPageData[blocksKey.value] : [];
|
|
148
|
+
|
|
149
|
+
if (isPageReady.value && currentPageData) {
|
|
150
|
+
return h(EditorPageRenderer, {
|
|
151
|
+
blocks: blocks,
|
|
152
|
+
layout: currentPageData[settingsKey.value]?.layout,
|
|
153
|
+
settings: currentPageData[settingsKey.value],
|
|
154
|
+
activeBlock: activeBlock.value,
|
|
155
|
+
hoveredBlockId: hoveredBlockId.value,
|
|
156
|
+
settingsOpen: settingsOpen.value,
|
|
157
|
+
editable: true,
|
|
158
|
+
theme: theme.value,
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
// Show loading state while PageRenderer loads blocks
|
|
162
|
+
return h("div", { class: "bg-white px-5 py-12 md:py-20" }, [
|
|
163
|
+
h("div", { class: "mx-auto max-w-md pb-7 text-center" }, [
|
|
164
|
+
h(
|
|
165
|
+
"svg",
|
|
166
|
+
{
|
|
167
|
+
class: "mx-auto size-8 animate-spin text-blue-600",
|
|
168
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
169
|
+
fill: "none",
|
|
170
|
+
viewBox: "0 0 24 24",
|
|
171
|
+
},
|
|
172
|
+
[
|
|
173
|
+
h("circle", {
|
|
174
|
+
class: "opacity-25",
|
|
175
|
+
cx: "12",
|
|
176
|
+
cy: "12",
|
|
177
|
+
r: "10",
|
|
178
|
+
stroke: "currentColor",
|
|
179
|
+
"stroke-width": "4",
|
|
180
|
+
}),
|
|
181
|
+
h("path", {
|
|
182
|
+
class: "opacity-75",
|
|
183
|
+
fill: "currentColor",
|
|
184
|
+
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",
|
|
185
|
+
}),
|
|
186
|
+
]
|
|
187
|
+
),
|
|
188
|
+
h("span", { class: "mt-4 text-gray-700" }, "Loading preview..."),
|
|
189
|
+
]),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Try to install Unhead plugin if available
|
|
197
|
+
// This is needed for layouts that use useHead from @vueuse/head
|
|
198
|
+
try {
|
|
199
|
+
// Dynamic import to check if @vueuse/head is available
|
|
200
|
+
// This module is externalized in vite.config.ts so it won't be bundled
|
|
201
|
+
// Using @vite-ignore to prevent static analysis
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
203
|
+
// @ts-ignore - @vueuse/head may not be available in all consuming apps
|
|
204
|
+
const headModule = await import(/* @vite-ignore */ "@vueuse/head");
|
|
205
|
+
if (headModule && typeof headModule.createHead === "function") {
|
|
206
|
+
const head = headModule.createHead();
|
|
207
|
+
app.use(head);
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
// @vueuse/head not available - layouts using useHead will show warnings but won't break
|
|
211
|
+
// This is expected if the consuming app doesn't use @vueuse/head
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Mount the app
|
|
215
|
+
app.mount(container);
|
|
216
|
+
|
|
217
|
+
// Notify parent that iframe is ready
|
|
218
|
+
sendToParent({ type: "IFRAME_READY" });
|
|
219
|
+
|
|
220
|
+
return app;
|
|
221
|
+
}
|