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.
Files changed (60) hide show
  1. package/README.md +23 -8
  2. package/dist/style.css +1 -1
  3. package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +1 -0
  4. package/dist/types/components/EditorPageRenderer/EditorPageRenderer.vue.d.ts +21 -0
  5. package/dist/types/components/EmptyState/EmptyState.vue.d.ts +2 -8
  6. package/dist/types/components/IframePreview/IframePreview.vue.d.ts +26 -0
  7. package/dist/types/components/IframePreview/iframeContent.d.ts +9 -0
  8. package/dist/types/components/IframePreview/iframePreviewApp.d.ts +36 -0
  9. package/dist/types/components/IframePreview/messageHandler.d.ts +55 -0
  10. package/dist/types/components/IframePreview/types.d.ts +77 -0
  11. package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +2 -0
  12. package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +2 -0
  13. package/dist/types/components/PageSettings/PageSettings.vue.d.ts +2 -0
  14. package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.vue.d.ts → WswgPageBuilder/WswgPageBuilder.vue.d.ts} +2 -0
  15. package/dist/types/index.d.ts +8 -2
  16. package/dist/types/util/registry.d.ts +2 -0
  17. package/dist/types/util/theme-registry.d.ts +42 -0
  18. package/dist/types/util/validation.d.ts +2 -2
  19. package/dist/vite-plugin.js +33 -29
  20. package/dist/vue-wswg-editor.es.js +2783 -1905
  21. package/package.json +1 -2
  22. package/src/assets/styles/_mixins.scss +15 -0
  23. package/src/components/AddBlockItem/AddBlockItem.vue +13 -4
  24. package/src/components/BlockBrowser/BlockBrowser.vue +5 -5
  25. package/src/components/BlockComponent/BlockComponent.vue +23 -50
  26. package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +12 -10
  27. package/src/components/BlockEditorFields/BlockEditorFields.vue +24 -4
  28. package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +9 -4
  29. package/src/components/BrowserNavigation/BrowserNavigation.vue +1 -1
  30. package/src/components/EditorPageRenderer/EditorPageRenderer.vue +641 -0
  31. package/src/components/EmptyState/EmptyState.vue +3 -12
  32. package/src/components/IframePreview/IframePreview.vue +211 -0
  33. package/src/components/IframePreview/iframeContent.ts +230 -0
  34. package/src/components/IframePreview/iframePreviewApp.ts +308 -0
  35. package/src/components/IframePreview/messageHandler.ts +219 -0
  36. package/src/components/IframePreview/types.ts +126 -0
  37. package/src/components/PageBlockList/PageBlockList.vue +8 -6
  38. package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +5 -3
  39. package/src/components/PageRenderer/PageRenderer.vue +18 -38
  40. package/src/components/PageSettings/PageSettings.vue +10 -6
  41. package/src/components/ResizeHandle/ResizeHandle.vue +68 -10
  42. package/src/components/{WswgJsonEditor/WswgJsonEditor.test.ts → WswgPageBuilder/WswgPageBuilder.test.ts} +8 -8
  43. package/src/components/WswgPageBuilder/WswgPageBuilder.vue +375 -0
  44. package/src/index.ts +10 -2
  45. package/src/shims.d.ts +4 -0
  46. package/src/types/Theme.d.ts +15 -0
  47. package/src/util/registry.ts +2 -2
  48. package/src/util/theme-registry.ts +397 -0
  49. package/src/util/validation.ts +102 -11
  50. package/src/vite-plugin.ts +8 -4
  51. package/types/vue-wswg-editor.d.ts +4 -0
  52. package/dist/types/components/PageRenderer/blockModules.d.ts +0 -3
  53. package/dist/types/components/PageRenderer/layoutModules.d.ts +0 -3
  54. package/src/components/PageRenderer/blockModules-alternative.ts.example +0 -9
  55. package/src/components/PageRenderer/blockModules-manual.ts.example +0 -19
  56. package/src/components/PageRenderer/blockModules-runtime.ts.example +0 -23
  57. package/src/components/PageRenderer/blockModules.ts +0 -32
  58. package/src/components/PageRenderer/layoutModules.ts +0 -32
  59. package/src/components/WswgJsonEditor/WswgJsonEditor.vue +0 -595
  60. /package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.test.d.ts → WswgPageBuilder/WswgPageBuilder.test.d.ts} +0 -0
