vue-wswg-editor 0.0.12 → 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/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 +2723 -1897
- 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 +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,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
|
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
v-model="pageData"
|
|
15
15
|
:editable="editable"
|
|
16
16
|
:settingsKey="settingsKey"
|
|
17
|
+
:activeTab="activeSettingsTab"
|
|
17
18
|
@close="showPageSettings = false"
|
|
18
19
|
/>
|
|
19
20
|
<!-- Active section-->
|
|
@@ -90,9 +91,8 @@
|
|
|
90
91
|
|
|
91
92
|
<script setup lang="ts">
|
|
92
93
|
import { computed } from "vue";
|
|
93
|
-
import {
|
|
94
|
+
import { getBlock } from "../../util/theme-registry";
|
|
94
95
|
import type { Block } from "../../types/Block";
|
|
95
|
-
import { toCamelCase } from "../../util/helpers";
|
|
96
96
|
import BlockBrowser from "../BlockBrowser/BlockBrowser.vue";
|
|
97
97
|
import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
|
|
98
98
|
import PageBlockList from "../PageBlockList/PageBlockList.vue";
|
|
@@ -114,12 +114,14 @@ const props = withDefaults(
|
|
|
114
114
|
blocksKey?: string;
|
|
115
115
|
settingsKey?: string;
|
|
116
116
|
hasPageSettings?: boolean;
|
|
117
|
+
activeSettingsTab?: string;
|
|
117
118
|
}>(),
|
|
118
119
|
{
|
|
119
120
|
editable: true,
|
|
120
121
|
blocksKey: "blocks",
|
|
121
122
|
settingsKey: "settings",
|
|
122
123
|
hasPageSettings: false,
|
|
124
|
+
activeSettingsTab: undefined,
|
|
123
125
|
}
|
|
124
126
|
);
|
|
125
127
|
|
|
@@ -136,7 +138,7 @@ const computedActiveBlock = computed(() => {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// Find the corresponding block in the registry
|
|
139
|
-
const registryBlock =
|
|
141
|
+
const registryBlock = getBlock(blockType);
|
|
140
142
|
|
|
141
143
|
if (!registryBlock) {
|
|
142
144
|
// If no registry block found, return activeBlock as-is
|
|
@@ -24,18 +24,15 @@
|
|
|
24
24
|
>
|
|
25
25
|
<component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
|
|
26
26
|
</div>
|
|
27
|
-
<pre>{{ blocks }}</pre>
|
|
28
27
|
</div>
|
|
29
28
|
</template>
|
|
30
29
|
</div>
|
|
31
30
|
</template>
|
|
32
31
|
|
|
33
32
|
<script setup lang="ts">
|
|
34
|
-
import {
|
|
35
|
-
import { generateNameVariations } from "../../util/helpers";
|
|
36
|
-
import { getBlockModule, loadBlockModules } from "./blockModules";
|
|
37
|
-
import { loadLayoutModules, getLayoutModule } from "./layoutModules";
|
|
33
|
+
import { computed, withDefaults, onBeforeMount, ref } from "vue";
|
|
38
34
|
import type { Block } from "../../types/Block";
|
|
35
|
+
import { initialiseRegistry, getBlock, getLayout } from "../../util/theme-registry";
|
|
39
36
|
|
|
40
37
|
const props = withDefaults(
|
|
41
38
|
defineProps<{
|
|
@@ -43,42 +40,18 @@ const props = withDefaults(
|
|
|
43
40
|
layout?: string;
|
|
44
41
|
settings?: Record<string, any>;
|
|
45
42
|
withLayout?: boolean;
|
|
43
|
+
theme?: string;
|
|
46
44
|
}>(),
|
|
47
45
|
{
|
|
48
46
|
layout: "default",
|
|
49
47
|
settings: () => ({}),
|
|
50
48
|
withLayout: false,
|
|
49
|
+
theme: "default",
|
|
51
50
|
}
|
|
52
51
|
);
|
|
53
52
|
|
|
54
53
|
const isReady = ref(false);
|
|
55
54
|
|
|
56
|
-
function getBlock(blockType: string): Component | undefined {
|
|
57
|
-
// Generate name variations and try to find a match
|
|
58
|
-
const nameVariations = generateNameVariations(blockType);
|
|
59
|
-
|
|
60
|
-
// Try each variation to find the block component
|
|
61
|
-
for (const variation of nameVariations) {
|
|
62
|
-
const module = getBlockModule(variation);
|
|
63
|
-
if (module) return module;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function getLayout(layoutName: string): Component | undefined {
|
|
70
|
-
// Generate name variations and try to find a match in layoutModules keys (file paths)
|
|
71
|
-
const nameVariations = generateNameVariations(layoutName);
|
|
72
|
-
|
|
73
|
-
// Try each variation to find the block component
|
|
74
|
-
for (const variation of nameVariations) {
|
|
75
|
-
const module = getLayoutModule(variation);
|
|
76
|
-
if (module) return module;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
55
|
// Get the layout component based on the layout prop
|
|
83
56
|
const layoutComponent = computed(() => {
|
|
84
57
|
return getLayout(props.layout);
|
|
@@ -102,8 +75,11 @@ function getMarginClass(block: Block): string {
|
|
|
102
75
|
|
|
103
76
|
onBeforeMount(async () => {
|
|
104
77
|
isReady.value = false;
|
|
105
|
-
|
|
106
|
-
|
|
78
|
+
|
|
79
|
+
// Initialise registries for editor preview
|
|
80
|
+
// Exclude the editing registry to load only the theme and blocks
|
|
81
|
+
await initialiseRegistry(props.theme, false);
|
|
82
|
+
|
|
107
83
|
isReady.value = true;
|
|
108
84
|
});
|
|
109
85
|
</script>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<h4 class="mt-1 text-lg font-bold">Page settings</h4>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
|
-
<div class="border-b border-gray-300 p-5">
|
|
14
|
+
<div v-if="availableLayouts.length > 1" class="border-b border-gray-300 p-5">
|
|
15
15
|
<!-- Page layout -->
|
|
16
16
|
<div class="editor-field-node">
|
|
17
17
|
<!-- Label -->
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
:fields="pageSettingsFields"
|
|
38
38
|
:editable="true"
|
|
39
39
|
:isLayoutBlock="true"
|
|
40
|
+
:activeTab="activeTab"
|
|
40
41
|
/>
|
|
41
42
|
</div>
|
|
42
43
|
</div>
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
|
|
45
46
|
<script setup lang="ts">
|
|
46
47
|
import { computed, onBeforeMount, onMounted, ref } from "vue";
|
|
47
|
-
import {
|
|
48
|
+
import { themeLayouts } from "../../util/theme-registry";
|
|
48
49
|
import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
|
|
49
50
|
|
|
50
51
|
const emit = defineEmits<{
|
|
@@ -57,23 +58,26 @@ const pageSettingsFields = ref<any>({});
|
|
|
57
58
|
const props = withDefaults(
|
|
58
59
|
defineProps<{
|
|
59
60
|
settingsKey?: string;
|
|
61
|
+
activeTab?: string;
|
|
60
62
|
}>(),
|
|
61
63
|
{
|
|
62
64
|
settingsKey: "settings",
|
|
65
|
+
activeTab: undefined,
|
|
63
66
|
}
|
|
64
67
|
);
|
|
65
68
|
const availableLayouts = computed(() => {
|
|
66
|
-
return
|
|
69
|
+
return Object.values(themeLayouts.value) || [];
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
function getLayoutSettings() {
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const activeLayout = pageData.value[props.settingsKey].layout;
|
|
74
|
+
if (!activeLayout) return;
|
|
75
|
+
pageSettingsFields.value = themeLayouts.value[activeLayout]?.fields || null;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
onBeforeMount(() => {
|
|
75
79
|
if (!pageData.value[props.settingsKey]) {
|
|
76
|
-
pageData.value.
|
|
80
|
+
pageData.value[props.settingsKey] = {};
|
|
77
81
|
}
|
|
78
82
|
});
|
|
79
83
|
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
<div class="resize-handle-wrapper group">
|
|
3
|
+
<div
|
|
4
|
+
id="page-builder-resize-handle"
|
|
5
|
+
ref="resizeHandle"
|
|
6
|
+
class="resize-handle shrink-0 cursor-col-resize transition-colors duration-200"
|
|
7
|
+
@mousedown="startResize"
|
|
8
|
+
></div>
|
|
9
|
+
<span
|
|
10
|
+
class="viewport-size absolute right-1 top-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
|
11
|
+
>
|
|
12
|
+
<div class="rounded-sm bg-yellow-400 px-1.5 py-1 font-mono text-xs font-medium text-gray-900">
|
|
13
|
+
{{ computedViewportSize }}
|
|
14
|
+
</div>
|
|
15
|
+
</span>
|
|
16
|
+
</div>
|
|
8
17
|
</template>
|
|
9
18
|
|
|
10
19
|
<script setup lang="ts">
|
|
11
|
-
import { ref } from "vue";
|
|
12
|
-
import { defineEmits } from "vue";
|
|
20
|
+
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
|
13
21
|
|
|
14
22
|
const emit = defineEmits<{
|
|
15
23
|
(e: "sidebarWidth", width: number): void;
|
|
@@ -18,6 +26,9 @@ const emit = defineEmits<{
|
|
|
18
26
|
const resizeHandle = ref<HTMLElement | null>(null);
|
|
19
27
|
const sidebarWidth = ref(380); // Default sidebar width (380px)
|
|
20
28
|
const isResizing = ref(false);
|
|
29
|
+
const viewportWidth = ref<number>(0);
|
|
30
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
31
|
+
let updateViewportWidthFn: (() => void) | null = null;
|
|
21
32
|
|
|
22
33
|
// Resize functionality
|
|
23
34
|
function startResize(event: MouseEvent) {
|
|
@@ -50,15 +61,62 @@ function startResize(event: MouseEvent) {
|
|
|
50
61
|
document.body.style.cursor = "col-resize";
|
|
51
62
|
document.body.style.userSelect = "none";
|
|
52
63
|
}
|
|
64
|
+
|
|
65
|
+
// Calculate viewport width based on #page-viewport element
|
|
66
|
+
const computedViewportSize = computed(() => {
|
|
67
|
+
return viewportWidth.value > 0 ? `${viewportWidth.value}px` : "0px";
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Setup ResizeObserver to watch #page-viewport element
|
|
71
|
+
onMounted(() => {
|
|
72
|
+
updateViewportWidthFn = () => {
|
|
73
|
+
const viewportElement = document.getElementById("page-preview-container");
|
|
74
|
+
if (viewportElement) {
|
|
75
|
+
viewportWidth.value = viewportElement.offsetWidth;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Initial measurement
|
|
80
|
+
updateViewportWidthFn();
|
|
81
|
+
|
|
82
|
+
// Use ResizeObserver to watch for size changes
|
|
83
|
+
const viewportElement = document.getElementById("page-preview-container");
|
|
84
|
+
if (viewportElement) {
|
|
85
|
+
resizeObserver = new ResizeObserver(() => {
|
|
86
|
+
if (updateViewportWidthFn) {
|
|
87
|
+
updateViewportWidthFn();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
resizeObserver.observe(viewportElement);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Also listen for window resize events as a fallback
|
|
94
|
+
window.addEventListener("resize", updateViewportWidthFn);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
onBeforeUnmount(() => {
|
|
98
|
+
if (resizeObserver) {
|
|
99
|
+
resizeObserver.disconnect();
|
|
100
|
+
resizeObserver = null;
|
|
101
|
+
}
|
|
102
|
+
if (updateViewportWidthFn) {
|
|
103
|
+
window.removeEventListener("resize", updateViewportWidthFn);
|
|
104
|
+
updateViewportWidthFn = null;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
53
107
|
</script>
|
|
54
108
|
|
|
55
109
|
<style scoped lang="scss">
|
|
56
110
|
/* Resize handle styles */
|
|
57
|
-
.resize-handle {
|
|
111
|
+
.resize-handle-wrapper {
|
|
58
112
|
position: sticky;
|
|
59
113
|
top: 0;
|
|
60
114
|
right: 0;
|
|
61
|
-
z-index:
|
|
115
|
+
z-index: 30;
|
|
116
|
+
height: var(--editor-height);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.resize-handle {
|
|
62
120
|
width: 3px;
|
|
63
121
|
height: var(--editor-height);
|
|
64
122
|
cursor: col-resize;
|