vue-wswg-editor 0.0.1
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 +91 -0
- package/dist/style.css +1 -0
- package/dist/types/components/AddBlockItem/AddBlockItem.vue.d.ts +6 -0
- package/dist/types/components/BlockBrowser/BlockBrowser.vue.d.ts +2 -0
- package/dist/types/components/BlockComponent/BlockComponent.vue.d.ts +15 -0
- package/dist/types/components/BlockEditorFieldNode/BlockEditorFieldNode.vue.d.ts +15 -0
- package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +15 -0
- package/dist/types/components/BlockMarginFieldNode/BlockMarginNode.vue.d.ts +23 -0
- package/dist/types/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue.d.ts +15 -0
- package/dist/types/components/BrowserNavigation/BrowserNavigation.vue.d.ts +5 -0
- package/dist/types/components/EmptyState/EmptyState.vue.d.ts +15 -0
- package/dist/types/components/PageBlockList/PageBlockList.vue.d.ts +19 -0
- package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +30 -0
- package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +28 -0
- package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +6 -0
- package/dist/types/components/PageRenderer/blockModules.d.ts +1 -0
- package/dist/types/components/PageSettings/PageSettings.vue.d.ts +15 -0
- package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +6 -0
- package/dist/types/components/WswgJsonEditor/WswgJsonEditor.test.d.ts +1 -0
- package/dist/types/components/WswgJsonEditor/WswgJsonEditor.vue.d.ts +40 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/dist/types/util/fieldConfig.d.ts +82 -0
- package/dist/types/util/helpers.d.ts +28 -0
- package/dist/types/util/registry.d.ts +21 -0
- package/dist/types/util/validation.d.ts +15 -0
- package/dist/vue-wswg-editor.es.js +3377 -0
- package/package.json +85 -0
- package/src/assets/images/empty-state.jpg +0 -0
- package/src/assets/styles/_mixins.scss +73 -0
- package/src/assets/styles/main.css +3 -0
- package/src/components/AddBlockItem/AddBlockItem.vue +50 -0
- package/src/components/BlockBrowser/BlockBrowser.vue +69 -0
- package/src/components/BlockComponent/BlockComponent.vue +186 -0
- package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +378 -0
- package/src/components/BlockEditorFields/BlockEditorFields.vue +91 -0
- package/src/components/BlockMarginFieldNode/BlockMarginNode.vue +132 -0
- package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +217 -0
- package/src/components/BrowserNavigation/BrowserNavigation.vue +27 -0
- package/src/components/EmptyState/EmptyState.vue +94 -0
- package/src/components/PageBlockList/PageBlockList.vue +103 -0
- package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +241 -0
- package/src/components/PageBuilderToolbar/PageBuilderToolbar.vue +63 -0
- package/src/components/PageRenderer/PageRenderer.vue +65 -0
- package/src/components/PageRenderer/blockModules-alternative.ts.example +9 -0
- package/src/components/PageRenderer/blockModules-manual.ts.example +19 -0
- package/src/components/PageRenderer/blockModules-runtime.ts.example +23 -0
- package/src/components/PageRenderer/blockModules.ts +3 -0
- package/src/components/PageSettings/PageSettings.vue +86 -0
- package/src/components/ResizeHandle/ResizeHandle.vue +105 -0
- package/src/components/WswgJsonEditor/WswgJsonEditor.test.ts +43 -0
- package/src/components/WswgJsonEditor/WswgJsonEditor.vue +391 -0
- package/src/index.ts +15 -0
- package/src/shims.d.ts +72 -0
- package/src/style.css +3 -0
- package/src/types/Block.d.ts +19 -0
- package/src/types/Layout.d.ts +9 -0
- package/src/util/fieldConfig.ts +173 -0
- package/src/util/helpers.ts +176 -0
- package/src/util/registry.ts +149 -0
- package/src/util/validation.ts +110 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
////////////////////////////////////////////////////////////
|
|
2
|
+
// File name traversal
|
|
3
|
+
////////////////////////////////////////////////////////////
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* File lookup utility for finding Vue components with flexible naming conventions
|
|
7
|
+
*
|
|
8
|
+
* This utility handles different naming formats (snake_case, camelCase, kebab-case, PascalCase)
|
|
9
|
+
* and various file/folder structures to provide maximum flexibility in component organization.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Converts a string to different naming conventions
|
|
14
|
+
*/
|
|
15
|
+
export function generateNameVariations(name: string): string[] {
|
|
16
|
+
const variations = new Set<string>();
|
|
17
|
+
|
|
18
|
+
// Original name
|
|
19
|
+
variations.add(name);
|
|
20
|
+
|
|
21
|
+
// Convert to different formats
|
|
22
|
+
// snake_case
|
|
23
|
+
const snakeCase = name
|
|
24
|
+
.replace(/([A-Z])/g, "_$1")
|
|
25
|
+
.replace(/-/g, "_")
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/^_+/, "");
|
|
28
|
+
variations.add(snakeCase);
|
|
29
|
+
|
|
30
|
+
// kebab-case
|
|
31
|
+
const kebabCase = name
|
|
32
|
+
.replace(/([A-Z])/g, "-$1")
|
|
33
|
+
.replace(/_/g, "-")
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/^-+/, "");
|
|
36
|
+
variations.add(kebabCase);
|
|
37
|
+
|
|
38
|
+
// camelCase
|
|
39
|
+
const camelCase = name
|
|
40
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
41
|
+
.replace(/^[A-Z]/, (char) => char.toLowerCase());
|
|
42
|
+
variations.add(camelCase);
|
|
43
|
+
|
|
44
|
+
// PascalCase
|
|
45
|
+
const pascalCase = name
|
|
46
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
47
|
+
.replace(/^[a-z]/, (char) => char.toUpperCase());
|
|
48
|
+
variations.add(pascalCase);
|
|
49
|
+
|
|
50
|
+
return Array.from(variations);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates file path patterns to try for a given block name and optional file name
|
|
55
|
+
* If fileName is provided, generates patterns using variations of both blockName (for directory) and fileName (for file)
|
|
56
|
+
* If fileName is not provided, uses blockName variations for both directory and file
|
|
57
|
+
*
|
|
58
|
+
* @param basePath - Base path (e.g., "@page-builder/blocks/")
|
|
59
|
+
* @param blockName - Block name in any format (e.g., "heroSection", "hero-section")
|
|
60
|
+
* @param fileName - Optional file name (e.g., "options.ts", "fields.ts"). If not provided, uses blockName
|
|
61
|
+
* @returns Array of path patterns to try
|
|
62
|
+
*/
|
|
63
|
+
export function generateFilePathPatterns(basePath: string, blockName: string, fileName?: string): string[] {
|
|
64
|
+
const patterns: string[] = [];
|
|
65
|
+
|
|
66
|
+
// Generate name variations for blockName (directory) and fileName (file)
|
|
67
|
+
const blockNameVariations = generateNameVariations(blockName);
|
|
68
|
+
const fileNameVariations = fileName ? generateNameVariations(fileName.replace(/\.\w+$/, "")) : blockNameVariations;
|
|
69
|
+
const fileExtension = fileName ? fileName.split(".").pop() || "" : "vue";
|
|
70
|
+
|
|
71
|
+
// For each block name variation (directory), try each file name variation
|
|
72
|
+
for (const blockVariation of blockNameVariations) {
|
|
73
|
+
// Convert block variation to different cases for directory
|
|
74
|
+
const blockKebab = blockVariation
|
|
75
|
+
.replace(/([A-Z])/g, "-$1")
|
|
76
|
+
.replace(/_/g, "-")
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.replace(/^-+/, "");
|
|
79
|
+
const blockPascal = blockVariation
|
|
80
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
81
|
+
.replace(/^[a-z]/, (char) => char.toUpperCase());
|
|
82
|
+
const blockCamel = blockVariation
|
|
83
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
84
|
+
.replace(/^[A-Z]/, (char) => char.toLowerCase());
|
|
85
|
+
const blockSnake = blockVariation
|
|
86
|
+
.replace(/([A-Z])/g, "_$1")
|
|
87
|
+
.replace(/-/g, "_")
|
|
88
|
+
.toLowerCase();
|
|
89
|
+
|
|
90
|
+
for (const fileVariation of fileNameVariations) {
|
|
91
|
+
// Convert file variation to different cases
|
|
92
|
+
const fileKebab = fileVariation
|
|
93
|
+
.replace(/([A-Z])/g, "-$1")
|
|
94
|
+
.replace(/_/g, "-")
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/^-+/, "");
|
|
97
|
+
const filePascal = fileVariation
|
|
98
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
99
|
+
.replace(/^[a-z]/, (char) => char.toUpperCase());
|
|
100
|
+
const fileCamel = fileVariation
|
|
101
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
102
|
+
.replace(/^[A-Z]/, (char) => char.toLowerCase());
|
|
103
|
+
const fileSnake = fileVariation
|
|
104
|
+
.replace(/([A-Z])/g, "_$1")
|
|
105
|
+
.replace(/-/g, "_")
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
|
|
108
|
+
// Generate patterns: {blockVariation}/{fileVariation}.{ext}
|
|
109
|
+
patterns.push(`${basePath}${blockKebab}/${fileKebab}.${fileExtension}`);
|
|
110
|
+
patterns.push(`${basePath}${blockKebab}/${filePascal}.${fileExtension}`);
|
|
111
|
+
patterns.push(`${basePath}${blockKebab}/${fileCamel}.${fileExtension}`);
|
|
112
|
+
patterns.push(`${basePath}${blockKebab}/${fileSnake}.${fileExtension}`);
|
|
113
|
+
|
|
114
|
+
patterns.push(`${basePath}${blockPascal}/${fileKebab}.${fileExtension}`);
|
|
115
|
+
patterns.push(`${basePath}${blockPascal}/${filePascal}.${fileExtension}`);
|
|
116
|
+
patterns.push(`${basePath}${blockPascal}/${fileCamel}.${fileExtension}`);
|
|
117
|
+
patterns.push(`${basePath}${blockPascal}/${fileSnake}.${fileExtension}`);
|
|
118
|
+
|
|
119
|
+
patterns.push(`${basePath}${blockCamel}/${fileKebab}.${fileExtension}`);
|
|
120
|
+
patterns.push(`${basePath}${blockCamel}/${filePascal}.${fileExtension}`);
|
|
121
|
+
patterns.push(`${basePath}${blockCamel}/${fileCamel}.${fileExtension}`);
|
|
122
|
+
patterns.push(`${basePath}${blockCamel}/${fileSnake}.${fileExtension}`);
|
|
123
|
+
|
|
124
|
+
patterns.push(`${basePath}${blockSnake}/${fileKebab}.${fileExtension}`);
|
|
125
|
+
patterns.push(`${basePath}${blockSnake}/${filePascal}.${fileExtension}`);
|
|
126
|
+
patterns.push(`${basePath}${blockSnake}/${fileCamel}.${fileExtension}`);
|
|
127
|
+
patterns.push(`${basePath}${blockSnake}/${fileSnake}.${fileExtension}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Remove duplicates and return
|
|
132
|
+
return [...new Set(patterns)];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Converts a any type of string to camelCase
|
|
136
|
+
* e.g., "hero_section" or "HeroSection" or "hero-section" or "heroSection" -> "HeroSection"
|
|
137
|
+
* e.g., "component_not_found" or "ComponentNotFound" or "component-not-found" or "componentNotFound" -> "ComponentNotFound"
|
|
138
|
+
*/
|
|
139
|
+
export function toCamelCase(input: string): string {
|
|
140
|
+
if (!input) return "";
|
|
141
|
+
|
|
142
|
+
// Replace all non-alphanumeric characters with spaces
|
|
143
|
+
const cleaned = input.replace(/[^a-zA-Z0-9]+/g, " ").trim();
|
|
144
|
+
|
|
145
|
+
// Split on spaces or uppercase-to-lowercase boundaries
|
|
146
|
+
const parts = cleaned.split(/\s+/).flatMap((part) => part.split(/([A-Z][a-z]*)/).filter(Boolean));
|
|
147
|
+
|
|
148
|
+
if (parts.length === 0) return "";
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
parts[0].toLowerCase() +
|
|
152
|
+
parts
|
|
153
|
+
.slice(1)
|
|
154
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
|
|
155
|
+
.join("")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function toNiceName(input: string): string {
|
|
160
|
+
if (!input) return "";
|
|
161
|
+
|
|
162
|
+
// Replace underscores and hyphens with spaces
|
|
163
|
+
const cleaned = input.replace(/[_-]/g, " ");
|
|
164
|
+
|
|
165
|
+
// Split on uppercase-to-lowercase boundaries (e.g., "FaqSection" -> ["Faq", "Section"])
|
|
166
|
+
// This regex finds positions where a lowercase letter is followed by an uppercase letter
|
|
167
|
+
const parts = cleaned
|
|
168
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
169
|
+
.split(/\s+/)
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
|
|
172
|
+
if (parts.length === 0) return "";
|
|
173
|
+
|
|
174
|
+
// Capitalize first letter of each word and join with spaces
|
|
175
|
+
return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
|
|
176
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ref, shallowRef, type Ref } from "vue";
|
|
2
|
+
import type { EditorFieldConfig } from "./fieldConfig";
|
|
3
|
+
import type { Block } from "../types/Block";
|
|
4
|
+
import type { Layout } from "../types/Layout";
|
|
5
|
+
import { generateNameVariations, generateFilePathPatterns, toCamelCase } from "./helpers";
|
|
6
|
+
// Dynamic imports for all page builder blocks and layouts
|
|
7
|
+
// IMPORTANT: These globs use the @page-builder alias which must be configured in the CONSUMING APP's vite.config.ts
|
|
8
|
+
// The globs are evaluated by the consuming app's Vite build, so they resolve relative to the consuming app's project root
|
|
9
|
+
// The consuming app should have: "@page-builder": fileURLToPath(new URL("../page-builder", import.meta.url))
|
|
10
|
+
// Using eager: true to load all modules immediately so we can access component metadata (name, props, icon)
|
|
11
|
+
const blockModules = import.meta.glob("@page-builder/blocks/**/*.vue", { eager: true });
|
|
12
|
+
const blockFieldsModules = import.meta.glob("@page-builder/blocks/**/fields.ts", { eager: true });
|
|
13
|
+
const layoutModules = import.meta.glob("@page-builder/layout/**/*.vue", { eager: true });
|
|
14
|
+
// Load all thumbnail images - Vite will process these as assets and provide URLs
|
|
15
|
+
// For images, Vite returns the URL as the default export when using eager: true
|
|
16
|
+
const thumbnailModules = import.meta.glob("@page-builder/blocks/**/thumbnail.png", { eager: true });
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Registry of all page builder blocks
|
|
20
|
+
* Automatically populated from the /page-builder/blocks directory of your app
|
|
21
|
+
*/
|
|
22
|
+
export const pageBuilderBlocks: Ref<Record<string, Block>> = shallowRef({});
|
|
23
|
+
export const pageBuilderLayouts: Ref<Record<string, Layout>> = shallowRef({});
|
|
24
|
+
const pageBuilderBlockFields: Ref<Record<string, any>> = ref({});
|
|
25
|
+
|
|
26
|
+
////////////////////////////////////////////////////////////
|
|
27
|
+
// Methods
|
|
28
|
+
////////////////////////////////////////////////////////////
|
|
29
|
+
|
|
30
|
+
export function getBlocks(): Record<string, Block> {
|
|
31
|
+
return pageBuilderBlocks.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getLayouts(): Record<string, Layout> {
|
|
35
|
+
return pageBuilderLayouts.value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the thumbnail URL for a block directory
|
|
40
|
+
* @param directory - Block directory path (e.g., "@page-builder/blocks/hero-section")
|
|
41
|
+
* @returns Thumbnail URL or undefined if not found
|
|
42
|
+
*/
|
|
43
|
+
export function getBlockThumbnailUrl(directory: string | undefined): string | undefined {
|
|
44
|
+
if (!directory) return undefined;
|
|
45
|
+
// Construct the thumbnail path from the directory
|
|
46
|
+
const thumbnailPath = `${directory}/thumbnail.png`;
|
|
47
|
+
// Look up the thumbnail in the preloaded modules
|
|
48
|
+
const thumbnailModule = thumbnailModules[thumbnailPath];
|
|
49
|
+
if (!thumbnailModule) return undefined;
|
|
50
|
+
// For images, Vite returns an object with a default property containing the URL
|
|
51
|
+
// When using eager: true, the module is already loaded
|
|
52
|
+
return (thumbnailModule as any).default as string | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getBlockComponent(blockType: string): Block | undefined {
|
|
56
|
+
// Generate name variations and try to find a match
|
|
57
|
+
const nameVariations = generateNameVariations(blockType);
|
|
58
|
+
|
|
59
|
+
for (const variation of nameVariations) {
|
|
60
|
+
const block = pageBuilderBlocks.value[variation];
|
|
61
|
+
if (block) {
|
|
62
|
+
return block;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getBlockFields(blockName: string): any {
|
|
70
|
+
try {
|
|
71
|
+
// Generate path variations for options.ts file
|
|
72
|
+
const pathVariations = generateFilePathPatterns("../page-builder/blocks/", blockName, "fields.ts");
|
|
73
|
+
// Find the path that exists in the loaded modules
|
|
74
|
+
const path = pathVariations.find((path) => blockFieldsModules[path]);
|
|
75
|
+
if (path && blockFieldsModules[path]) {
|
|
76
|
+
// Get the block fields
|
|
77
|
+
const blockFields = (blockFieldsModules[path] as any).default;
|
|
78
|
+
return blockFields || {};
|
|
79
|
+
}
|
|
80
|
+
return {};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("Error getting block fields for block: ", blockName, error);
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getLayoutFields(layoutName: string): Record<string, EditorFieldConfig> {
|
|
88
|
+
// Get the data from availableLayouts
|
|
89
|
+
const layout = Object.values(pageBuilderLayouts.value).find((layout) => layout.__name === layoutName);
|
|
90
|
+
if (!layout) return {};
|
|
91
|
+
|
|
92
|
+
return layout?.fields || {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function initialiseBlockFieldsRegistry(): void {
|
|
96
|
+
Object.keys(pageBuilderBlockFields.value).forEach((key) => {
|
|
97
|
+
delete pageBuilderBlockFields.value[key];
|
|
98
|
+
});
|
|
99
|
+
Object.entries(blockFieldsModules).forEach(([path, module]) => {
|
|
100
|
+
const blockFields = (module as any).default;
|
|
101
|
+
pageBuilderBlockFields.value[path] = blockFields;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function initialiseLayoutRegistry(): void {
|
|
106
|
+
Object.keys(pageBuilderLayouts.value).forEach((key) => {
|
|
107
|
+
delete pageBuilderLayouts.value[key];
|
|
108
|
+
});
|
|
109
|
+
Object.entries(layoutModules).forEach(([path, module]) => {
|
|
110
|
+
const layout = (module as any).default;
|
|
111
|
+
// exclude modules without name
|
|
112
|
+
if (!layout.label) return;
|
|
113
|
+
pageBuilderLayouts.value[path] = layout;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function initialiseBlockRegistry(): void {
|
|
118
|
+
// Clear existing registry
|
|
119
|
+
Object.keys(pageBuilderBlocks.value).forEach((key) => {
|
|
120
|
+
delete pageBuilderBlocks.value[key];
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Load all blocks from the glob pattern
|
|
124
|
+
// With eager: true, module is the actual module object, not a loader function
|
|
125
|
+
Object.entries(blockModules).forEach(([path, module]) => {
|
|
126
|
+
const component = (module as any).default;
|
|
127
|
+
if (component && component.__name) {
|
|
128
|
+
const blockType = toCamelCase(component.type || component.__name);
|
|
129
|
+
// Extract directory path from component path (e.g., "@page-builder/blocks/hero-section/hero-section.vue" -> "@page-builder/blocks/hero-section")
|
|
130
|
+
const directory = path.replace(/\/[^/]+\.vue$/, "");
|
|
131
|
+
const block: Block = {
|
|
132
|
+
fields: getBlockFields(blockType),
|
|
133
|
+
...component, // Component can override fields
|
|
134
|
+
directory: directory, // directory path where the block component is located (e.g., "@page-builder/blocks/hero-section")
|
|
135
|
+
type: blockType,
|
|
136
|
+
};
|
|
137
|
+
pageBuilderBlocks.value[blockType] = block;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function initialiseRegistry(): void {
|
|
143
|
+
initialiseLayoutRegistry();
|
|
144
|
+
initialiseBlockFieldsRegistry();
|
|
145
|
+
initialiseBlockRegistry();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Initialise the registry when the module is loaded
|
|
149
|
+
initialiseRegistry();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ValidatorFunction } from "./fieldConfig";
|
|
2
|
+
import { getBlockComponent, getLayoutFields } from "./registry";
|
|
3
|
+
import { toNiceName } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export function validateField(value: any, validator: ValidatorFunction) {
|
|
6
|
+
return validator(value);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ValidationResult {
|
|
10
|
+
title: string;
|
|
11
|
+
isValid: boolean;
|
|
12
|
+
errors: Record<string, string | boolean>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate all fields in the value
|
|
17
|
+
* @param value - The value to validate
|
|
18
|
+
* @param blocksKey - The key of the blocks in the value
|
|
19
|
+
* @param settingsKey - The key of the settings in the value
|
|
20
|
+
* @returns A record of validation results
|
|
21
|
+
*/
|
|
22
|
+
export async function validateAllFields(
|
|
23
|
+
value: any,
|
|
24
|
+
blocksKey: string = "blocks",
|
|
25
|
+
settingsKey: string = "settings"
|
|
26
|
+
): Promise<Record<string, ValidationResult>> {
|
|
27
|
+
const validationResults: Record<string, ValidationResult> = {};
|
|
28
|
+
|
|
29
|
+
// Validate settings first so it appears at the top
|
|
30
|
+
const settingsResult = await validateSettings(value, settingsKey);
|
|
31
|
+
if (settingsResult.errors && Object.keys(settingsResult.errors).length > 0) {
|
|
32
|
+
validationResults[settingsKey] = settingsResult;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate blocks
|
|
36
|
+
const blockResults = await validateBlocks(value, blocksKey);
|
|
37
|
+
Object.assign(validationResults, blockResults);
|
|
38
|
+
|
|
39
|
+
return validationResults;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function validateSettings(value: any, settingsKey: string = "settings"): Promise<ValidationResult> {
|
|
43
|
+
const validationResult: ValidationResult = {
|
|
44
|
+
title: "Settings",
|
|
45
|
+
isValid: true,
|
|
46
|
+
errors: {},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (!value[settingsKey]) return validationResult;
|
|
50
|
+
const layoutOptions = getLayoutFields(value[settingsKey].layout);
|
|
51
|
+
|
|
52
|
+
// Loop each field in the settings
|
|
53
|
+
for (const field in value[settingsKey]) {
|
|
54
|
+
const fieldConfig = layoutOptions[field];
|
|
55
|
+
// If the field has a validator, validate it
|
|
56
|
+
if (fieldConfig?.validator) {
|
|
57
|
+
const result = await validateField(value[settingsKey][field], fieldConfig.validator);
|
|
58
|
+
// If validation fails (returns false or a string), add to validation results
|
|
59
|
+
if (result !== true) {
|
|
60
|
+
validationResult.errors[field] = result;
|
|
61
|
+
validationResult.isValid = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return validationResult;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function validateBlocks(value: any, blocksKey: string = "blocks"): Promise<Record<string, ValidationResult>> {
|
|
70
|
+
const validationResults: Record<string, ValidationResult> = {};
|
|
71
|
+
// Get the blocks from the value
|
|
72
|
+
const blocks = value[blocksKey];
|
|
73
|
+
if (!blocks) return validationResults;
|
|
74
|
+
|
|
75
|
+
// Loop each block
|
|
76
|
+
for (const block of blocks) {
|
|
77
|
+
// Get the block type
|
|
78
|
+
const blockType = block.type;
|
|
79
|
+
// Get the block editor fields
|
|
80
|
+
const blockComponent = getBlockComponent(blockType);
|
|
81
|
+
|
|
82
|
+
// Add validation results entry for the section
|
|
83
|
+
validationResults[blockType] = {
|
|
84
|
+
title: toNiceName(blockType),
|
|
85
|
+
isValid: true,
|
|
86
|
+
errors: {},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Skip if no editor fields are found
|
|
90
|
+
if (Object.keys(blockComponent?.fields || {}).length === 0) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Loop each field in the block
|
|
95
|
+
for (const field in blockComponent?.fields || {}) {
|
|
96
|
+
const fieldConfig = blockComponent?.fields?.[field];
|
|
97
|
+
// If the field has a validator, validate it
|
|
98
|
+
if (fieldConfig?.validator) {
|
|
99
|
+
const result = await validateField(block[field], fieldConfig.validator);
|
|
100
|
+
// If validation fails (returns false or a string), add to validation results
|
|
101
|
+
if (result !== true) {
|
|
102
|
+
validationResults[blockType].errors[field] = result;
|
|
103
|
+
validationResults[blockType].isValid = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Return validation results if there are any errors, otherwise return true
|
|
109
|
+
return validationResults;
|
|
110
|
+
}
|