@@ -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,230 @@
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
+ const vueRouterCdnUrl = "https://unpkg.com/vue-router@4/dist/vue-router.esm-browser.js";
110
+ // Get parent origin to use as base URL for relative asset paths
111
+ const parentOrigin = typeof window !== "undefined" ? window.location.origin : "";
112
+
113
+ // Define Vue feature flags before Vue is loaded
114
+ // These must be defined as global constants before any Vue code runs
115
+ const vueFeatureFlagsScript = `<script>
116
+ // Define Vue feature flags to prevent warnings
117
+ // These must be defined before Vue or the library code is imported
118
+ // Using var to make them available globally (not just on window)
119
+ var __VUE_OPTIONS_API__ = true;
120
+ var __VUE_PROD_DEVTOOLS__ = false;
121
+ var __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false;
122
+
123
+ // Also set on window for compatibility
124
+ window.__VUE_OPTIONS_API__ = true;
125
+ window.__VUE_PROD_DEVTOOLS__ = false;
126
+ window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false;
127
+ </script>`;
128
+
129
+ // Generate script that loads Vue, vue-router, and initializes the iframe app
130
+ // The import map above allows vue-router to resolve "vue" as a bare module specifier
131
+ const appScript = iframeAppModuleUrl
132
+ ? `<script type="module">
133
+ import { createApp } from 'vue';
134
+ import * as VueRouter from 'vue-router';
135
+ // Make vue-router available globally so it can be accessed in the iframe app
136
+ window.VueRouter = VueRouter;
137
+
138
+ import { createIframeApp } from '${iframeAppModuleUrl}';
139
+
140
+ const appEl = document.getElementById('app');
141
+ if (appEl) {
142
+ createIframeApp(appEl).catch(error => {
143
+ console.error('Failed to create iframe app:', error);
144
+ });
145
+ }
146
+ </script>`
147
+ : `<script type="module">
148
+ // Fallback: Wait for parent to send module URL via postMessage
149
+ import { createApp } from 'vue';
150
+ import * as VueRouter from 'vue-router';
151
+ // Make vue-router available globally so it can be imported in the iframe app
152
+ window.VueRouter = VueRouter;
153
+
154
+ let appInitialized = false;
155
+
156
+ window.addEventListener('message', async (event) => {
157
+ if (event.data.type === 'INIT_IFRAME_APP' && event.data.moduleUrl && !appInitialized) {
158
+ appInitialized = true;
159
+ try {
160
+ const { createIframeApp } = await import(event.data.moduleUrl);
161
+ const appEl = document.getElementById('app');
162
+ if (appEl) {
163
+ createIframeApp(appEl);
164
+ }
165
+ } catch (error) {
166
+ console.error('Failed to load iframe app module:', error);
167
+ // Fallback: create minimal Vue app
168
+ const appEl = document.getElementById('app');
169
+ if (appEl) {
170
+ createApp({
171
+ template: '<div class="p-4">Loading preview...</div>'
172
+ }).mount(appEl);
173
+ }
174
+ }
175
+ }
176
+ });
177
+
178
+ // Notify parent that we're ready to receive module URL
179
+ if (window.parent) {
180
+ window.parent.postMessage({ type: 'IFRAME_READY_FOR_APP' }, '*');
181
+ }
182
+ </script>`;
183
+
184
+ return `<!DOCTYPE html>
185
+ <html lang="en">
186
+ <head>
187
+ <meta charset="UTF-8">
188
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
189
+ <title>Page Preview</title>
190
+ ${parentOrigin ? `<base href="${parentOrigin}/">` : ""}
191
+ <!-- Import map to allow vue-router to resolve "vue" -->
192
+ <!-- Stub for @vue/devtools-api to avoid loading devtools in iframe -->
193
+ <script type="importmap">
194
+ {
195
+ "imports": {
196
+ "vue": "${vueCdnUrl}",
197
+ "vue-router": "${vueRouterCdnUrl}",
198
+ "@vue/devtools-api": "data:text/javascript,export const setupDevtoolsPlugin=()=>{};export const on=()=>{};export const off=()=>{};export const once=()=>{};export const emit=()=>{};export const notifyComponentUpdate=()=>{};export const addTimelineLayer=()=>{};export const addCustomCommand=()=>{};export const addCustomTab=()=>{};"
199
+ }
200
+ }
201
+ </script>
202
+ ${parentStylesheets}
203
+ ${parentCSSVariables}
204
+ <style>
205
+ /* Additional iframe-specific styles */
206
+ html, body {
207
+ margin: 0;
208
+ padding: 0;
209
+ width: 100%;
210
+ height: 100%;
211
+ overflow-x: hidden;
212
+ }
213
+
214
+ #app {
215
+ width: 100%;
216
+ min-height: 100vh;
217
+ }
218
+
219
+ .page-renderer-wrapper {
220
+ width: 100%;
221
+ }
222
+ </style>
223
+ </head>
224
+ <body>
225
+ <div id="app"></div>
226
+ ${vueFeatureFlagsScript}
227
+ ${appScript}
228
+ </body>
229
+ </html>`;
230
+ }