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
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { mount } from "@vue/test-utils";
|
|
3
|
-
import
|
|
3
|
+
import WswgPageBuilder from "./WswgPageBuilder.vue";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
5
|
+
describe("WswgPageBuilder", () => {
|
|
6
6
|
it("renders the placeholder component", () => {
|
|
7
|
-
const wrapper = mount(
|
|
7
|
+
const wrapper = mount(WswgPageBuilder);
|
|
8
8
|
|
|
9
|
-
expect(wrapper.text()).toContain("
|
|
9
|
+
expect(wrapper.text()).toContain("WswgPageBuilder");
|
|
10
10
|
expect(wrapper.text()).toContain("This is a placeholder component");
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it("renders header slot content when provided", () => {
|
|
14
14
|
const headerContent = "Custom Header Content";
|
|
15
|
-
const wrapper = mount(
|
|
15
|
+
const wrapper = mount(WswgPageBuilder, {
|
|
16
16
|
slots: {
|
|
17
17
|
header: headerContent,
|
|
18
18
|
},
|
|
@@ -22,7 +22,7 @@ describe("WswgJsonEditor", () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("renders header slot with HTML content", () => {
|
|
25
|
-
const wrapper = mount(
|
|
25
|
+
const wrapper = mount(WswgPageBuilder, {
|
|
26
26
|
slots: {
|
|
27
27
|
header: "<div class='custom-header'>Header Title</div>",
|
|
28
28
|
},
|
|
@@ -33,10 +33,10 @@ describe("WswgJsonEditor", () => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it("does not render header slot when not provided", () => {
|
|
36
|
-
const wrapper = mount(
|
|
36
|
+
const wrapper = mount(WswgPageBuilder);
|
|
37
37
|
|
|
38
38
|
// The component should still render without the header slot
|
|
39
|
-
expect(wrapper.text()).toContain("
|
|
39
|
+
expect(wrapper.text()).toContain("WswgPageBuilder");
|
|
40
40
|
// Header slot should be empty/not present
|
|
41
41
|
expect(wrapper.html()).not.toContain("custom-header");
|
|
42
42
|
});
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="wswg-page-builder" :class="{ 'settings-open': showPageSettings }">
|
|
3
|
+
<slot v-if="loading || themeLoading" name="loading">
|
|
4
|
+
<div class="wswg-page-builder-loading flex h-full flex-col items-center justify-center gap-4">
|
|
5
|
+
<svg
|
|
6
|
+
class="mx-auto size-8 animate-spin text-blue-600"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
fill="none"
|
|
9
|
+
viewBox="0 0 24 24"
|
|
10
|
+
>
|
|
11
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
12
|
+
|
|
13
|
+
<path
|
|
14
|
+
class="opacity-75"
|
|
15
|
+
fill="currentColor"
|
|
16
|
+
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"
|
|
17
|
+
></path>
|
|
18
|
+
</svg>
|
|
19
|
+
<span>Loading editor...</span>
|
|
20
|
+
</div>
|
|
21
|
+
</slot>
|
|
22
|
+
<!-- WYSIWYG editor -->
|
|
23
|
+
<div v-else class="wswg-page-builder-body">
|
|
24
|
+
<!-- Page preview -->
|
|
25
|
+
<div class="wswg-page-builder-preview overflow-y-auto">
|
|
26
|
+
<div
|
|
27
|
+
id="page-preview-container"
|
|
28
|
+
class="mx-auto flex flex-col overflow-hidden rounded-lg bg-white transition-all duration-300"
|
|
29
|
+
style="height: -webkit-fill-available"
|
|
30
|
+
:class="{ 'w-full': editorViewport === 'desktop', 'w-96': editorViewport === 'mobile' }"
|
|
31
|
+
>
|
|
32
|
+
<BrowserNavigation v-if="showBrowserBar" class="browser-navigation-bar" :url="url" />
|
|
33
|
+
<IframePreview
|
|
34
|
+
ref="previewRef"
|
|
35
|
+
:pageData="pageData"
|
|
36
|
+
:activeBlock="activeBlock"
|
|
37
|
+
:hoveredBlockId="hoveredBlockId"
|
|
38
|
+
:viewport="editorViewport"
|
|
39
|
+
:editable="editable"
|
|
40
|
+
:blocksKey="blocksKey"
|
|
41
|
+
:settingsKey="settingsKey"
|
|
42
|
+
:settingsOpen="showPageSettings"
|
|
43
|
+
:theme="theme"
|
|
44
|
+
@hover-block="setHoveredBlockId"
|
|
45
|
+
@click-block="handleBlockClick"
|
|
46
|
+
@block-reorder="handleBlockReorder"
|
|
47
|
+
@block-add="handleBlockAdd"
|
|
48
|
+
@click-partial="handleClickPartial"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Resizable divider -->
|
|
54
|
+
<ResizeHandle @sidebar-width="handleSidebarWidth" />
|
|
55
|
+
|
|
56
|
+
<!-- Sidebar -->
|
|
57
|
+
<div
|
|
58
|
+
id="page-builder-sidebar"
|
|
59
|
+
class="page-builder-sidebar-wrapper bg-white"
|
|
60
|
+
:style="{ width: sidebarWidth + 'px' }"
|
|
61
|
+
>
|
|
62
|
+
<PageBuilderSidebar
|
|
63
|
+
v-model="pageData"
|
|
64
|
+
v-model:activeBlock="activeBlock"
|
|
65
|
+
v-model:hoveredBlockId="hoveredBlockId"
|
|
66
|
+
v-model:showPageSettings="showPageSettings"
|
|
67
|
+
v-model:showAddBlockMenu="showAddBlockMenu"
|
|
68
|
+
v-model:editorViewport="editorViewport"
|
|
69
|
+
:hasPageSettings="hasPageSettings"
|
|
70
|
+
:editable="editable"
|
|
71
|
+
:blocksKey="blocksKey"
|
|
72
|
+
:settingsKey="settingsKey"
|
|
73
|
+
:activeSettingsTab="activeSettingsTab"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup lang="ts">
|
|
81
|
+
import { ref, withDefaults, onBeforeMount, computed, watch } from "vue";
|
|
82
|
+
import ResizeHandle from "../ResizeHandle/ResizeHandle.vue";
|
|
83
|
+
import PageBuilderSidebar from "../PageBuilderSidebar/PageBuilderSidebar.vue";
|
|
84
|
+
import BrowserNavigation from "../BrowserNavigation/BrowserNavigation.vue";
|
|
85
|
+
import IframePreview from "../IframePreview/IframePreview.vue";
|
|
86
|
+
import { initialiseRegistry, getBlock, themeLayouts } from "../../util/theme-registry";
|
|
87
|
+
import type { Block } from "../../types/Block";
|
|
88
|
+
import { onKeyStroke, onClickOutside } from "@vueuse/core";
|
|
89
|
+
|
|
90
|
+
const props = withDefaults(
|
|
91
|
+
defineProps<{
|
|
92
|
+
editable?: boolean;
|
|
93
|
+
loading?: boolean;
|
|
94
|
+
url?: string;
|
|
95
|
+
showBrowserBar?: boolean;
|
|
96
|
+
blocksKey?: string;
|
|
97
|
+
settingsKey?: string;
|
|
98
|
+
defaultBlockMargin?: "none" | "small" | "medium" | "large";
|
|
99
|
+
theme?: string;
|
|
100
|
+
}>(),
|
|
101
|
+
{
|
|
102
|
+
editable: false,
|
|
103
|
+
loading: false,
|
|
104
|
+
url: "",
|
|
105
|
+
showBrowserBar: false,
|
|
106
|
+
blocksKey: "blocks",
|
|
107
|
+
settingsKey: "settings",
|
|
108
|
+
defaultBlockMargin: "none",
|
|
109
|
+
theme: "default",
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const editorViewport = ref<"desktop" | "mobile">("desktop");
|
|
114
|
+
const showPageSettings = ref(false);
|
|
115
|
+
const showAddBlockMenu = ref(false);
|
|
116
|
+
const activeBlock = ref<any>(null);
|
|
117
|
+
const isSorting = ref(false);
|
|
118
|
+
const hoveredBlockId = ref<string | null>(null);
|
|
119
|
+
const activeSettingsTab = ref<string | undefined>(undefined);
|
|
120
|
+
const sidebarWidth = ref(380); // Default sidebar width (380px)
|
|
121
|
+
const previewRef = ref<HTMLElement | null>(null);
|
|
122
|
+
const themeLoading = ref(false);
|
|
123
|
+
|
|
124
|
+
// Model value for the JSON page data
|
|
125
|
+
const pageData = defineModel<Record<string, any>>();
|
|
126
|
+
|
|
127
|
+
// Apply the sidebar width from the resize handle
|
|
128
|
+
function handleSidebarWidth(width: number) {
|
|
129
|
+
sidebarWidth.value = width;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check if the page has settings
|
|
133
|
+
const hasPageSettings = computed(() => {
|
|
134
|
+
// Show page settings if there are multiple layouts to choose from
|
|
135
|
+
// Note: This computed might run before registry is initialized, so we check if layouts exist
|
|
136
|
+
if (Object.keys(themeLayouts).length > 1) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If the layout has settings
|
|
141
|
+
if (pageData.value?.[props.settingsKey]) {
|
|
142
|
+
if (Object.keys(pageData.value?.[props.settingsKey]).length > 0) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
function handleBlockClick(block: Block | null) {
|
|
151
|
+
activeBlock.value = block;
|
|
152
|
+
showPageSettings.value = false;
|
|
153
|
+
hoveredBlockId.value = null;
|
|
154
|
+
showAddBlockMenu.value = false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function handleAddBlock(blockType: string, insertIndex?: number) {
|
|
158
|
+
if (!pageData.value) return;
|
|
159
|
+
|
|
160
|
+
// Ensure blocks array exists
|
|
161
|
+
if (!pageData.value[props.blocksKey]) {
|
|
162
|
+
pageData.value[props.blocksKey] = [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Create a new block object
|
|
166
|
+
const newBlock = {
|
|
167
|
+
id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
168
|
+
type: blockType,
|
|
169
|
+
margin: props.defaultBlockMargin
|
|
170
|
+
? { top: props.defaultBlockMargin, bottom: props.defaultBlockMargin }
|
|
171
|
+
: undefined,
|
|
172
|
+
};
|
|
173
|
+
// Get the default prop values from the block component
|
|
174
|
+
const blockComponent = getBlock(blockType);
|
|
175
|
+
if (blockComponent?.props) {
|
|
176
|
+
// loop props and set their default value
|
|
177
|
+
Object.entries(blockComponent.props).forEach(([key, value]: [string, any]) => {
|
|
178
|
+
if (value.default) {
|
|
179
|
+
if (typeof value.default === "function") {
|
|
180
|
+
newBlock[key as keyof typeof newBlock] = value.default();
|
|
181
|
+
} else {
|
|
182
|
+
newBlock[key as keyof typeof newBlock] = value.default;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add the new block at the specified index or at the end
|
|
189
|
+
if (insertIndex !== undefined) {
|
|
190
|
+
pageData.value[props.blocksKey].splice(insertIndex, 0, newBlock);
|
|
191
|
+
} else {
|
|
192
|
+
pageData.value[props.blocksKey].push(newBlock);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Set the new block as active only when adding to the end (from EmptyState)
|
|
196
|
+
activeBlock.value = newBlock;
|
|
197
|
+
showAddBlockMenu.value = false;
|
|
198
|
+
|
|
199
|
+
return newBlock;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setHoveredBlockId(id: string | null) {
|
|
203
|
+
if (isSorting.value) return;
|
|
204
|
+
hoveredBlockId.value = id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// On escape key press
|
|
208
|
+
onKeyStroke("Escape", () => {
|
|
209
|
+
// Unset active block
|
|
210
|
+
activeBlock.value = null;
|
|
211
|
+
hoveredBlockId.value = null;
|
|
212
|
+
showAddBlockMenu.value = false;
|
|
213
|
+
showPageSettings.value = false;
|
|
214
|
+
activeSettingsTab.value = undefined;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Click outside detection
|
|
218
|
+
onClickOutside(
|
|
219
|
+
previewRef,
|
|
220
|
+
() => {
|
|
221
|
+
// Unset active block
|
|
222
|
+
activeBlock.value = null;
|
|
223
|
+
hoveredBlockId.value = null;
|
|
224
|
+
showAddBlockMenu.value = false;
|
|
225
|
+
showPageSettings.value = false;
|
|
226
|
+
activeSettingsTab.value = undefined;
|
|
227
|
+
},
|
|
228
|
+
{ ignore: ["#page-builder-sidebar", "#page-builder-resize-handle"] }
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
onBeforeMount(async () => {
|
|
232
|
+
// Initialise the registry with the active theme
|
|
233
|
+
// Include the editing registry to load additional theme fields and thumbnails
|
|
234
|
+
await initialiseRegistry(props.theme, true);
|
|
235
|
+
|
|
236
|
+
sanitizePageData();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Watch for theme changes to re-initialize the registry
|
|
240
|
+
watch(
|
|
241
|
+
() => props.theme,
|
|
242
|
+
async (newTheme) => {
|
|
243
|
+
themeLoading.value = true;
|
|
244
|
+
await initialiseRegistry(newTheme, true);
|
|
245
|
+
sanitizePageData();
|
|
246
|
+
themeLoading.value = false;
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
function handleBlockReorder(oldIndex: number, newIndex: number) {
|
|
251
|
+
if (!pageData.value?.[props.blocksKey]) return;
|
|
252
|
+
|
|
253
|
+
const blocks = pageData.value[props.blocksKey];
|
|
254
|
+
if (oldIndex < 0 || oldIndex >= blocks.length || newIndex < 0 || newIndex >= blocks.length) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Move block from oldIndex to newIndex
|
|
259
|
+
const movedBlock = blocks[oldIndex];
|
|
260
|
+
blocks.splice(oldIndex, 1);
|
|
261
|
+
blocks.splice(newIndex, 0, movedBlock);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function handleBlockAdd(blockType: string, index: number) {
|
|
265
|
+
// Use the existing handleAddBlock function
|
|
266
|
+
handleAddBlock(blockType, index);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function handleClickPartial(partialValue: string) {
|
|
270
|
+
const hasValue = partialValue !== null && partialValue !== "";
|
|
271
|
+
|
|
272
|
+
// Show the settings sidebar when a partial is clicked
|
|
273
|
+
showPageSettings.value = true;
|
|
274
|
+
// Clear active block when clicking on a partial
|
|
275
|
+
activeBlock.value = null;
|
|
276
|
+
hoveredBlockId.value = null;
|
|
277
|
+
showAddBlockMenu.value = false;
|
|
278
|
+
|
|
279
|
+
// If partial has a value (e.g., "header"), set it as the active tab
|
|
280
|
+
if (hasValue && partialValue) {
|
|
281
|
+
activeSettingsTab.value = partialValue;
|
|
282
|
+
} else {
|
|
283
|
+
activeSettingsTab.value = undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function sanitizePageData() {
|
|
288
|
+
// Initialise the page data if not present
|
|
289
|
+
if (!pageData.value) {
|
|
290
|
+
pageData.value = {};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Initialise settings value if not present
|
|
294
|
+
if (!pageData.value?.[props.settingsKey]) {
|
|
295
|
+
if (pageData.value) {
|
|
296
|
+
pageData.value[props.settingsKey] = {};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Set the default layout if not present
|
|
301
|
+
if (!pageData.value?.[props.settingsKey]?.layout) {
|
|
302
|
+
const settings = pageData.value?.[props.settingsKey];
|
|
303
|
+
if (settings) {
|
|
304
|
+
if (typeof settings.layout !== "string") {
|
|
305
|
+
settings.layout = "default";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Sanitise the layout data (must be a valid layout name string)
|
|
311
|
+
if (pageData.value?.[props.settingsKey]?.layout) {
|
|
312
|
+
const settings = pageData.value?.[props.settingsKey];
|
|
313
|
+
if (settings) {
|
|
314
|
+
if (typeof settings.layout !== "string") {
|
|
315
|
+
settings.layout = "default";
|
|
316
|
+
} else if (!themeLayouts.value[settings.layout]) {
|
|
317
|
+
settings.layout = "default";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Set a default blocks array if not present
|
|
323
|
+
if (!pageData.value?.[props.blocksKey]) {
|
|
324
|
+
if (pageData.value) {
|
|
325
|
+
pageData.value[props.blocksKey] = [];
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
</script>
|
|
330
|
+
|
|
331
|
+
<style lang="scss">
|
|
332
|
+
@use "../../assets/styles/mixins" as *;
|
|
333
|
+
|
|
334
|
+
$editor-background-color: #6a6a6a;
|
|
335
|
+
|
|
336
|
+
.wswg-page-builder {
|
|
337
|
+
position: relative;
|
|
338
|
+
width: 100%;
|
|
339
|
+
max-width: 100vw;
|
|
340
|
+
height: var(--editor-height);
|
|
341
|
+
overflow-y: auto;
|
|
342
|
+
|
|
343
|
+
&-loading {
|
|
344
|
+
display: flex;
|
|
345
|
+
align-items: center;
|
|
346
|
+
justify-content: center;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
&-body {
|
|
350
|
+
display: flex;
|
|
351
|
+
width: 100%;
|
|
352
|
+
background-color: var(--editor-bg-color, $editor-background-color);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
&-preview {
|
|
356
|
+
position: relative;
|
|
357
|
+
display: flex;
|
|
358
|
+
flex: 1;
|
|
359
|
+
flex-grow: 1;
|
|
360
|
+
flex-shrink: 0;
|
|
361
|
+
flex-direction: column;
|
|
362
|
+
height: -webkit-fill-available;
|
|
363
|
+
min-height: 0;
|
|
364
|
+
padding: 1.5rem;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.page-builder-sidebar-wrapper {
|
|
368
|
+
position: sticky;
|
|
369
|
+
top: 0;
|
|
370
|
+
z-index: 12;
|
|
371
|
+
height: var(--editor-height);
|
|
372
|
+
overflow-y: auto;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
// This ensures createField is available immediately without waiting for CSS or component imports
|
|
3
3
|
export { createField } from "./util/fieldConfig";
|
|
4
4
|
export type { EditorFieldConfig, ValidatorFunction } from "./util/fieldConfig";
|
|
5
|
-
export { getLayouts, initialiseRegistry } from "./util/registry";
|
|
5
|
+
export { getLayouts, initialiseRegistry, initialiseLayoutRegistry, initialiseBlockRegistry } from "./util/registry";
|
|
6
6
|
export { validateField, validateAllFields, type ValidationResult } from "./util/validation";
|
|
7
7
|
|
|
8
|
+
export { getActiveTheme, setActiveTheme, getThemeThumbnail, getThemes } from "./util/theme-registry";
|
|
9
|
+
export type { Theme } from "./types/Theme";
|
|
10
|
+
export type { Layout } from "./types/Layout";
|
|
11
|
+
export type { Block } from "./types/Block";
|
|
12
|
+
|
|
8
13
|
// Export components (component exports don't cause side effects until used)
|
|
9
|
-
export { default as
|
|
14
|
+
export { default as WswgPageBuilder } from "./components/WswgPageBuilder/WswgPageBuilder.vue";
|
|
10
15
|
// Export PageRenderer separately - it doesn't use the registry, so it won't trigger field loading
|
|
11
16
|
export { default as PageRenderer } from "./components/PageRenderer/PageRenderer.vue";
|
|
17
|
+
// Export iframe preview app for Vue app in iframe
|
|
18
|
+
export { createIframeApp, getIframeAppModuleUrl } from "./components/IframePreview/iframePreviewApp";
|
|
19
|
+
export type { IframeAppState, IframeAppCallbacks } from "./components/IframePreview/iframePreviewApp";
|
|
12
20
|
|
|
13
21
|
// Import CSS - Vite will extract this to dist/style.css during build
|
|
14
22
|
// Consuming apps should import "vue-wswg-editor/style.css"
|
package/src/shims.d.ts
CHANGED
|
@@ -67,6 +67,10 @@ declare module "vue-wswg-editor:thumbnails" {
|
|
|
67
67
|
export const modules: Record<string, () => Promise<any>>;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
declare module "vue-wswg-editor:themes" {
|
|
71
|
+
export const modules: Record<string, () => Promise<any>>;
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
// SortableJS type declaration - reference @types/sortablejs and re-export as ESM default
|
|
71
75
|
/// <reference types="sortablejs" />
|
|
72
76
|
declare module "sortablejs" {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom component type with additional layout-specific properties
|
|
3
|
+
* These properties are set via defineOptions() in Vue components
|
|
4
|
+
*/
|
|
5
|
+
export type Theme = {
|
|
6
|
+
id: string; // The ID of the theme
|
|
7
|
+
path: string; // The path to the theme
|
|
8
|
+
title: string; // The title of the theme
|
|
9
|
+
description: string; // The description of the theme
|
|
10
|
+
version: string; // The version of the theme
|
|
11
|
+
author: string; // The author of the theme
|
|
12
|
+
authorWebsite: string; // The author website of the theme
|
|
13
|
+
tags: string[]; // The tags of the theme
|
|
14
|
+
license: string; // The license of the theme
|
|
15
|
+
};
|
package/src/util/registry.ts
CHANGED
|
@@ -161,7 +161,7 @@ async function initialiseBlockFieldsRegistry(): Promise<void> {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
async function initialiseLayoutRegistry(): Promise<void> {
|
|
164
|
+
export async function initialiseLayoutRegistry(): Promise<void> {
|
|
165
165
|
Object.keys(pageBuilderLayouts.value).forEach((key) => {
|
|
166
166
|
delete pageBuilderLayouts.value[key];
|
|
167
167
|
});
|
|
@@ -186,7 +186,7 @@ async function initialiseLayoutRegistry(): Promise<void> {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
async function initialiseBlockRegistry(): Promise<void> {
|
|
189
|
+
export async function initialiseBlockRegistry(): Promise<void> {
|
|
190
190
|
// Clear existing registry
|
|
191
191
|
Object.keys(pageBuilderBlocks.value).forEach((key) => {
|
|
192
192
|
delete pageBuilderBlocks.value[key];
|