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,397 @@
|
|
|
1
|
+
import { shallowRef, markRaw, type Ref } from "vue";
|
|
2
|
+
import type { Theme } from "../types/Theme";
|
|
3
|
+
import type { Layout } from "../types/Layout";
|
|
4
|
+
import type { Block } from "../types/Block";
|
|
5
|
+
import { generateNameVariations, toCamelCase } from "./helpers";
|
|
6
|
+
import { EditorFieldConfig } from "./fieldConfig";
|
|
7
|
+
|
|
8
|
+
// Load all thumbnail images - Vite will process these as assets and provide URLs
|
|
9
|
+
// For images, Vite returns the URL as the default export when using eager: true
|
|
10
|
+
// const thumbnailModules = import.meta.glob("@page-builder/blocks/**/thumbnail.png", { eager: true });
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Registry of all page builder themes, blocks, layouts, fields & thumbnails
|
|
14
|
+
*/
|
|
15
|
+
export const pageBuilderThemes: Ref<Record<string, Theme>> = shallowRef({});
|
|
16
|
+
export const themeLayouts: Ref<Record<string, Layout>> = shallowRef({});
|
|
17
|
+
export const themeBlocks: Ref<Record<string, Block>> = shallowRef({});
|
|
18
|
+
export const themeBlockFields: Ref<Record<string, Record<string, EditorFieldConfig>>> = shallowRef({});
|
|
19
|
+
export const activeThemeId: Ref<string | undefined> = shallowRef(undefined);
|
|
20
|
+
let blockThumbnails: Record<string, any> | null = null; // non reactive cache of block thumbnails
|
|
21
|
+
let themeThumbnails: Record<string, any> | null = null; // non reactive cache of theme thumbnails
|
|
22
|
+
|
|
23
|
+
////////////////////////////////////////////////////////////
|
|
24
|
+
// Helper Functions
|
|
25
|
+
////////////////////////////////////////////////////////////
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract the default export from a module, handling different module formats
|
|
29
|
+
* Works with both eager and lazy-loaded modules from import.meta.glob
|
|
30
|
+
*/
|
|
31
|
+
export function getModuleDefault(module: any): any {
|
|
32
|
+
if (!module) return undefined;
|
|
33
|
+
|
|
34
|
+
// If module is a function (lazy-loaded), we'd need to await it, but with eager: true it should be resolved
|
|
35
|
+
if (typeof module === "function") {
|
|
36
|
+
// This shouldn't happen with eager: true, but handle it just in case
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Try .default first (standard ES module format)
|
|
41
|
+
if (typeof module === "object") {
|
|
42
|
+
// Check if it has .default property
|
|
43
|
+
if ("default" in module && module.default !== undefined) {
|
|
44
|
+
return module.default;
|
|
45
|
+
}
|
|
46
|
+
// If no .default but module has component-like properties (__name, etc.), maybe module itself is the component
|
|
47
|
+
if ("__name" in module || "name" in module || "setup" in module || "render" in module) {
|
|
48
|
+
return module;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: return the module as-is
|
|
53
|
+
return module;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**************************************************
|
|
57
|
+
* THEMES
|
|
58
|
+
**************************************************/
|
|
59
|
+
export async function initialiseThemeRegistry(): Promise<void> {
|
|
60
|
+
// Clear existing registry
|
|
61
|
+
Object.keys(pageBuilderThemes.value).forEach((key) => {
|
|
62
|
+
delete pageBuilderThemes.value[key];
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Lazy load virtual modules to prevent initialization order issues
|
|
66
|
+
const { modules: themeModules } = await import("vue-wswg-editor:themes");
|
|
67
|
+
|
|
68
|
+
for (const [path, module] of Object.entries(themeModules)) {
|
|
69
|
+
let resolvedModule = module;
|
|
70
|
+
// If module is a function (lazy-loaded), call it to get the actual module
|
|
71
|
+
if (typeof module === "function") {
|
|
72
|
+
resolvedModule = await module();
|
|
73
|
+
}
|
|
74
|
+
const themeConfig = getModuleDefault(resolvedModule);
|
|
75
|
+
if (!themeConfig) continue;
|
|
76
|
+
|
|
77
|
+
// Extract theme ID from directory name containing theme.config.js
|
|
78
|
+
// Path format: "@page-builder/demo-theme/theme.config.js" -> theme ID is "demo-theme"
|
|
79
|
+
const pathParts = path.split("/");
|
|
80
|
+
// Remove the filename (theme.config.js) to get the directory path
|
|
81
|
+
const directoryPath = path.replace(/\/[^/]+\.config\.js$/, "");
|
|
82
|
+
// Get the directory name (second to last part of the path)
|
|
83
|
+
const themeId = pathParts[pathParts.length - 2];
|
|
84
|
+
|
|
85
|
+
if (!themeId) continue;
|
|
86
|
+
|
|
87
|
+
const theme: Theme = {
|
|
88
|
+
id: themeId,
|
|
89
|
+
path: directoryPath,
|
|
90
|
+
title: themeConfig.title || themeId,
|
|
91
|
+
description: themeConfig.description || "",
|
|
92
|
+
version: themeConfig.version || "1.0.0",
|
|
93
|
+
author: themeConfig.author || "",
|
|
94
|
+
authorWebsite: themeConfig.authorWebsite || "",
|
|
95
|
+
tags: themeConfig.tags || [],
|
|
96
|
+
license: themeConfig.license || "",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
pageBuilderThemes.value[themeId] = theme;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If there are no themes, throw an error
|
|
103
|
+
if (Object.keys(pageBuilderThemes.value).length === 0) {
|
|
104
|
+
console.error("[vue-wswg-editor:registry] No themes found");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getThemes(): Theme[] {
|
|
110
|
+
return Object.values(pageBuilderThemes.value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getActiveTheme(): Theme {
|
|
114
|
+
if (!activeThemeId.value) {
|
|
115
|
+
throw new Error("No active theme found");
|
|
116
|
+
}
|
|
117
|
+
return pageBuilderThemes.value[activeThemeId.value];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function setActiveTheme(themeId?: string): Promise<void> {
|
|
121
|
+
// If the themeID is not found, set the active theme ID to the first theme in the registry
|
|
122
|
+
if (!themeId || !pageBuilderThemes.value[themeId]) {
|
|
123
|
+
const firstThemeId = Object.keys(pageBuilderThemes.value)[0];
|
|
124
|
+
if (!firstThemeId) {
|
|
125
|
+
throw new Error("[vue-wswg-editor:registry] No themes found. Cannot set active theme.");
|
|
126
|
+
}
|
|
127
|
+
activeThemeId.value = firstThemeId;
|
|
128
|
+
console.warn(
|
|
129
|
+
`[vue-wswg-editor:registry] No theme found with ID: ${themeId}, setting active theme to: ${activeThemeId.value}`
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
activeThemeId.value = themeId;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**************************************************
|
|
137
|
+
* THEME THUMBNAILS
|
|
138
|
+
**************************************************/
|
|
139
|
+
async function initialiseThemeThumbnailsRegistry(): Promise<void> {
|
|
140
|
+
const { modules: thumbnailModules } = await import("vue-wswg-editor:thumbnails");
|
|
141
|
+
themeThumbnails = thumbnailModules;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getThemeThumbnail(themeDirectory: string): string | undefined {
|
|
145
|
+
if (!themeDirectory || !themeThumbnails) return undefined;
|
|
146
|
+
|
|
147
|
+
// Try thumbnail.jpg first, then fall back to thumbnail.png
|
|
148
|
+
const thumbnailPaths = [`${themeDirectory}/thumbnail.jpg`, `${themeDirectory}/thumbnail.png`];
|
|
149
|
+
|
|
150
|
+
for (const thumbnailPath of thumbnailPaths) {
|
|
151
|
+
const thumbnailModule = themeThumbnails[thumbnailPath];
|
|
152
|
+
if (thumbnailModule) {
|
|
153
|
+
try {
|
|
154
|
+
const thumbnailUrl = (thumbnailModule as any)?.default;
|
|
155
|
+
if (thumbnailUrl) {
|
|
156
|
+
return thumbnailUrl as string;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// Silently continue to next format if this one fails
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Return undefined if no thumbnail found
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**************************************************
|
|
170
|
+
* LAYOUTS
|
|
171
|
+
**************************************************/
|
|
172
|
+
export async function initialiseLayoutRegistry(): Promise<void> {
|
|
173
|
+
// Clear existing registry
|
|
174
|
+
Object.keys(themeLayouts.value).forEach((key) => {
|
|
175
|
+
delete themeLayouts.value[key];
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Get the active theme
|
|
179
|
+
const activeTheme = getActiveTheme();
|
|
180
|
+
|
|
181
|
+
// Use virtual module to load all layouts (scans all themes at build time)
|
|
182
|
+
// Then filter to only process layouts from the active theme
|
|
183
|
+
const { modules: layoutModules } = await import("vue-wswg-editor:layouts");
|
|
184
|
+
|
|
185
|
+
// Filter to only layouts from the active theme
|
|
186
|
+
const themeLayoutPath = `${activeTheme.path}/layout`;
|
|
187
|
+
const themeLayoutModules = Object.entries(layoutModules).filter(([path]) => path.startsWith(themeLayoutPath));
|
|
188
|
+
|
|
189
|
+
// Process only the active theme's layouts
|
|
190
|
+
for (const [, module] of themeLayoutModules) {
|
|
191
|
+
let resolvedModule = module;
|
|
192
|
+
// If module is a function (lazy-loaded), call it to get the actual module
|
|
193
|
+
if (typeof module === "function") {
|
|
194
|
+
resolvedModule = await module();
|
|
195
|
+
}
|
|
196
|
+
const layout = getModuleDefault(resolvedModule);
|
|
197
|
+
// exclude modules without name or label
|
|
198
|
+
if (!layout || !layout.label) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// Mark layout component as raw to prevent Vue from making it reactive
|
|
202
|
+
themeLayouts.value[layout.__name] = markRaw(layout);
|
|
203
|
+
}
|
|
204
|
+
// If there are no layouts, warn
|
|
205
|
+
if (Object.keys(themeLayouts.value).length === 0) {
|
|
206
|
+
console.warn(`[vue-wswg-editor:registry] No layouts found for theme: ${activeTheme.id}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function getLayout(layoutType: string): Layout | undefined {
|
|
212
|
+
// Generate name variations and try to find a match
|
|
213
|
+
const nameVariations = generateNameVariations(layoutType);
|
|
214
|
+
|
|
215
|
+
for (const variation of nameVariations) {
|
|
216
|
+
const layout = themeLayouts.value[variation];
|
|
217
|
+
if (layout) {
|
|
218
|
+
return layout;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**************************************************
|
|
226
|
+
* BLOCK FIELDS
|
|
227
|
+
**************************************************/
|
|
228
|
+
async function initialiseBlockFieldsRegistry(): Promise<void> {
|
|
229
|
+
// Clear existing registry
|
|
230
|
+
Object.keys(themeBlockFields.value).forEach((key) => {
|
|
231
|
+
delete themeBlockFields.value[key];
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Get the active theme
|
|
235
|
+
const activeTheme = getActiveTheme();
|
|
236
|
+
|
|
237
|
+
// Use virtual module to load all block fields (scans all themes at build time)
|
|
238
|
+
// Then filter to only process block fields from the active theme
|
|
239
|
+
const { modules: blockFieldsModules } = await import("vue-wswg-editor:fields");
|
|
240
|
+
|
|
241
|
+
// Filter to only block fields from the active theme
|
|
242
|
+
const themeBlockFieldsPath = `${activeTheme.path}/blocks`;
|
|
243
|
+
const themeBlockFieldsModules = Object.entries(blockFieldsModules).filter(([path]) =>
|
|
244
|
+
path.startsWith(themeBlockFieldsPath)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Process only the active theme's block fields
|
|
248
|
+
for (const [path, module] of themeBlockFieldsModules) {
|
|
249
|
+
let resolvedModule = module;
|
|
250
|
+
// If module is a function (lazy-loaded), call it to get the actual module
|
|
251
|
+
if (typeof module === "function") {
|
|
252
|
+
resolvedModule = await module();
|
|
253
|
+
}
|
|
254
|
+
const blockFields = getModuleDefault(resolvedModule);
|
|
255
|
+
// Mark block fields component as raw to prevent Vue from making it reactive
|
|
256
|
+
themeBlockFields.value[path] = markRaw(blockFields);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getBlockFieldsFile(path: string): Record<string, EditorFieldConfig> {
|
|
261
|
+
const fieldsPath = `${path}/fields.ts`;
|
|
262
|
+
try {
|
|
263
|
+
// Generate path for fields.ts file
|
|
264
|
+
const blockField = themeBlockFields.value[fieldsPath];
|
|
265
|
+
if (blockField) {
|
|
266
|
+
return blockField;
|
|
267
|
+
}
|
|
268
|
+
return {};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error("Error getting block fields for block: ", fieldsPath, error);
|
|
271
|
+
return {};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**************************************************
|
|
276
|
+
* BLOCKS
|
|
277
|
+
**************************************************/
|
|
278
|
+
export async function initialiseBlockRegistry(): Promise<void> {
|
|
279
|
+
// Clear existing registry
|
|
280
|
+
Object.keys(themeBlocks.value).forEach((key) => {
|
|
281
|
+
delete themeBlocks.value[key];
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Get the active theme
|
|
285
|
+
const activeTheme = getActiveTheme();
|
|
286
|
+
|
|
287
|
+
// Use virtual module to load all layouts (scans all themes at build time)
|
|
288
|
+
// Then filter to only process layouts from the active theme
|
|
289
|
+
const { modules: blockModules } = await import("vue-wswg-editor:blocks");
|
|
290
|
+
|
|
291
|
+
// Filter to only layouts from the active theme
|
|
292
|
+
const themeBlockPath = `${activeTheme.path}/blocks`;
|
|
293
|
+
const themeBlockModules = Object.entries(blockModules).filter(([path]) => path.startsWith(themeBlockPath));
|
|
294
|
+
|
|
295
|
+
// Process only the active theme's layouts
|
|
296
|
+
for (const [path, module] of themeBlockModules) {
|
|
297
|
+
let resolvedModule = module;
|
|
298
|
+
// If module is a function (lazy-loaded), call it to get the actual module
|
|
299
|
+
if (typeof module === "function") {
|
|
300
|
+
resolvedModule = await module();
|
|
301
|
+
}
|
|
302
|
+
const blockModule = getModuleDefault(resolvedModule);
|
|
303
|
+
// exclude modules without name or label
|
|
304
|
+
if (!blockModule || !blockModule.__name) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Format the block
|
|
309
|
+
const blockType = toCamelCase(blockModule.type || blockModule.__name);
|
|
310
|
+
// Extract directory path from component path (e.g., "@page-builder/my-theme/blocks/hero-section/hero-section.vue" -> "@page-builder/my-theme/blocks/hero-section")
|
|
311
|
+
const directory = path.replace(/\/[^/]+\.vue$/, "");
|
|
312
|
+
// Mark the component itself as raw before spreading
|
|
313
|
+
const rawComponent = markRaw(blockModule);
|
|
314
|
+
const block: Block = {
|
|
315
|
+
fields: getBlockFieldsFile(directory), // Load the block fields file
|
|
316
|
+
...rawComponent, // Component can override fields if defined directly
|
|
317
|
+
path: directory, // Path where the block component is located (e.g., "@page-builder/my-theme/blocks/hero-section")
|
|
318
|
+
type: blockType,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Add the block to the registry
|
|
322
|
+
themeBlocks.value[blockType] = markRaw(block);
|
|
323
|
+
}
|
|
324
|
+
// If there are no blocks, warn
|
|
325
|
+
if (Object.keys(themeBlocks.value).length === 0) {
|
|
326
|
+
console.warn(`[vue-wswg-editor:registry] No blocks found for theme: ${activeTheme.id}`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function getBlock(blockType: string): Block | undefined {
|
|
332
|
+
// Generate name variations and try to find a match
|
|
333
|
+
const nameVariations = generateNameVariations(blockType);
|
|
334
|
+
|
|
335
|
+
for (const variation of nameVariations) {
|
|
336
|
+
const block = themeBlocks.value[variation];
|
|
337
|
+
if (block) {
|
|
338
|
+
return block;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**************************************************
|
|
346
|
+
* BLOCK THUMBNAILS
|
|
347
|
+
**************************************************/
|
|
348
|
+
async function initialiseBlockThumbnailsRegistry(): Promise<void> {
|
|
349
|
+
const { modules: thumbnailModules } = await import("vue-wswg-editor:thumbnails");
|
|
350
|
+
blockThumbnails = thumbnailModules;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function getBlockThumbnail(blockDirectory: string): string | undefined {
|
|
354
|
+
if (!blockDirectory || !blockThumbnails) return undefined;
|
|
355
|
+
const thumbnailPath = `${blockDirectory}/thumbnail.png`;
|
|
356
|
+
const thumbnailModule = blockThumbnails[thumbnailPath];
|
|
357
|
+
if (!thumbnailModule) return undefined;
|
|
358
|
+
return (thumbnailModule as any).default as string | undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**************************************************
|
|
362
|
+
* REGISTRY COMPOSERS
|
|
363
|
+
**************************************************/
|
|
364
|
+
|
|
365
|
+
/** THEME BLOCK & FIELD REGISTRY */
|
|
366
|
+
async function initialiseThemeSubRegistries(useEditingRegistry?: boolean): Promise<void> {
|
|
367
|
+
// Load the layouts that belong to the active theme
|
|
368
|
+
await initialiseLayoutRegistry();
|
|
369
|
+
// Load the additional theme fields and thumbnails if in edit mode
|
|
370
|
+
if (useEditingRegistry) {
|
|
371
|
+
await initialiseBlockFieldsRegistry();
|
|
372
|
+
await initialiseBlockThumbnailsRegistry();
|
|
373
|
+
await initialiseThemeThumbnailsRegistry();
|
|
374
|
+
}
|
|
375
|
+
// Load the blocks that belong to the active theme
|
|
376
|
+
await initialiseBlockRegistry();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Initialize the registry with a specific theme
|
|
381
|
+
* @param themeId - The theme ID to load blocks/layouts from
|
|
382
|
+
*/
|
|
383
|
+
export async function initialiseRegistry(themeId?: string, useEditingRegistry: boolean = true): Promise<void> {
|
|
384
|
+
try {
|
|
385
|
+
// First, initialize theme registry to discover all available themes
|
|
386
|
+
await initialiseThemeRegistry();
|
|
387
|
+
|
|
388
|
+
// Set the active theme ID
|
|
389
|
+
await setActiveTheme(themeId);
|
|
390
|
+
|
|
391
|
+
// Load the theme sub registries (layouts, blocks, fields)
|
|
392
|
+
await initialiseThemeSubRegistries(useEditingRegistry);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error("[vue-wswg-editor:registry] Error during registry initialization:", error);
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
397
|
+
}
|
package/src/util/validation.ts
CHANGED
|
@@ -1,25 +1,116 @@
|
|
|
1
1
|
import type { EditorFieldConfig, ValidatorFunction } from "./fieldConfig";
|
|
2
|
-
import {
|
|
2
|
+
import { getBlock } from "./theme-registry";
|
|
3
|
+
import { getLayoutFields } from "./registry";
|
|
3
4
|
import { toNiceName } from "./helpers";
|
|
4
5
|
|
|
5
|
-
export function validateField(
|
|
6
|
+
export async function validateField(
|
|
7
|
+
value: any,
|
|
8
|
+
fieldConfig: EditorFieldConfig
|
|
9
|
+
): Promise<boolean | string | ValidationResult> {
|
|
6
10
|
// Create generic validator from field config properties (minLength, maxLength, etc.)
|
|
7
11
|
const genericValidator = createGenericValidator(fieldConfig);
|
|
8
12
|
|
|
9
13
|
// Combine generic validator with custom validator if provided
|
|
10
14
|
const combinedValidator = combineValidators(genericValidator, fieldConfig.validator);
|
|
11
15
|
|
|
12
|
-
//
|
|
13
|
-
if (
|
|
16
|
+
// Validate the field itself first
|
|
17
|
+
if (combinedValidator) {
|
|
18
|
+
const result = await combinedValidator(value);
|
|
19
|
+
if (result !== true) {
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle nested structures
|
|
25
|
+
// For repeater fields, validate each item's fields
|
|
26
|
+
if (fieldConfig.type === "repeater" && fieldConfig.repeaterFields && Array.isArray(value)) {
|
|
27
|
+
const nestedValidationResult: ValidationResult = {
|
|
28
|
+
title: fieldConfig.label || "Items",
|
|
29
|
+
isValid: true,
|
|
30
|
+
errors: {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < value.length; i++) {
|
|
34
|
+
const item = value[i];
|
|
35
|
+
if (!item) continue;
|
|
36
|
+
|
|
37
|
+
const itemValidationResult: ValidationResult = {
|
|
38
|
+
title: `Item ${i + 1}`,
|
|
39
|
+
isValid: true,
|
|
40
|
+
errors: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (const fieldName of Object.keys(fieldConfig.repeaterFields)) {
|
|
44
|
+
const nestedFieldConfig: EditorFieldConfig | undefined = fieldConfig.repeaterFields[fieldName];
|
|
45
|
+
if (!nestedFieldConfig) continue;
|
|
46
|
+
const nestedValue = item[fieldName];
|
|
47
|
+
const nestedResult = await validateField(nestedValue, nestedFieldConfig);
|
|
48
|
+
if (nestedResult !== true) {
|
|
49
|
+
const fieldLabel = nestedFieldConfig.label || fieldName;
|
|
50
|
+
// If nested result is a ValidationResult, nest it; otherwise use as string
|
|
51
|
+
if (typeof nestedResult === "object" && "isValid" in nestedResult) {
|
|
52
|
+
itemValidationResult.errors[fieldLabel] = nestedResult;
|
|
53
|
+
} else {
|
|
54
|
+
itemValidationResult.errors[fieldLabel] = nestedResult;
|
|
55
|
+
}
|
|
56
|
+
itemValidationResult.isValid = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!itemValidationResult.isValid) {
|
|
61
|
+
nestedValidationResult.errors[`Item ${i + 1}`] = itemValidationResult;
|
|
62
|
+
nestedValidationResult.isValid = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!nestedValidationResult.isValid) {
|
|
67
|
+
return nestedValidationResult;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For object fields, validate each nested field
|
|
72
|
+
if (
|
|
73
|
+
fieldConfig.type === "object" &&
|
|
74
|
+
fieldConfig.objectFields &&
|
|
75
|
+
value &&
|
|
76
|
+
typeof value === "object" &&
|
|
77
|
+
!Array.isArray(value)
|
|
78
|
+
) {
|
|
79
|
+
const nestedValidationResult: ValidationResult = {
|
|
80
|
+
title: fieldConfig.label || "Object",
|
|
81
|
+
isValid: true,
|
|
82
|
+
errors: {},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
for (const fieldName of Object.keys(fieldConfig.objectFields)) {
|
|
86
|
+
const nestedFieldConfig: EditorFieldConfig | undefined = fieldConfig.objectFields[fieldName];
|
|
87
|
+
if (!nestedFieldConfig) continue;
|
|
88
|
+
const nestedValue = value[fieldName];
|
|
89
|
+
const nestedResult = await validateField(nestedValue, nestedFieldConfig);
|
|
90
|
+
if (nestedResult !== true) {
|
|
91
|
+
const fieldLabel = nestedFieldConfig.label || fieldName;
|
|
92
|
+
// If nested result is a ValidationResult, nest it; otherwise use as string
|
|
93
|
+
if (typeof nestedResult === "object" && "isValid" in nestedResult) {
|
|
94
|
+
nestedValidationResult.errors[fieldLabel] = nestedResult;
|
|
95
|
+
} else {
|
|
96
|
+
nestedValidationResult.errors[fieldLabel] = nestedResult;
|
|
97
|
+
}
|
|
98
|
+
nestedValidationResult.isValid = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!nestedValidationResult.isValid) {
|
|
103
|
+
return nestedValidationResult;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
14
106
|
|
|
15
|
-
|
|
16
|
-
return combinedValidator(value);
|
|
107
|
+
return true;
|
|
17
108
|
}
|
|
18
109
|
|
|
19
110
|
export interface ValidationResult {
|
|
20
111
|
title: string;
|
|
21
112
|
isValid: boolean;
|
|
22
|
-
errors: Record<string, string | boolean>;
|
|
113
|
+
errors: Record<string, string | boolean | ValidationResult>;
|
|
23
114
|
}
|
|
24
115
|
|
|
25
116
|
/**
|
|
@@ -63,7 +154,7 @@ async function validateSettings(value: any, settingsKey: string = "settings"): P
|
|
|
63
154
|
const fieldConfig = layoutOptions[field];
|
|
64
155
|
if (!fieldConfig) continue;
|
|
65
156
|
const result = await validateField(value[settingsKey][field], fieldConfig);
|
|
66
|
-
// If validation fails (returns false or
|
|
157
|
+
// If validation fails (returns false, string, or ValidationResult), add to validation results
|
|
67
158
|
if (result !== true) {
|
|
68
159
|
validationResult.errors[fieldConfig.label || field] = result;
|
|
69
160
|
validationResult.isValid = false;
|
|
@@ -84,11 +175,11 @@ async function validateBlocks(value: any, blocksKey: string = "blocks"): Promise
|
|
|
84
175
|
// Get the block type
|
|
85
176
|
const blockType = block.type;
|
|
86
177
|
// Get the block editor fields
|
|
87
|
-
const blockComponent =
|
|
178
|
+
const blockComponent = getBlock(blockType);
|
|
88
179
|
|
|
89
180
|
// Add validation results entry for the section
|
|
90
181
|
validationResults[blockType] = {
|
|
91
|
-
title: blockComponent
|
|
182
|
+
title: blockComponent?.label || toNiceName(blockType),
|
|
92
183
|
isValid: true,
|
|
93
184
|
errors: {},
|
|
94
185
|
};
|
|
@@ -104,7 +195,7 @@ async function validateBlocks(value: any, blocksKey: string = "blocks"): Promise
|
|
|
104
195
|
if (!fieldConfig) continue;
|
|
105
196
|
// Validate
|
|
106
197
|
const result = await validateField(block[field], fieldConfig);
|
|
107
|
-
// If validation fails (returns false or
|
|
198
|
+
// If validation fails (returns false, string, or ValidationResult), add to validation results
|
|
108
199
|
if (result !== true) {
|
|
109
200
|
validationResults[blockType].errors[fieldConfig.label || field] = result;
|
|
110
201
|
validationResults[blockType].isValid = false;
|
package/src/vite-plugin.ts
CHANGED
|
@@ -15,6 +15,7 @@ export function vueWswgEditorPlugin(options: VueWswgEditorPluginOptions): Plugin
|
|
|
15
15
|
fields: "\0vue-wswg-editor:fields",
|
|
16
16
|
layouts: "\0vue-wswg-editor:layouts",
|
|
17
17
|
thumbnails: "\0vue-wswg-editor:thumbnails",
|
|
18
|
+
themes: "\0vue-wswg-editor:themes",
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
return {
|
|
@@ -41,6 +42,7 @@ export function vueWswgEditorPlugin(options: VueWswgEditorPluginOptions): Plugin
|
|
|
41
42
|
"vue-wswg-editor:blocks",
|
|
42
43
|
"vue-wswg-editor:fields",
|
|
43
44
|
"vue-wswg-editor:thumbnails",
|
|
45
|
+
"vue-wswg-editor:themes",
|
|
44
46
|
];
|
|
45
47
|
for (const item of itemsToExclude) {
|
|
46
48
|
if (!exclude.includes(item)) {
|
|
@@ -90,13 +92,15 @@ export function vueWswgEditorPlugin(options: VueWswgEditorPluginOptions): Plugin
|
|
|
90
92
|
// Using a more explicit format to ensure Vite processes it correctly
|
|
91
93
|
switch (id) {
|
|
92
94
|
case virtualModules.layouts:
|
|
93
|
-
return `export const modules = import.meta.glob("${options.rootDir}
|
|
95
|
+
return `export const modules = import.meta.glob("${options.rootDir}/*/layout/**/*.vue", { eager: true });`;
|
|
94
96
|
case virtualModules.blocks:
|
|
95
|
-
return `export const modules = import.meta.glob("${options.rootDir}
|
|
97
|
+
return `export const modules = import.meta.glob("${options.rootDir}/*/blocks/**/*.vue", { eager: true });`;
|
|
96
98
|
case virtualModules.fields:
|
|
97
|
-
return `export const modules = import.meta.glob("${options.rootDir}
|
|
99
|
+
return `export const modules = import.meta.glob("${options.rootDir}/*/blocks/**/fields.ts", { eager: true });`;
|
|
98
100
|
case virtualModules.thumbnails:
|
|
99
|
-
return `export const modules = import.meta.glob("${options.rootDir}
|
|
101
|
+
return `export const modules = import.meta.glob(["${options.rootDir}/*/blocks/**/thumbnail.png", "${options.rootDir}/*/thumbnail.jpg", "${options.rootDir}/*/thumbnail.png"], { eager: true });`;
|
|
102
|
+
case virtualModules.themes:
|
|
103
|
+
return `export const modules = import.meta.glob("${options.rootDir}/**/theme.config.js", { eager: true });`;
|
|
100
104
|
default:
|
|
101
105
|
return undefined;
|
|
102
106
|
}
|
|
@@ -159,3 +159,7 @@ declare module "vue-wswg-editor:fields" {
|
|
|
159
159
|
declare module "vue-wswg-editor:thumbnails" {
|
|
160
160
|
export const modules: Record<string, () => Promise<any>>;
|
|
161
161
|
}
|
|
162
|
+
|
|
163
|
+
declare module "vue-wswg-editor:themes" {
|
|
164
|
+
export const modules: Record<string, () => Promise<any>>;
|
|
165
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// Alternative 1: Use relative paths instead of aliases
|
|
2
|
-
// This works because the consuming app processes this file
|
|
3
|
-
// The path is relative to where the consuming app's vite.config.ts is located
|
|
4
|
-
// For participant/admin projects, this resolves to ../../page-builder/blocks/**/*.vue
|
|
5
|
-
export const blockModules = import.meta.glob("../../page-builder/blocks/**/*.vue", { eager: true });
|
|
6
|
-
|
|
7
|
-
// However, this is fragile because it depends on the file location relative to the consuming app
|
|
8
|
-
// Better: Use a build-time script (see Alternative 2)
|
|
9
|
-
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Alternative 4: Manual explicit imports
|
|
2
|
-
// Most explicit, but requires updating when adding new blocks
|
|
3
|
-
|
|
4
|
-
import HeroSection from "../../../../page-builder/blocks/hero-section/hero-section.vue";
|
|
5
|
-
import CardsSection from "../../../../page-builder/blocks/cards-section/cards-section.vue";
|
|
6
|
-
import FaqSection from "../../../../page-builder/blocks/faq-section/faq-section.vue";
|
|
7
|
-
import FlexibleContent from "../../../../page-builder/blocks/flexible-content/flexible-content.vue";
|
|
8
|
-
import TabsSection from "../../../../page-builder/blocks/tabs-section/tabs-section.vue";
|
|
9
|
-
import TextSection from "../../../../page-builder/blocks/text-section/text-section.vue";
|
|
10
|
-
|
|
11
|
-
export const blockModules = {
|
|
12
|
-
"hero-section": HeroSection,
|
|
13
|
-
"cards-section": CardsSection,
|
|
14
|
-
"faq-section": FaqSection,
|
|
15
|
-
"flexible-content": FlexibleContent,
|
|
16
|
-
"tabs-section": TabsSection,
|
|
17
|
-
"text-section": TextSection,
|
|
18
|
-
};
|
|
19
|
-
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Alternative 3: Runtime registration pattern
|
|
2
|
-
// Components register themselves when imported
|
|
3
|
-
|
|
4
|
-
import { ref, type Ref } from "vue";
|
|
5
|
-
import type { Component } from "vue";
|
|
6
|
-
|
|
7
|
-
// Registry store
|
|
8
|
-
export const blockRegistry: Ref<Record<string, Component>> = ref({});
|
|
9
|
-
|
|
10
|
-
// Registration function
|
|
11
|
-
export function registerBlock(type: string, component: Component) {
|
|
12
|
-
blockRegistry.value[type] = component;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// In each block component, add:
|
|
16
|
-
// import { registerBlock } from "./blockModules-runtime";
|
|
17
|
-
// registerBlock("hero-section", defineComponent({ ... }));
|
|
18
|
-
|
|
19
|
-
// Then manually import blocks:
|
|
20
|
-
// import "./blocks/hero-section/hero-section.vue";
|
|
21
|
-
// import "./blocks/cards-section/cards-section.vue";
|
|
22
|
-
// etc.
|
|
23
|
-
|